Compare commits

..
Author SHA1 Message Date
Elizabeth Thompson 876b8641e2 fix(sql): catch sqlglot ParseError when parsing RLS predicates
SQLStatement.parse_predicate called sqlglot.parse_one unguarded, so a
syntactically invalid RLS predicate raised a raw sqlglot ParseError.
Reachable via apply_rls (e.g. POST /api/v1/sqllab/estimate with
RLS_IN_SQLLAB enabled), this surfaced as an opaque 500 instead of a
typed 422.

Wrap the call to convert ParseError/SqlglotError into SupersetParseError,
mirroring the existing idiom in SQLStatement._parse.
2026-08-28 16:49:17 +00:00
rlei fc26991cd4 feat(plugin-chart-echarts): add gridline and axis tick controls (#43428) 2026-08-28 09:44:39 -07:00
shauryaandShaurya a5c68c8df9 fix(number-format): handle sub-byte values and unit rollover in memory formatter (#43549)
Co-authored-by: Shaurya <19599684+no-hup@users.noreply.github.com>
2026-08-28 09:44:19 -07:00
Đỗ Trọng Hải 53e76afd70 feat(ci): enforce min release age for npm dep installation (#43164)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-08-28 23:42:41 +07:00
shauryaandShaurya d997d363e3 docs: update frontend Node/npm prerequisites to match engines (#43546)
Co-authored-by: Shaurya <19599684+no-hup@users.noreply.github.com>
2026-08-28 23:16:34 +07:00
Lalith Kothuru 9a6f6ee0c0 docs: fix docstring parameter names that do not match signatures (#43630) 2026-08-28 22:57:43 +07:00
Joe Li 94dd3d049c fix(ci): repair scheduled pre-commit drift (#43603) 2026-08-28 22:56:18 +07:00
b3f718da62 fix(explore): keep certification badges after saving or swapping a dataset (#43319)
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 08:47:36 -07:00
Mehmet Salih YavuzandEnzo Martellucci abf338d611 feat(filters): search filter values server-side in Explore (#43518)
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
2026-08-28 16:46:09 +03:00
Mehmet Salih Yavuz e518b21994 fix(dashboard): keep the chart menu usable in fullscreen on production builds (#43555) 2026-08-28 15:47:48 +03:00
Mehmet Salih Yavuz e18f27e1ce fix(native-filters): allow clearing an optional "select first value" filter (#43405) 2026-08-28 15:46:46 +03:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9bf457dea6 chore(deps): bump github/codeql-action/analyze from 4.37.7 to 4.37.8 (#43643)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-28 15:26:18 +03:00
Enzo Martellucci cb7b790733 fix(dataset): surface real validation message in save-error dialog (#43459) 2026-08-28 13:35:09 +02:00
Alexandru Soare 8bec85158c feat(modals): add renderExtraFields prop to chart and dashboard properties modals (#43622) 2026-08-28 13:18:51 +03:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> a30e4a4350 chore(deps): bump github/codeql-action/init from 4.37.7 to 4.37.8 (#43644)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-28 02:39:11 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 933dbbc2a2 chore(deps-dev): bump eslint-plugin-react-you-might-not-need-an-effect from 1.0.1 to 1.0.2 in /superset-frontend (#43645)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-28 02:39:06 -07:00
Enzo Martellucci a59b96c4f5 fix(explore): keep the row count label visible when the row limit is reached (#43296) 2026-08-28 10:51:27 +02:00
MafiandMatt Fitzgerald 12cd259c55 fix(dataset): preserve validation error messages (#43631)
Co-authored-by: Matt Fitzgerald <matt.fitzgerald@preset.io>
2026-08-28 01:01:17 -04:00
Chandan P 3ddc3b1d56 fix(cache): fall back to default timeout when cache_timeout is None (#43149) 2026-08-27 21:50:53 -07:00
Chandan P 98ec6018df fix(embedded): honor "can view query" permission for guest users (#43151) 2026-08-27 21:49:52 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Joe Li
2ebd415b8a chore(deps-dev): bump mcp from 1.29.1 to 2.0.0 (#43611)
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-08-27 21:49:50 -07:00
81b3e85522 fix(echarts): guard cross-filter when labelMap entry is missing (#42559)
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 21:49:21 -07:00
Viktor Högberg fd64efd72d fix(uploads): set catalog on datasets created by file upload (#43301) 2026-08-27 21:46:51 -07:00
bucketbase26andJoe Li e39bfb255b fix(explore): prevent duplicate Date Range tooltips (#43425)
Co-authored-by: Joe Li <joe@preset.io>
2026-08-27 21:02:57 -07:00
b7301ac88a fix: Dashboard export with charts from multiple databases (#37120)
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 20:27:54 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> fa59b44cfe chore(deps): bump mapbox-gl from 3.28.1 to 3.29.0 in /superset-frontend (#43619)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 19:58:55 -07:00
8c5be889d9 fix(cockroachdb): replace abandoned cockroachdb package with sqlalchemy-cockroachdb (#43501)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 19:48:13 -07:00
Evan RusackasandClaude b733b57e9e docs(mcp): document chart type plugin filtering config (#43597)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 19:47:07 -07:00
Joe Li 13e97ba913 fix(sql-lab): fix transparent background in ag-grid native menus (#43368) 2026-08-27 16:56:36 -07:00
Evan RusackasandClaude Sonnet 5 f9e43a37d8 docs(theming): document THEME_DEFAULT_MODE config setting (#43601)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 16:19:40 -07:00
Evan RusackasandClaude 5ff44c5ed6 docs(cli): document import_directory --username option (#43602)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 16:19:25 -07:00
Elizabeth ThompsonandClaude Opus 4.8 7b145a520c fix(tags): catch TemplateError when validating access for tagged SQL Lab queries (#43423)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 15:06:46 -07:00
Elizabeth Thompson 9f62aace7e fix(examples): replace deprecated granularity_sqla with granularity (#43432) 2026-08-27 15:06:42 -07:00
Elizabeth ThompsonandClaude Opus 4.8 20be48c085 fix(explore): catch TemplateError when validating access for permalinked query datasources (#43605)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 15:06:38 -07:00
Aurimas Navardauskas e1c7a51674 fix(mcp): flag failed tool calls with isError (#43374) 2026-08-27 14:59:19 -07:00
Gabriel Torres Ruiz ecbd6578b0 fix(db_engine_specs): always mask OAuth2 client secret in encrypted extra (#43491) 2026-08-27 18:42:51 -03:00
Amin Ghadersohi ea3206b076 fix(mcp): surface rejected filter columns in get_chart_data (#43598) 2026-08-27 17:36:50 -04:00
Amin Ghadersohi 478f612f25 fix(mcp): add open-world tool annotations (#43529) 2026-08-27 17:36:30 -04:00
Amin Ghadersohi a16bc0d94f fix(mcp): apply time grain overrides from extra_form_data (#43599) 2026-08-27 17:35:23 -04:00
Amin GhadersohiandClaude bcc6af6c5f fix(dao): don't mask transient OperationalError as a "not found" result (#43479)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 17:34:53 -04:00
Evan RusackasandClaude 94b59420b7 docs(embedding): document EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE (#43600)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 13:15:08 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Joe Li
6814a89230 chore(deps): bump gunicorn from 26.0.0 to 26.2.0 (#43609)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-27 13:08:37 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ebbe2e8714 chore(deps): bump the storybook group in /docs with 2 updates (#43610)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 13:08:32 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
b8acf4b207 chore(deps-dev): bump syntaqlite from 0.7.1 to 0.9.0 (#43612)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-27 13:08:28 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Joe Li
ba5d5091b9 chore(deps): bump holidays from 0.102 to 0.103 (#43614)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-27 13:08:24 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8c88c0d3ac chore(deps-dev): update teradatasql requirement from >=20.0.0.65 to >=20.0.0.66 (#43615)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 13:08:18 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
e9e652d22d chore(deps-dev): bump trino from 0.338.0 to 0.339.0 (#43616)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-27 13:08:14 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
8f82d851cf chore(deps-dev): bump tiktoken from 0.13.0 to 0.14.0 (#43617)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-27 13:08:09 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 83934c038e chore(deps-dev): bump the storybook group in /superset-frontend with 5 updates (#43618)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 13:08:05 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 35fc36a97e chore(deps-dev): bump baseline-browser-mapping from 2.11.15 to 2.11.16 in /superset-frontend (#43621)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 13:08:00 -07:00
Evan RusackasandClaude Opus 4.8 88c4e689e3 fix(pivot-table): keep D3_FORMAT-styled numbers intact in pivoted Excel export (#42601)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 12:55:57 -07:00
Evan RusackasandClaude Sonnet 5 29f4a05eb7 docs(security): document the RLS indicator badge on dataset list/Explore (#43591)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 12:54:21 -07:00
Evan RusackasandClaude 323741b043 docs(alerts-reports): document report/alert content format options (#43594)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 12:53:31 -07:00
Evan RusackasandClaude 5c3c6362ae docs(cache): document NATIVE_FILTER_OPTIONS_CACHE_TIMEOUT (#43592)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 12:53:27 -07:00
Evan RusackasandClaude c1b6ff340b docs(mcp): document the update_dataset_metric tool (#43596)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 12:53:19 -07:00
Evan RusackasandClaude 05830ebc77 docs: document PNG/PDF chart export options (#43593)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 12:53:09 -07:00
Evan RusackasandClaude Sonnet 5 74dd690102 docs(security): document EXTRA_EDITORS_RESOLVER (#43595)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 12:52:56 -07:00
Joe Li 22027ec24c fix(explore): require a value for adhoc filters before save (#43317) 2026-08-27 12:17:46 -07:00
a3072c0bf6 fix(deckgl): give handlebars tooltips an opaque background (#43195)
Co-authored-by: bikashJMV <bikash@jmv.co.in>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 12:17:32 -07:00
6d8cbbfe8d fix(api): stop export downloads inheriting the one-year Cache-Control (#43185)
Co-authored-by: bikashJMV <bikash@jmv.co.in>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-27 12:17:02 -07:00
Sepuri Sai KrishnaandEvan Rusackas 926e0e6a92 fix(chart-data): validate select()'s exclude option instead of raising 500s (#42410)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-27 12:14:33 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d013bc7a21 chore(deps): bump baseline-browser-mapping from 2.11.15 to 2.11.16 in /docs (#43613)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 12:11:32 -07:00
87c48cc473 feat(echarts): make bar chart label position user-configurable (#38695)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
2026-08-27 12:10:38 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> c2266f860c chore(deps-dev): update clickhouse-connect requirement from <2.0,>=1.7.1 to >=1.7.2,<2.0 (#43608)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 12:06:41 -07:00
Viktor Högberg e8aea6b6df fix: remove redundant 'tooltip' tooltip in scoping modal (#43455) 2026-08-27 11:10:26 -07:00
Evan Rusackas bde1c483b0 chore(deps): group Dependabot security-update PRs per directory (#43604) 2026-08-27 11:08:21 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>hainenber
ff1d9f9e09 chore(deps): bump dawidd6/action-download-artifact from 21 to 24 (#43441)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2026-08-27 10:24:38 -07:00
Amogh AtreyaandEvan Rusackas 76151beade fix(export): truncate exported filenames to prevent Windows path extraction errors (#42531) (#42541)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-27 08:48:58 -07:00
Amin GhadersohiandClaude 905a35d05f fix(mcp): resolve dashboard permalinks (#43482)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 11:35:36 -04:00
Mehmet Salih Yavuz fde0fbd315 fix(ci): unblock lint-frontend after the oxlint 1.79.0 bump (#43587) 2026-08-27 17:32:04 +03:00
Endi Monan 5879994e68 fix(charts): resolve export filenames from the chart, not the form data (#43280) 2026-08-27 08:31:32 -03:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d1b7a4fdb4 chore(deps): bump dompurify from 3.4.13 to 3.4.14 in /superset-frontend (#43579)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 03:43:02 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 5ea7d05d98 chore(deps): bump immer from 11.1.17 to 11.1.18 in /superset-frontend (#43581)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 03:42:57 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0ab64e305c chore(deps-dev): bump lerna from 10.0.0 to 10.0.1 in /superset-frontend (#43580)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 03:33:14 -07:00
Evan RusackasandClaude Opus 4.8 7d355a254e fix(Modal): stop the draggable modal from hijacking text selection (#43498)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 03:30:23 -07:00
Evan RusackasandClaude Opus 4.8 ca63bb532d fix(plugin-chart-table): preserve comparison arrow when a column-specific formatter entry is missing (#43494)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 03:30:17 -07:00
b71293acde fix(sqllab): stop a database with no extra from breaking SET_DATABASES (#43216)
Co-authored-by: bikashJMV <bikash@jmv.co.in>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-08-26 20:51:08 -07:00
0e172a6ff3 feat(waterfall): add show/hide toggles for X and Y axes (#42371)
Co-authored-by: Greg Neighbors <gregneighbors@Gregs-MacBook-Air-2.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Greg Neighbors <gregneighbors@Gregs-Air-2.lan>
2026-08-26 20:29:26 -07:00
rlei 1c66b71e23 feat(plugin-chart-echarts): allow decal patterns in custom ECharts options (#43427) 2026-08-26 18:01:52 -07:00
Alejandro Solares 6b0aa8714d chore(deps): raise Pillow/PyJWT floors to match resolved pins (#43519) 2026-08-26 15:55:14 -07:00
Vitor Avila 371e5e25e8 fix(trino): honor the verify arg for the user impersonation flow (#43534) 2026-08-26 19:44:30 -03:00
Luiz OtavioandClaude Opus 5 a140e74f5f fix(excel): handle duplicate column labels in xlsx export (#43561)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 16:39:03 -03:00
Amin Ghadersohi b8308a44a3 fix(pivot-table): exclude rollup totals from conditional formatting scale (#43481) 2026-08-26 15:08:07 -04:00
Amin Ghadersohi 13927f27e2 fix(mcp): raise SDK floor (#43530) 2026-08-26 15:04:59 -04:00
Amin Ghadersohi 434511cb37 fix(mcp): reject unsafe dashboard layout replacements (#43476) 2026-08-26 14:28:26 -04:00
JUST.in DO ITandClaude Sonnet 5 70c9203aa9 fix(sqllab): release DB connection before fetching query results from backend (#43371)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 10:37:42 -07:00
Alexandru Soare 455e6603c1 feat(reports): gate retry functionality behind ALERT_REPORTS_RETRY flag (#43553) 2026-08-26 09:18:11 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0597f36bb6 chore(deps-dev): bump oxlint from 1.78.0 to 1.79.0 in /superset-frontend (#43543)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-26 08:20:39 -07:00
Rafael BenitezandClaude Opus 4.8 e7ca8b8a57 fix(chart): render ECharts charts at full resolution in download-as-image (#43456)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-26 06:22:23 -04:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 686245a905 chore(deps-dev): bump oxfmt from 0.63.0 to 0.64.0 in /superset-websocket (#43537)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-26 02:17:00 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 5a4900abb1 chore(deps-dev): bump vitest from 4.1.10 to 4.1.11 in /superset-websocket (#43538)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-26 02:16:56 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 450e2edc2b chore(deps): bump @swc/core from 1.16.0 to 1.16.1 in /docs (#43539)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-26 02:16:53 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 2bc96a47a4 chore(deps): bump swagger-ui-react from 5.32.13 to 5.32.14 in /docs (#43540)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-26 02:16:49 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 622bc45c9e chore(deps-dev): bump @swc/core from 1.16.0 to 1.16.1 in /superset-frontend (#43541)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-26 02:16:45 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> e97db01482 chore(deps-dev): bump oxfmt from 0.63.0 to 0.64.0 in /superset-frontend (#43542)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-26 02:16:40 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 6194c852f1 chore(deps-dev): bump oxfmt from 0.63.0 to 0.64.0 in /docs (#43544)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-26 02:16:36 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 19eca6523f chore(deps): bump uuid from 14.0.1 to 14.0.2 in /superset-frontend (#43545)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-26 02:16:31 -07:00
Amin GhadersohiandClaude 1fd763bd29 fix(mcp): honor and validate chart filters (#43478)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-25 20:54:09 -07:00
b4f7114a09 fix(semantic-layer): warn that deleting a layer cascade-deletes its dependent views (#42845)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:47:45 -07:00
Vitor Avila 2dfe8a5bd9 fix(OAuth2): Support creating OAuth2 connections via SQLAlchemy URI (#43489) 2026-08-26 00:07:19 -03:00
62d74be0af fix(metadb): apply SUPERSET_META_DB_LIMIT after join instead of per-table (#36304) (#42598)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-25 19:31:11 -07:00
Shivam Goel 473c318335 chore(superset-core): add __init__.py to semantic_layers (#43528) 2026-08-25 18:07:28 -07:00
Evan RusackasandSuperset Dev 61ab0cdb5d fix(ocient): update GIS test fixtures for pyocient's relocated geo types (#43496)
Co-authored-by: Superset Dev <dev@superset.apache.org>
2026-08-25 17:18:02 -07:00
e6b9205821 fix(country-map): give Alborz its own ISO code instead of reusing Tehran's (#42429)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-08-25 16:59:48 -07:00
0dcb2ca53a feat(maps): Add Italy regions and autonomous provinces country map (#42309)
Co-authored-by: lum4chi <francesco.lumachi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-25 15:45:31 -07:00
BexultanandBexultan Mustafin 649d6f1b41 fix(mcp): defer unknown numeric types to compile (#43131)
Co-authored-by: Bexultan Mustafin <bexultan.mustafin@ffins.kz>
2026-08-25 15:35:00 -07:00
BexultanandBexultan Mustafin 90dab7cf61 fix(mcp): accept common chart input variants (#43130)
Co-authored-by: Bexultan Mustafin <bexultan.mustafin@ffins.kz>
2026-08-25 15:22:52 -07:00
Sepuri Sai KrishnaandJoe Li 68386a53ee chore(database): remove dead extra validation exception classes (#42411)
Co-authored-by: Joe Li <joe@preset.io>
2026-08-25 15:14:16 -07:00
b89da3e9fc docs(versioning): fix post-flip doc and comment drift (#43493)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 13:00:25 -07:00
Mike BridgeandClaude Fable 5 88d2c2954e feat(deletion-retention): persist purge block reason codes on the audit log (#43485)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 12:46:55 -07:00
Amin Ghadersohi f903e02d91 chore(deps): restore permissive marshmallow lower bound (>=3.0, <5) (#43521) 2026-08-25 14:38:44 -04:00
Maxime Beaucheminandsadpandajoe fc4d7221ec fix(explore): skip re-fetch when navigating away from /explore (#39506)
Co-authored-by: sadpandajoe <jcli38@gmail.com>
2026-08-25 11:32:25 -07:00
Chen, Ting-AnandJoe Li 3585e8235a fix(i18n): review Traditional Chinese count labels (#43063)
Co-authored-by: Joe Li <joe@preset.io>
2026-08-25 10:28:12 -07:00
Evan RusackasandSuperset Dev 34ffa37aaa chore(ci): drop inert SQLALCHEMY_WARN_20 flag from unit-test CI (#43495)
Co-authored-by: Superset Dev <dev@superset.apache.org>
2026-08-25 10:19:38 -07:00
Russlan Ramdowar 02b3e43b66 fix(number-format): preserve custom smart formatter id (#43439) 2026-08-25 10:11:52 -07:00
DmitryandDmitry Kucher f9530f31ab fix(table): keep each metric's own aggregate in the summary row by default (#43421)
Co-authored-by: Dmitry Kucher <dima@virtuman.com>
2026-08-25 10:10:02 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>rusackasClaude Opus 4.8
fd3849cef1 chore(deps): bump antd from 6.6.0 to 6.6.1 in /superset-frontend (#43509)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-25 05:56:18 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ef0ad01aa5 chore(deps-dev): bump the storybook group across 1 directory with 5 updates (#43507)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 05:52:46 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d480152735 chore(deps): bump the storybook group in /docs with 2 updates (#43505)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 02:46:30 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> cbfa94c4dd chore(deps): bump antd from 6.6.0 to 6.6.1 in /docs (#43506)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 02:46:26 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d2aeb29223 chore(deps-dev): bump baseline-browser-mapping from 2.11.14 to 2.11.15 in /superset-frontend (#43508)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 02:46:22 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7bad69f523 chore(deps): bump dayjs from 1.11.22 to 1.11.23 in /superset-frontend (#43510)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 02:46:17 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9dd8c42f3d chore(deps): bump dompurify from 3.4.12 to 3.4.13 in /superset-frontend (#43511)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 02:46:12 -07:00
6c2fef29cb feat(snowflake): Add support for OAuth 2.0 authentication (#36856)
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-24 21:22:14 -07:00
Evan RusackasandSuperset Dev bd7b739212 chore: remove obsolete pandas/SQLAlchemy version compat shim (#43497)
Co-authored-by: Superset Dev <dev@superset.apache.org>
2026-08-24 20:11:52 -07:00
f83fb7c0e0 fix(archived-list): use the semantic-layers-aware label for the dataset type (#43465)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-08-24 20:38:29 -04:00
a9d54a0037 fix(soft-delete): card-view chart delete shows the archive dialog (#43469)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-08-24 20:37:52 -04:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>rusackasClaude Opus 4.8
8b792ab660 chore(deps-dev): bump @swc/core from 1.15.47 to 1.16.0 in /superset-frontend (#43446)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 16:52:00 -07:00
Durgaprasad M LandEvan Rusackas 107204a1fc fix(sqllab): allow SQL Lab query owners to create charts without all_datasource_access (#42479)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-08-24 16:51:57 -07:00
7e3d092ac4 fix(soft-delete): name the recovery location in the archive confirmation (#43401)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 18:56:20 -04:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dfd057b920 chore(deps-dev): update clickhouse-connect requirement from <2.0,>=1.6.0 to >=1.7.1,<2.0 (#43419)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-24 13:54:23 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Đỗ Trọng Hải
52571a5b8b chore(deps): bump the rjsf group in /superset-frontend with 3 updates (#43440)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-08-24 13:54:19 -07:00
Joe Li ca3d7670b7 fix(ci): restore scheduled CI checks (#43266) 2026-08-24 12:31:50 -07:00
Joe Li 94855e9626 fix(frontend): ignore stale list responses (#43306) 2026-08-24 12:13:34 -07:00
Joe LiandClaude 7dbf71a379 fix(query-object): reject malformed ad-hoc metrics (#43353)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-24 12:12:25 -07:00
Joe LiandClaude Sonnet 5 24b95f9ca7 fix(gsheets): correctly format Date-column filter literals (#43355)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 12:12:04 -07:00
Joe Li c635d0754f fix(explore): honor column Label in filter search and pill (#43404) 2026-08-24 12:11:52 -07:00
Joe Li 31f06c0ee6 fix(dataset): correct Hours Offset filter bounds and grain-truncation order (#43173) 2026-08-24 11:47:16 -07:00
Joe LiandClaude Opus 4.8 27ec80c07b fix(explore): clear stale custom time-shift date error (#43146)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 11:46:48 -07:00
Joe Li a8a8b51afb fix(plugin-chart-table): guard row-indexed comparison-color lookups against undefined entries (#43139) 2026-08-24 11:45:25 -07:00
6ab21b381a fix(mysql): resolve wire-protocol column types and mutate rows from immutable results (#42821)
Co-authored-by: Jean Massucatto <massucattoj@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-24 11:45:15 -07:00
Đỗ Trọng Hải b0962ba5ed fix(plugin/chart/parallel-coordinate): prevent frontend crash with empty/undefined metrics when moving between pages quickly (#42392)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-08-24 11:44:50 -07:00
Joe LiandClaude Opus 4.8 bf5f3a9e6b test(dashboard): migrate drill-to-detail E2E from Cypress to Playwright (#41437)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-24 11:44:35 -07:00
c980b3a361 fix(mcp): tighten auth and request validation edge cases (#43388)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 10:58:59 -07:00
dc436c76f7 fix(datasets): keep a metric's warning text when editing from Explore (#43177)
Co-authored-by: bikashJMV <bikash@jmv.co.in>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-08-24 10:47:45 -07:00
6eced8e919 fix(roles): let the permissions dropdown size to its content (#43196)
Co-authored-by: bikashJMV <bikash@jmv.co.in>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-08-24 10:46:03 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1bde62f997 chore(deps-dev): bump gevent from 26.7.0 to 26.8.0 (#43411)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-24 10:21:06 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
f1f6347885 chore(deps-dev): bump prophet from 1.3.0 to 1.4.0 (#43412)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-24 10:21:02 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
8591f52ced chore(deps): bump pyarrow from 25.0.0 to 25.0.1 (#43414)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-24 10:20:59 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ceb75b9350 chore(deps): bump sqlalchemy from 2.0.51 to 2.0.52 (#43415)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-24 10:20:55 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> db539288ac chore(deps-dev): update teradatasql requirement from >=20.0.0.64 to >=20.0.0.65 (#43416)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-24 10:20:51 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
a392e8b102 chore(deps): bump marshmallow from 4.3.0 to 4.3.1 (#43417)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-24 10:20:48 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
e450acf1c7 chore(deps): bump sqlglot from 30.16.0 to 30.17.0 (#43418)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-24 10:20:44 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 2c02965f2b chore(deps): bump @swc/core from 1.15.47 to 1.16.0 in /docs (#43443)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-24 10:20:38 -07:00
Evan Rusackas f3142e7b15 fix(tags): fix broken import in daos/tag.py (#43466) 2026-08-24 10:15:08 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 649c062825 chore(deps): bump baseline-browser-mapping from 2.11.13 to 2.11.15 in /docs (#43444)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-24 09:52:03 -07:00
3f10d8b1cc fix(versioning): suppress automatic chart normalization changes (#43350)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 09:09:20 -07:00
Mehmet Salih Yavuz 15b98aa6da fix(plugin-chart-echarts): apply contribution before rename with time comparison (#43384) 2026-08-24 19:08:08 +03:00
7f1b41444c fix: add missing access checks to semantic-layer and theme endpoints (#43389)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 08:52:45 -07:00
8a32324184 fix: add missing ownership checks to tag, report-log, and dataset-schema endpoints (#43390)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 08:44:22 -07:00
Joe Li 4623d9219a fix: update in the wild link on community page (#43403) 2026-08-24 22:28:19 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dcac129b51 chore(deps): bump js-yaml from 5.2.3 to 5.3.0 in /docs (#43442)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-24 22:17:39 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7196ce8a0c chore(deps): bump docker/setup-buildx-action from 4.2.0 to 4.3.0 (#43445)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-24 22:16:57 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> e7756aeef9 chore(deps): bump immer from 11.1.16 to 11.1.17 in /superset-frontend (#43448)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-24 22:16:27 +07:00
DanielSwift1992 af859717a7 chore: remove two stale codecov ignore paths (#43438) 2026-08-24 22:15:57 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8fa48d73f6 chore(deps-dev): bump concurrently from 10.0.4 to 10.0.5 in /superset-frontend (#43449)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-24 22:04:38 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 90a3d93002 chore(deps): bump dayjs from 1.11.21 to 1.11.22 in /superset-frontend (#43450)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-24 22:04:09 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 5ffb72151e chore(deps): bump react-error-boundary from 6.1.2 to 6.1.3 in /superset-frontend (#43451)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-24 22:03:04 +07:00
Enzo Martellucci c3ed8b312d fix(security): surface extra_editors in dashboard/chart lists (#43392) 2026-08-24 11:49:29 +02:00
Elizabeth Thompson 9f505eb0cb fix(examples): replace deprecated timeseries_limit_metric with series_limit_metric (#43422) 2026-08-22 15:02:20 -07:00
Joe LiandClaude Sonnet 5 f2610e9dca fix(sqllab): default PostgreSQL port to 5432 in the dynamic connection form (#43351)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 15:59:33 -07:00
abdullah revehaandAbdullah Sahin 09bd8c98de feat(chart): enable cross-filter on temporal x-axis (bar/label click) (#42792)
Co-authored-by: Abdullah Sahin <you@example.comclear>
2026-08-21 15:45:06 -07:00
Aurimas Navardauskas 5812c0eab5 fix(mcp): fail closed when MCP_AUTH_FACTORY raises (#43329) 2026-08-21 15:43:51 -07:00
Mike BridgeandClaude Fable 5 5f3e34908c fix(charts): surface blocking alerts/reports when archiving a chart (#43402)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 19:33:32 -03:00
Evan RusackasandSuperset Dev 9781254363 fix(export): escape formula-triggering values consistently and bound post-processing inputs (#43396)
Co-authored-by: Superset Dev <dev@superset.apache.org>
2026-08-21 12:37:33 -07:00
65893113fe fix(import): tighten ownership and validation checks across asset importers (#43393)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 12:35:33 -07:00
04017f3956 fix(sqllab): re-validate access against rendered SQL and tighten cache/permalink scoping (#43394)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 12:35:06 -07:00
2b6044b624 fix: tighten SSRF validation, executor resolution, and cache scoping across reports/thumbnails (#43395)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 12:34:37 -07:00
Evan RusackasandSuperset Dev aee98b527d fix(charts): escape untrusted strings before rendering into chart tooltips and popups (#43397)
Co-authored-by: Superset Dev <dev@superset.apache.org>
2026-08-21 12:31:31 -07:00
edda9d1bbb fix(frontend): tighten SQL Lab autorun scoping and HTML-rendering defaults (#43398)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 12:31:08 -07:00
Joe LiandClaude Sonnet 5 03eac279e5 fix(explore): exclude permalink_key from chart URL params (#43354)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 11:43:18 -07:00
yousophandClaude Opus 4.8 fd7095df9a fix(explore): align viz type gallery thumbnails and Featured tag (#43373)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-21 10:40:37 -07:00
d422f5b4b6 fix: last date label hidden on time series x-axis (#39899) (#42299)
Co-authored-by: Prathamesh Hukkeri <prathamesh04@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-21 09:59:06 -07:00
Alexandru Soare a05a099987 fix(embedded): block custom SQL injection in guest user chart payloads (#43111) 2026-08-21 17:05:40 +03:00
Evan RusackasandClaude Code 05842a6350 feat(metrics): add MEDIAN/STDDEV_SAMP/VAR_SAMP as system-wide aggregates (#42895)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-21 04:15:17 -07:00
Evan RusackasandClaude Opus 4.8 e45dd2d3f1 ci: pull CI service images from GHCR mirror (fork-safe) [depends on #40880] (#40882)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-21 04:11:37 -07:00
Mehmet Salih Yavuz 65910abf21 fix(sqllab): disable Save dataset until the query runs successfully (#43330) 2026-08-21 12:56:42 +03:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 6e22050b50 chore(deps): bump the storybook group in /docs with 2 updates (#43377)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-21 01:10:41 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 132340b652 chore(deps): bump astral-sh/setup-uv from 10.0.0 to 10.0.1 (#43378)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-21 01:10:37 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 14408eb3db chore(deps-dev): bump the storybook group in /superset-frontend with 5 updates (#43379)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-21 01:10:33 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 87743ef3f8 chore(deps): bump dompurify from 3.4.12 to 3.4.13 in /superset-frontend (#43380)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-21 01:10:29 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> fb53761ec8 chore(deps-dev): bump baseline-browser-mapping from 2.11.13 to 2.11.14 in /superset-frontend (#43381)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-21 01:10:25 -07:00
Amin Ghadersohi f7d505e1fd fix(listview): stop card clicks creating a duplicate history entry (#43310) 2026-08-20 23:23:46 -04:00
22396d504a fix(ux): use title case for button labels (#40048)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-20 20:14:33 -07:00
Gaston LaterzaandClaude Opus 5 27ea5de44a fix(i18n-es): correct case/number collapse in the semantic-layer labels (#43311)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 20:03:57 -07:00
Joe LiandClaude Sonnet 5 18fc2c6228 fix(sqllab): stop copying a permalink when opening a saved query (#43147)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 15:44:20 -07:00
Amin Ghadersohi 18a36d04c7 fix(mcp): preserve user-authored result values (#43202) 2026-08-20 18:29:39 -04:00
ʈᵃᵢ 01b1d58ac9 fix(plugin-chart-echarts): restore tooltips for metrics labelled like… (#43369) 2026-08-20 14:58:11 -07:00
Joe Li 7441ce90ae fix(charts): align grain-less time comparisons safely (#43315) 2026-08-20 14:55:01 -07:00
Amin Ghadersohi 42ba2a4433 fix(reports): humanize day-of-month + day-of-week crontabs as OR (#43307) 2026-08-20 16:46:08 -04:00
madhushreeagandmadhushree agarwal 271564cb0d feat(config): add EXTRA_PANDAS_POSTPROCESSING_OPS extension point (#43337)
Co-authored-by: madhushree agarwal <madhushree_agarwal@apple.com>
2026-08-20 13:15:41 -07:00
JUST.in DO ITandClaude Sonnet 5 bcfb4346f6 fix(mcp): honor use_cache and cache_timeout in get_chart_data (#43349)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 13:01:41 -07:00
Joe Li b5fe00b407 test(frontend): shrink flaky/misplaced recently-archived e2e coverage to Jest unit tests (#43264) 2026-08-20 11:48:02 -07:00
Parman MohammadalizadehandEvan Rusackas 148ffaff50 fix(plugin-chart-echarts): omit stacked value labels on zero-height segments (#42756)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-20 11:04:19 -07:00
Ankit 83c6ea4e03 fix(explore): show the empty state when Samples returns no result payload (#43115) 2026-08-20 11:00:54 -07:00
Hans Yu 1ca5e61f54 chore: Unset future flag in engines and sessions. (#43273) 2026-08-20 10:58:37 -07:00
rlei 1ef12580a7 fix(chart): ignore chart actions for a chart no longer in state (#43228) 2026-08-20 10:57:21 -07:00
c0884c0f0c fix(explore): keep x-axis label when overriding Time Column with time comparison (#42875)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-20 10:52:22 -07:00
Sepuri Sai KrishnaandClaude Opus 5 2ff79bd495 fix(github): point the issue templates at labels that exist (#43357)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 10:13:36 -07:00
e7dccd44a7 fix(reports): prevent blank/partial report PDFs from virtualized charts (#43348)
Co-authored-by: Matt Fitzgerald <matt.fitzgerald@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-08-20 09:10:30 -04:00
dependabot[bot] fdd3945dde chore(deps): bump github/codeql-action/analyze from 4.37.6 to 4.37.7 (#43361)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-20 13:58:31 +03:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8b67fb4d30 chore(deps-dev): bump globals from 17.10.0 to 17.11.0 in /superset-websocket (#43360)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-20 00:41:49 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b288db91f9 chore(deps): bump github/codeql-action/init from 4.37.6 to 4.37.7 (#43362)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-20 00:41:43 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 80bba12d0f chore(deps-dev): bump globals from 17.10.0 to 17.11.0 in /docs (#43363)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-20 00:41:38 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 5d4358a149 chore(deps): bump google-auth-library from 11.0.1 to 11.0.2 in /superset-frontend (#43364)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-20 00:41:25 -07:00
921 changed files with 35708 additions and 10667 deletions
-2
View File
@@ -3,9 +3,7 @@ codecov:
after_n_builds: 4
ignore:
- "superset/migrations/versions/*.py"
- "superset-frontend/packages/superset-ui-demo/**/*"
- "**/*.stories.tsx"
- "**/*.stories.jsx"
coverage:
status:
project:
+1 -1
View File
@@ -1,6 +1,6 @@
name: Bug report
description: Report a bug to improve Superset's stability
labels: ["bug"]
labels: ["#bug"]
body:
- type: markdown
attributes:
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: Cosmetic Issue
about: Describe a cosmetic issue with CSS, positioning, layout, labeling, or similar
labels: "cosmetic-issue"
labels: "#bug:cosmetic"
---
## Screenshot
+26
View File
@@ -5,6 +5,10 @@ updates:
directory: "/"
schedule:
interval: "daily"
groups:
security:
applies-to: "security-updates"
patterns: ["*"]
cooldown:
default-days: 7
@@ -61,6 +65,9 @@ updates:
- npm
- dependabot
groups:
security:
applies-to: "security-updates"
patterns: ["*"]
rjsf:
patterns:
- "@rjsf/*"
@@ -98,6 +105,10 @@ updates:
labels:
- pip
- dependabot
groups:
security:
applies-to: "security-updates"
patterns: ["*"]
cooldown:
default-days: 7
@@ -105,6 +116,10 @@ updates:
directory: ".github/actions"
schedule:
interval: "daily"
groups:
security:
applies-to: "security-updates"
patterns: ["*"]
open-pull-requests-limit: 10
versioning-strategy: increase
cooldown:
@@ -115,6 +130,9 @@ updates:
schedule:
interval: "daily"
groups:
security:
applies-to: "security-updates"
patterns: ["*"]
storybook:
patterns:
- "@storybook/*"
@@ -142,6 +160,10 @@ updates:
labels:
- npm
- dependabot
groups:
security:
applies-to: "security-updates"
patterns: ["*"]
versioning-strategy: increase
cooldown:
default-days: 7
@@ -153,6 +175,10 @@ updates:
labels:
- npm
- dependabot
groups:
security:
applies-to: "security-updates"
patterns: ["*"]
open-pull-requests-limit: 10
versioning-strategy: increase
cooldown:
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
python-version: "3.11"
enable-cache: true
+2 -2
View File
@@ -67,7 +67,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -78,6 +78,6 @@ jobs:
# queries: security-extended,security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
category: "/language:${{matrix.language}}"
@@ -105,6 +105,7 @@ jobs:
tool: customSmallerIsBetter
output-file-path: bundle-size-summary.json
external-data-json-path: bundle-size-history.json
github-token: ${{ secrets.GITHUB_TOKEN }}
fail-on-alert: false
summary-always: true
+1 -1
View File
@@ -93,7 +93,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Copy image to GHCR
env:
+2 -2
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@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
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@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
continue-on-error: true
with:
workflow: superset-python-integrationtest.yml
+1 -1
View File
@@ -140,7 +140,7 @@ jobs:
run: |
yarn install --check-cache
- name: Download database diagnostics from integration tests
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
with:
workflow: superset-python-integrationtest.yml
run_id: ${{ github.event.workflow_run.id }}
+4 -4
View File
@@ -78,14 +78,14 @@ jobs:
USE_DASHBOARD: ${{ github.event.inputs.use_dashboard == 'true' || 'false' }}
services:
postgres:
image: postgres:17-alpine
image: ghcr.io/apache/superset/ci/postgres:17-alpine
env:
POSTGRES_USER: superset
POSTGRES_PASSWORD: superset
ports:
- 15432:5432
redis:
image: redis:7-alpine
image: ghcr.io/apache/superset/ci/redis:7-alpine
ports:
- 16379:6379
steps:
@@ -186,14 +186,14 @@ jobs:
GITHUB_TOKEN: ${{ github.token }}
services:
postgres:
image: postgres:17-alpine
image: ghcr.io/apache/superset/ci/postgres:17-alpine
env:
POSTGRES_USER: superset
POSTGRES_PASSWORD: superset
ports:
- 15432:5432
redis:
image: redis:7-alpine
image: ghcr.io/apache/superset/ci/redis:7-alpine
ports:
- 16379:6379
steps:
@@ -53,9 +53,7 @@ jobs:
mysql+mysqldb://superset:superset@127.0.0.1:13306/superset?charset=utf8mb4&binary_prefix=true
services:
mysql:
image: mysql:8.0
# Authenticated pulls use our higher Docker Hub rate limit. Empty on
# fork PRs (secrets unavailable) -> runner falls back to anonymous.
image: ghcr.io/apache/superset/ci/mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
ports:
@@ -66,7 +64,7 @@ jobs:
--health-timeout=5s
--health-retries=5
redis:
image: redis:7-alpine
image: ghcr.io/apache/superset/ci/redis:7-alpine
options: --entrypoint redis-server
ports:
- 16379:6379
@@ -143,7 +141,7 @@ jobs:
SUPERSET__SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://superset:superset@127.0.0.1:15432/superset
services:
postgres:
image: postgres:17-alpine
image: ghcr.io/apache/superset/ci/postgres:17-alpine
env:
POSTGRES_USER: superset
POSTGRES_PASSWORD: superset
@@ -152,7 +150,7 @@ jobs:
# GitHub action runner's default installations
- 15432:5432
redis:
image: redis:7-alpine
image: ghcr.io/apache/superset/ci/redis:7-alpine
ports:
- 16379:6379
steps:
@@ -202,7 +200,7 @@ jobs:
sqlite:///${{ github.workspace }}/.temp/examples.db?check_same_thread=true
services:
redis:
image: redis:7-alpine
image: ghcr.io/apache/superset/ci/redis:7-alpine
ports:
- 16379:6379
steps:
@@ -52,7 +52,7 @@ jobs:
SUPERSET__SQLALCHEMY_EXAMPLES_URI: presto://localhost:15433/memory/default
services:
postgres:
image: postgres:17-alpine
image: ghcr.io/apache/superset/ci/postgres:17-alpine
env:
POSTGRES_USER: superset
POSTGRES_PASSWORD: superset
@@ -61,7 +61,7 @@ jobs:
# GitHub action runner's default installations
- 15432:5432
presto:
image: starburstdata/presto:350-e.6
image: ghcr.io/apache/superset/ci/presto:350-e.6
env:
POSTGRES_USER: superset
POSTGRES_PASSWORD: superset
@@ -70,7 +70,7 @@ jobs:
# GitHub action runner's default installations
- 15433:8080
redis:
image: redis:7-alpine
image: ghcr.io/apache/superset/ci/redis:7-alpine
ports:
- 16379:6379
steps:
@@ -116,7 +116,7 @@ jobs:
UPLOAD_FOLDER: /tmp/.superset/uploads/
services:
postgres:
image: postgres:17-alpine
image: ghcr.io/apache/superset/ci/postgres:17-alpine
env:
POSTGRES_USER: superset
POSTGRES_PASSWORD: superset
@@ -125,7 +125,7 @@ jobs:
# GitHub action runner's default installations
- 15432:5432
redis:
image: redis:7-alpine
image: ghcr.io/apache/superset/ci/redis:7-alpine
ports:
- 16379:6379
steps:
@@ -53,12 +53,6 @@ jobs:
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "next"]') }}
env:
PYTHONPATH: ${{ github.workspace }}
# Promotes the SQLAlchemy 2.0 deprecation warnings already locked in as
# errors via pytest.ini's `filterwarnings` to actually run in CI, so a
# regression on those fails the build instead of relying on a
# contributor remembering to set this locally. See the migration
# battleplan: https://github.com/apache/superset/discussions/40273
SQLALCHEMY_WARN_20: "1"
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -75,13 +69,18 @@ jobs:
SUPERSET_SECRET_KEY: not-a-secret
run: |
pytest --durations-min=0.5 --cov-report= --cov=superset ./tests/common ./tests/unit_tests --cache-clear --maxfail=50 --junit-xml=test-results/junit-unit.xml
# COVERAGE_FILE keeps these scoped gates off the default .coverage that
# the step above wrote. pytest-cov starts a fresh data file per run, so
# without it the last gate replaces the full-suite data and the report
# uploaded below contains only that gate's subtree -- every other file
# then reads as uncovered, and patch coverage on an unrelated PR is 0%.
- name: Python 100% coverage unit tests
env:
SUPERSET_TESTENV: true
SUPERSET_SECRET_KEY: not-a-secret
run: |
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100 --junit-xml=test-results/junit-sql-coverage.xml
pytest --durations-min=0.5 --cov=superset/semantic_layers/ ./tests/unit_tests/semantic_layers/ --cache-clear --cov-fail-under=100 --junit-xml=test-results/junit-semantic-layers-coverage.xml
COVERAGE_FILE="${RUNNER_TEMP}/.coverage.sql" pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100 --junit-xml=test-results/junit-sql-coverage.xml
COVERAGE_FILE="${RUNNER_TEMP}/.coverage.semantic_layers" pytest --durations-min=0.5 --cov=superset/semantic_layers/ ./tests/unit_tests/semantic_layers/ --cache-clear --cov-fail-under=100 --junit-xml=test-results/junit-semantic-layers-coverage.xml
- name: Upload code coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
+1
View File
@@ -10,6 +10,7 @@
.stylelintignore
.flake8
.nvmrc
.npmrc
.rat-excludes
.swcrc
.*log
@@ -1,262 +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.
-->
# [SIP] Proposal for a dashboard component Extensions contribution point
> **Companion SIP:** Pairs with [`SIP.md`](SIP.md) (first-class iframe component +
> runtime CSP allowlist). That SIP is the **reference implementation** that proves
> this contribution point: the iframe's UI becomes an extension-contributed
> dashboard component, while its security-sensitive CSP backend stays in core.
>
> **Status:** Draft — POC tracked in `feat/csp-runtime-allowlist-iframe`.
## Motivation
Adding a new dashboard layout component to Superset today is a **core-only,
high-friction** operation. The iframe component in the companion SIP had to touch
~12 files: a type constant, the `componentLookup` map, the builder palette, and
**seven hardcoded behavior maps** keyed by component-type string
(`isValidChild`, `componentIsResizable`, `newComponentFactory`,
`shouldWrapChildInRow`, `getDetailedComponentWidth`, `isDashboardEmpty`, plus the
prop bundle injected by `DashboardComponent.tsx`). Component types are a **closed
enum** baked into core.
There is a legacy escape hatch — the `DashboardComponentsRegistry` /
`DYNAMIC_TYPE` path (`src/visualizations/dashboardComponents/`) — but it is an
**antique that should be deprecated**:
- It is disconnected from the modern VS Code-style Extensions framework
(`@apache-superset/core`, `ENABLE_EXTENSIONS`), which already has contribution
points for `commands`, `menus`, `views`, `editors`, and `chat`.
- Components registered through it are **second-class**: `DynamicComponent`
renders them in a generic wrapper that only passes `dashboardData`. They do not
receive the first-class layout lifecycle (edit mode, meta editing, resize, DnD)
and cannot declare their own layout behavior.
We want a **single, modern way** to contribute a first-class dashboard layout
component — via the Extensions framework — and to deprecate the legacy registry.
The iframe component is the ideal pilot because it is self-contained.
## Proposed Change
### 1. A `dashboardComponents` contribution point
Add `dashboardComponents` to the Extensions `Contributions` interface
(`packages/superset-core/src/contributions/index.ts`), alongside `views`,
`commands`, etc., with a public registration API mirroring the existing ones
(`registerDashboardComponent` returning a `Disposable`), exposed on
`window.superset.dashboardComponents` and wired into `ExtensionsLoader`.
### 2. The Dashboard Component Contract (the heart of this SIP)
The contract has two halves. Getting this right is the real work — it becomes a
**public API Superset must support indefinitely**.
**(a) Declarative behavior metadata** — replaces the seven hardcoded util maps:
```ts
interface DashboardComponentContribution {
id: string; // unique type key, namespaced, e.g. "my-org.iframe"
name: string; // palette label
description?: string;
icon: string; // contributed icon id or known icon name
resizable?: boolean; // -> componentIsResizable
defaultMeta?: { // -> newComponentFactory
width?: number;
height?: number;
[key: string]: unknown;
};
nesting?: { // -> isValidChild / shouldWrapChildInRow
validParents?: string[]; // e.g. [GRID, ROW, COLUMN, TAB]
wrapInRow?: boolean;
minWidth?: number; // -> getDetailedComponentWidth
};
isUserContent?: boolean; // -> isDashboardEmpty
loadComponent: () => Promise<{ default: ComponentType<DashboardComponentProps> }>;
}
```
**(b) Runtime props contract** — a small, stable surface. Crucially, **the host
owns the chrome** (the `Draggable` + `ResizableContainer` + `HoverMenu`/delete
wrapper that every current `componentLookup` component re-implements today). The
extension component renders only its *content* and, optionally, an *editor*:
```ts
interface DashboardComponentProps {
id: string;
meta: Record<string, unknown>;
editMode: boolean;
updateMeta: (patch: Record<string, unknown>) => void; // wraps updateComponents
// resize/drag/delete handled by the host wrapper, NOT the component
}
```
This is a strict improvement over the status quo: the iframe component in the
companion PR hand-rolls the Draggable/Resizable/HoverMenu wrapper; under this
contract that boilerplate moves into the host once, and contributed components
shrink to "render content + edit meta."
### 3. Registry-driven core
Refactor `componentLookup` and the seven behavior maps to consult a registry,
with the **built-in leaf components seeded into it** at startup. Structural
container components (Chart, Tabs, Row, Column, Header) *are* the layout engine
and stay bespoke; the contribution point targets **leaf/content components**
(today: Markdown, Divider, Iframe; tomorrow: anything). `DashboardComponent.tsx`
resolves contributed types through the registry and renders them inside the
shared host chrome.
### 4. Deprecate `DashboardComponentsRegistry` / `DYNAMIC_TYPE`
Mark the legacy registry and `DYNAMIC_TYPE` deprecated. Provide a shim so existing
dynamic components keep working, with a migration note pointing at the new
contribution point. Removal happens in a later major per Superset's deprecation
policy.
### 5. Graceful fallback for unknown types
A saved dashboard layout stores component **type strings** in its position JSON.
If a dashboard references a type whose extension is disabled/uninstalled, the host
must render a non-destructive placeholder ("This component requires the *X*
extension") and **preserve the meta on save** so re-enabling the extension
restores it. The layout engine already tolerates unknown types defensively
(`componentLookup[type]` → null; `isValidChild` → false); this SIP makes that an
intentional, user-visible contract rather than silent breakage.
### 6. Backend: APIs yes, security policy no
The Extensions framework **already** lets a component contribute a backend REST
API: the `@api` decorator (`superset-core/.../rest_api/decorators.py`) detects
extension context and registers the route via `appbuilder.add_api()` at entrypoint
import, serving it under `/extensions/{publisher}/{name}/...` and auto-creating
the endpoint's FAB permission. **No new work is required for an extension to ship
an API.**
What an extension **cannot** do today, and what this SIP explicitly leaves to
core:
- **Role policy for a permission.** Endpoint permissions are auto-created, but
whether a permission is *Admin-only* (e.g. via
`SupersetSecurityManager.ADMIN_ONLY_VIEW_MENUS`) is decided in core at
`sync_role_definitions` time. The manifest's `permissions: list[str]` field is
currently **dormant** (never read), and the `ContributionProcessorRegistry` that
would process it is scaffolding that is not wired into the load pipeline.
- **Security-sensitive request hooks** (e.g. rewriting CSP/Talisman headers).
This is exactly why the companion CSP feature keeps its backend in core: the
component *UI* is extension-shaped, but punching holes in the CSP and gating it
admin-only are core security responsibilities.
A **future, optional** extension of this SIP could finish wiring
`ContributionProcessorRegistry` + a manifest permission-policy schema so
extensions can declare role policy — but that is itself a security-review-worthy
change and is out of scope here.
## New or Changed Public Interfaces
- **New contribution point** `dashboardComponents` on the `Contributions`
interface; new `registerDashboardComponent(...) -> Disposable` API; new
`window.superset.dashboardComponents` namespace.
- **New public types** `DashboardComponentContribution` and
`DashboardComponentProps` (the contract) — these become long-term public API.
- **Changed (internal → registry-driven)** `componentLookup` and the seven
behavior util maps; `DashboardComponent.tsx` resolution path; the host gains a
shared component-chrome wrapper.
- **Deprecated** `DashboardComponentsRegistry`, `DYNAMIC_TYPE`,
`NewDynamicComponent`, `setupDashboardComponents`.
## New dependencies
None. Reuses the existing Extensions framework (module federation, manifest
schema, `@api` decorator) and the existing functional-registry utilities.
## Migration Plan and Compatibility
- **No DB migration.** This is a frontend/framework change plus the (already
supported) extension API path.
- **Layout JSON is unchanged** — component types remain type strings. The new
fallback behavior makes *unknown* types degrade gracefully instead of rendering
nothing.
- **Backwards compatible:** built-in components are seeded into the registry, so
existing dashboards render identically. Legacy `DYNAMIC_TYPE` components keep
working via a deprecation shim.
- **Rollout:** the contribution point is only active under `ENABLE_EXTENSIONS`;
with it off, behavior is identical to today.
## Rejected Alternatives
- **Keep / extend `DashboardComponentsRegistry`.** It is disconnected from the
modern Extensions framework and produces second-class components. Deprecating it
in favor of one contribution model is the goal, not a side effect.
- **Require all built-in components to become extensions.** Chart/Tabs/Row/Column
are the layout engine; extracting them is high-risk and low-value. The
contribution point *adds* leaf components; it does not mandate extraction.
- **Let the extension component own its own DnD/resize chrome** (as
`componentLookup` components do today). Rejected: it bloats the contract,
duplicates host logic, and makes the public API fragile. The host owns chrome.
- **One combined SIP with the CSP feature.** Rejected: the framework change and
the security-sensitive feature are distinct discussions with different
reviewers and risk profiles, even though they share a POC branch.
- **Move the CSP permission/role policy into the extension.** Not supported today
(dormant manifest `permissions`, unwired contribution processor) and
undesirable: admin-only gating and CSP-header rewriting are core security
responsibilities.
## Implementation Status (POC)
Implemented on the POC branch (`@apache-superset/core` mirrors the `chat`
contribution-point pattern from #41000/#41205):
- [x] `DashboardComponentDefinition` + `DashboardComponentProps` contract types
(`packages/superset-core/src/dashboardComponents`), added to the
`Contributions` interface and the package's subpath exports
- [x] `dashboardComponents` contribution point: host `DashboardComponentsProvider`
registry + public `registerDashboardComponent`/`getDashboardComponents` API
(`src/core/dashboardComponents`), exposed on `window.superset` via
`ExtensionsStartup` + `Namespaces`
- [x] Shared host component-chrome wrapper `DashboardExtensionComponent`
(owns Draggable/Resizable/HoverMenu/Delete; reads `resizable` from the
definition) behind the new `EXTENSION_TYPE`
- [x] `componentLookup` + builder palette resolve the registry; the seven
behavior maps carry `EXTENSION_TYPE` leaf behavior
- [x] Unknown-type graceful fallback (placeholder + meta preserved on save)
- [x] Deprecation notices on `DashboardComponentsRegistry` / `DYNAMIC_TYPE`
(legacy path still functions)
- [x] Reference component: the built-in iframe is now delivered **through** the
contribution point (`src/dashboard/extensions/iframe`), registered at
startup exactly as a third-party extension would; its CSP backend remains
in core per the companion SIP
- [x] Tests: registry lifecycle (register/get/replace/dispose), host-wrapper
resolution + fallback + `updateMeta`, iframe content + CSP UX
- [x] Per-component behavior policy honored by the layout engine: `resizable`,
`minWidth`, `isUserContent`, `validParents`, and `wrapInRow` are seeded onto
instance `meta` at creation and read by `componentIsResizable`,
`getDetailedComponentWidth`, `isDashboardEmpty`, `isValidChild`, and
`shouldWrapChildInRow` (the pure layout utils stay registry-free; behavior
round-trips in the saved layout)
- [x] Developer docs: `extension-points/dashboard-components.md` + a
`contribution-types.md` section + sidebar entry, with an example extension
Remaining (follow-up, not POC-blocking):
- [ ] Manifest `contributions.dashboardComponents` declarative validation in the
Python/TS manifest schema (runtime side-effect registration works today,
matching how `chat` does it)
- [ ] Remove the legacy `DashboardComponentsRegistry`/`DYNAMIC_TYPE` (major)
-232
View File
@@ -1,232 +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.
-->
# [SIP] Proposal for a first-class iframe dashboard component with a runtime CSP allowlist
> **Companion SIP:** This proposal pairs with
> [`SIP-DASHBOARD-COMPONENT-CONTRIBUTION-POINT.md`](SIP-DASHBOARD-COMPONENT-CONTRIBUTION-POINT.md),
> which proposes the Extensions contribution point that would let this iframe
> component (and others) be shipped as an extension. The two are deliberately
> separate discussions: **this** SIP covers the security-sensitive feature
> (runtime CSP override + permissions); the companion covers the framework
> change. They share one POC branch so the end-to-end story is demonstrable.
> **Status:** Draft — tracking the implementation in `feat/csp-runtime-allowlist-iframe`.
> This document follows the SIP issue template and is kept in sync with the branch
> as the implementation evolves. See SIP-0
> (<https://github.com/apache/superset/issues/5602>) for the SIP process.
## Motivation
Superset ships a Talisman/Content-Security-Policy (CSP) configuration that, by
design, prevents users from embedding arbitrary external content in a dashboard.
The default policy declares `default-src 'self'` and **no** `frame-src`
directive, so an `<iframe>` pointing at any third-party origin is blocked by the
browser.
This is correct and secure default behavior, but it creates real friction:
- There is **no first-class "iframe" dashboard component**. Users historically
smuggled iframes through Markdown, which is both a footgun and blocked by CSP.
- When an embed *is* legitimately needed (an internal tool, a status page, a
partner widget), the only way to allow it is to **edit `TALISMAN_CONFIG` and
restart every Superset process**. That is a deploy-time, ops-team operation —
far too heavyweight for "let me embed this one dashboard from our other
internal app."
- There is no in-product signal telling a user *why* their embed is blank, and
no path to fix it.
We want to (a) make embedding a real, supported component, and (b) give trusted
Admins a controlled, audited way to widen the CSP at runtime — without
abandoning the secure-by-default posture that operators rely on.
## Proposed Change
The change has five parts.
### 1. A first-class `IFRAME` dashboard layout component
A new grid component (`IFRAME_TYPE`) modeled on the existing Markdown/Divider
components. In edit mode the user pastes a URL; in view mode the component
renders a sandboxed `<iframe>`. The component is registered through the same
surface as every other layout element (type constant, `componentLookup`, drag
palette, nesting/resize/width/wrap util maps).
The iframe is rendered with a restrictive `sandbox` attribute
(`allow-scripts allow-same-origin allow-popups allow-forms`).
### 2. Domain flagging
When the runtime-allowlist feature is enabled, the component compares the
embedded URL's **origin** against the current allowlist (fetched from the new
API). If the origin is not yet allowed, it shows an inline warning explaining
that the domain is blocked by the CSP.
### 3. "Enable domain in CSP" button
If the current user holds the new permission (Admins by default), the warning
includes an **Enable domain in CSP** button. Clicking it `POST`s the origin to
the allowlist API and re-checks. Users without the permission instead see "ask
an administrator."
### 4. Permission gating
Mutating the allowlist requires `can write on CSPAllowlist`. The `CSPAllowlist`
view-menu is registered in `SupersetSecurityManager.ADMIN_ONLY_VIEW_MENUS`, so
the capability is reserved for Admins (or a custom role explicitly granted it),
consistent with how other trusted, security-sensitive operations are scoped.
### 5. Runtime CSP override ("punched holes")
A new `csp_allowlist` metadata table stores allowlist entries. An `after_request`
hook — registered **before** flask-talisman so that, because Flask runs
`after_request` callbacks in reverse registration order, it runs **after**
Talisman has set the header — merges the operator-curated entries into the
response CSP header. Entries are cached in-process with a short TTL to avoid a DB
hit per response; a write through the API invalidates the cache in the handling
worker, and other workers converge when their cached copy expires.
The entire runtime-override path is inert unless the `CSP_RUNTIME_ALLOWLIST`
feature flag is enabled, so the static, deploy-time policy remains the default
and operators opt in explicitly.
```
Browser ──> Flask request
Talisman after_request (sets "Content-Security-Policy: default-src 'self'; …")
merge_runtime_csp_allowlist (if flag on: appends allowlist origins to frame-src, …)
Response ──> Browser ("…; frame-src 'self' https://embed.example")
```
#### Design decisions (resolved)
- **Scope: global.** Allowlist entries apply server-wide. CSP is a single
per-response header; a global allowlist keeps the merge context-free and
avoids per-dashboard request plumbing. (Per-dashboard scoping is a possible
future extension.)
- **Operator control: feature-flagged kill-switch.** The runtime override only
functions when `CSP_RUNTIME_ALLOWLIST` is on (default **off**). Operators who
want a purely static policy simply leave it off and the table is never
consulted.
## New or Changed Public Interfaces
### REST API
- `GET /api/v1/csp_allowlist/` — list entries
- `GET /api/v1/csp_allowlist/<id>` — get one
- `POST /api/v1/csp_allowlist/` — create (validates origin + directive)
- `PUT /api/v1/csp_allowlist/<id>` — update
- `DELETE /api/v1/csp_allowlist/<id>` — delete
- `DELETE /api/v1/csp_allowlist/?q=!(...)` — bulk delete
All write methods require `can write on CSPAllowlist` (Admin-only by default).
Origins are validated server-side: bare `scheme://host[:port]` only — no
wildcards, paths, query strings, fragments, or credentials. Only a fixed set of
directives may be widened (`frame-src`, `child-src`, `img-src`, `connect-src`,
`media-src`, `font-src`); notably **not** `script-src`.
### Model
- `CSPAllowlistEntry` (`superset/models/csp.py`, table `csp_allowlist`):
`id`, `uuid`, `domain`, `directive` (default `frame-src`), `description`,
audit columns. Unique on `(domain, directive)`.
### Feature flag
- `CSP_RUNTIME_ALLOWLIST` (default `False`) — gates the entire runtime-override
path, backend and frontend.
### Config
- `CSP_RUNTIME_ALLOWLIST_CACHE_TTL` (default `30` seconds) — in-process cache TTL
for the allowlist; also settable via env var.
### Frontend
- New `IFRAME` dashboard layout component and its registration across the
dashboard util maps.
- New `FeatureFlag.CspRuntimeAllowlist` enum member.
### Security model
- New `CSPAllowlist` view-menu added to `ADMIN_ONLY_VIEW_MENUS`.
## New dependencies
None. The implementation uses existing libraries (flask-talisman,
Flask-AppBuilder, marshmallow, SQLAlchemy on the backend; existing
`@superset-ui/core` components on the frontend).
## Migration Plan and Compatibility
- One Alembic migration adds the `csp_allowlist` table
(`4a50792bd265`, down-revision `3a8e6f2c1b95`). The table is empty on creation.
- Fully backward compatible: with the feature flag off (the default), behavior is
identical to today — the static CSP is authoritative and the new table is never
read. No existing dashboards, URLs, or policies change.
- Rollback: dropping the table and disabling the flag fully reverts the feature.
### Security review notes
This feature deliberately relocates a *capability* (widening the CSP) from a
purely deploy-time operator control into a runtime, permission-gated, audited
operation. The mitigations that keep it within Superset's trust model:
- **Off by default** behind a feature flag the operator owns.
- **Admin-only** write permission (a fully trusted principal per `SECURITY.md`).
- **Strict origin validation** server-side — no wildcards, no `script-src`.
- **Audit trail** via the audit mixin (`created_by` / `changed_by`).
- The iframe is **sandboxed** and the merge can only *widen* a directive to a
specific origin, never relax nonce/`strict-dynamic` protections on
`script-src`.
## Rejected Alternatives
- **Dynamically reconfiguring flask-talisman at runtime.** Talisman is configured
once at app init. Rather than mutate its internals, we add our own
`after_request` hook that post-processes the header it already sets. This is
simpler, avoids depending on Talisman internals, and rides the same per-request
header machinery Talisman already uses for its nonce.
- **Per-dashboard allowlist scoping.** More precise, but CSP is a per-response
header; per-dashboard scoping adds request-context complexity for marginal
benefit in the common case. Left as a possible future extension.
- **"Always on" runtime override (no kill-switch).** Simpler, but moves a
security control fully into the app with no operator opt-out. Rejected in favor
of the feature-flag kill-switch.
- **Shared/Redis-backed allowlist cache with cross-worker invalidation.**
Correct but heavier. A short-TTL in-process cache is good enough: writes take
effect immediately in the handling worker and within the TTL elsewhere, with no
new infrastructure dependency.
## Implementation Status
- [x] Feature flag `CSP_RUNTIME_ALLOWLIST` + `CSP_RUNTIME_ALLOWLIST_CACHE_TTL`
- [x] `CSPAllowlistEntry` model + Alembic migration
- [x] DAO, marshmallow schemas (with origin/directive validation), REST API
- [x] Admin-only permission (`CSPAllowlist` view-menu)
- [x] `after_request` CSP merge hook + in-process TTL cache + invalidation
- [x] `IFRAME` dashboard component + registration across util maps
- [x] Domain flagging + permission-gated "Enable domain in CSP" button
- [x] Tests: backend unit (validation + merge + hook), backend integration (API),
frontend unit (util + component)
- [ ] Docs (`docs/`) + `UPDATING.md` entry
- [ ] Community/security review feedback
+60 -12
View File
@@ -25,6 +25,30 @@ assists people when migrating to a new version.
## Next
- `SAMPLES_ROW_LIMIT` is now the default for `/datasource/samples` requests without a valid explicit `per_page`, rather than a hard per-request ceiling; explicit limits are honored up to the existing global row-limit ceiling, matching `/chart/data` SAMPLES requests.
- The `cockroachdb` extra (`pip install apache-superset[cockroachdb]`) now installs `sqlalchemy-cockroachdb` instead of the abandoned `cockroachdb` package, whose SQLAlchemy dialect could not be imported under SQLAlchemy 2.0. Existing environments with the old package installed should `pip uninstall cockroachdb && pip install sqlalchemy-cockroachdb` (or simply reinstall the extra) to restore CockroachDB connectivity.
### MCP tool results preserve stored string values
Structured MCP tool results no longer add `<UNTRUSTED-CONTENT>` wrappers or
rewrite delimiter-looking text inside string fields. Tool-result content remains
user-controlled data, but clients must convey that trust boundary outside domain
values instead of recognizing or removing marker strings.
Clients that handled the former delimiter convention should stop stripping marker
text: the same text can be legitimate stored content. Response models and content
types are unchanged, and no metadata-database migration is required. Automated
read-modify-write workflows should be paused or pinned away from older instances
until every serving instance is upgraded; a mixed-version response has no reliable
signal that tells a client whether its text is decorated. Redis-backed MCP response
caches use a new internal namespace after the upgrade, so upgraded instances do not
reuse older cached results.
Values that a client already wrote back with presentation wrappers cannot be
distinguished safely from intentional content. Operators should review possible
`<UNTRUSTED-CONTENT>` / `</UNTRUSTED-CONTENT>` wrappers and
`[ESCAPED-UNTRUSTED-CONTENT-OPEN]` /
`[ESCAPED-UNTRUSTED-CONTENT-CLOSE]` substitutions rather than applying an automatic
marker-removal migration.
### OAuth2 database callback metrics include their outcome
@@ -35,6 +59,8 @@ the old counter to use the outcome-specific replacements.
- [42930](https://github.com/apache/superset/pull/42930): Dataset import data-URI fetches no longer honor an HTTP(S) proxy when `DATASET_IMPORT_ALLOW_INTERNAL_DATA_URLS` is `False` (the default): the connection is now made directly to the destination so the peer-address check validates the real target instead of a proxy's. Deployments that require an egress proxy to reach legitimate external data URLs for dataset import should set `DATASET_IMPORT_ALLOW_INTERNAL_DATA_URLS = True` or otherwise ensure those URLs resolve without one.
- [42935](https://github.com/apache/superset/pull/42935): The MCP service now refuses to start (`MCPAuthConfigError`) when `MCP_JWT_ISSUER` trusts more than one issuer and no `MCP_USER_RESOLVER` is configured, instead of only logging a warning. This was already a documented misconfiguration (the default resolver isn't issuer-scoped, so distinct trusted issuers minting the same username/email would resolve to the same Superset user); deployments trusting multiple issuers must configure an `MCP_USER_RESOLVER` that derives its identity from the token's `iss` claim before upgrading. Single-issuer deployments are unaffected.
- [42429](https://github.com/apache/superset/pull/42429): The Country Map chart's Iran GeoJSON now gives Alborz province its own ISO 3166-2 code, `IR-32`, instead of `IR-30`. `ISO` is the join key used to color/filter provinces on this chart, so any existing dataset keyed on `IR-30` for Alborz will silently stop matching after upgrading; re-key that data to `IR-32`.
- [43388](https://github.com/apache/superset/pull/43388): The MCP service now refuses to start (`MCPAuthConfigError`) if `MCP_DEV_USERNAME` and `MCP_AUTH_ENABLED = True` are both set, and separately if `MCP_AUTH_ENABLED = True` but no usable JWT key material is configured (RSA key/JWKS, or an explicit `MCP_JWT_SECRET` for HMAC) — both previously started with authentication silently weaker than configured. Deployments combining a dev-mode username with JWT auth enabled, or enabling JWT auth without key material, must pick one before upgrading: unset `MCP_DEV_USERNAME` for a real auth deployment, or unset `MCP_AUTH_ENABLED` (or configure the key material) for a dev-mode one. Response caching (`MCP_CACHE_CONFIG["enabled"] = True`) now also excludes every tool with a side effect by default, not only a partial list, so a previously-cached mutating tool call is no longer served from cache; no config change is needed to pick this up.
- [42393](https://github.com/apache/superset/pull/42393): Exported dataset YAML now carries a `uuid` for each metric and column so that custom folder assignments (which reference metrics/columns by UUID) survive an import into another workspace. This affects any export bundle that contains datasets, not just a dataset export: chart, dashboard, database and full-asset exports all embed the same dataset YAML, so a dashboard exported from this release also fails to import into an older one even though no dataset was exported directly. As with `folders` and `currency_code_column`, the affected `datasets/` files fail schema validation (`Unknown field: uuid`) when imported into Superset releases that predate this change; regenerate or hand-edit exports for older targets in mixed-version fleets.
- [42300](https://github.com/apache/superset/pull/42300): Timeseries charts (line/area/bar) with a Y-axis bound in effect — either an explicit `yAxisBounds` or one derived from `truncateYAxis` — now clamp out-of-range data points to that bound instead of letting ECharts drop the point (and the line segments around it) entirely. Any existing chart with a configured Y-axis bound and data outside it will look different after upgrading: a gap becomes a point pinned to the boundary. The clamp also rewrites the value ECharts reads for that point's tooltip and data label, so the displayed value is the bound rather than the true observation.
- [42087](https://github.com/apache/superset/pull/42087): Stored calculated-column and metric expressions are validated when a query is built, under the same sub-query policy already applied to adhoc expressions. Previously only the dataset update path checked them on save, so expressions written by v1 import, by dataset duplication, or before that check existed were never validated. Since `ALLOW_ADHOC_SUBQUERY` defaults to `False` (see [19242](https://github.com/apache/superset/pull/19242)), a dataset whose stored expression contains a sub-query works before upgrading and afterwards fails at chart render with `Custom SQL fields cannot contain sub-queries.` There is no migration step, and the error does not name the offending dataset column, so audit stored expressions before upgrading: either rewrite them without the sub-query, or set `ALLOW_ADHOC_SUBQUERY = True` to keep the previous behaviour for both stored and adhoc expressions.
@@ -102,6 +128,23 @@ dialect; each package's constraint in `pyproject.toml` documents why.
No application-level configuration changes are required for deployments
that don't touch SQLAlchemy directly.
### New metric aggregates: MEDIAN, Sample Standard Deviation, Sample Variance
`MEDIAN`, `STDDEV_SAMP`, and `VAR_SAMP` are now available anywhere a metric
aggregate is chosen (every chart type, SQL Lab, MCP), not only in Pivot
Table's controls. Support is opt-in per database engine *spec class*,
verified against a live instance before being enabled: Postgres, MySQL
(`STDDEV_SAMP`/`VAR_SAMP` only, no `MEDIAN`), DuckDB, and Redshift (inherits
Postgres's support, not yet separately verified) ship enabled in this
release. Engine specs that subclass one of those (e.g. MariaDB, Aurora
MySQL/Postgres, TimescaleDB) inherit the same support, on the same
not-yet-independently-verified basis. Picking one of these aggregates on a
database that has not opted in returns a clear "not supported on this
database" error rather than a failed query. See
`docs/sip/median-stddev-variance-aggregates.md` for the full design
rationale, including why this is safe to add without reintroducing the
totals/subtotals correctness bug fixed by #41184 (SIP-216).
### Soft delete is on by default, and purging is live
`SOFT_DELETE` now ships **on** (`DEFAULT_FEATURE_FLAGS`), so deleting a
@@ -143,10 +186,12 @@ misrepresents the entity as unchanged.
- **Storage growth.** Capture writes shadow rows per save, so the metadata
database grows with edit volume. The `version_history.prune_old_versions`
beat task removes rows whose transaction is older than
`SUPERSET_VERSION_HISTORY_RETENTION_DAYS` (default 30). A deployment that
replaces `CELERY_CONFIG` rather than inheriting it must carry both the
`superset.tasks.version_history_retention` import and the beat entry; a
startup warning names whichever is absent.
`SUPERSET_VERSION_HISTORY_RETENTION_DAYS` (default 30).
- **Check a replaced `CELERY_CONFIG`.** Carry both the
`superset.tasks.version_history_retention` import and the
`version_history.prune_old_versions` beat entry; see
[Version-history retention (pruning)](#version-history-retention-pruning) for
the startup-warning behavior.
- **`PUT` responses change shape.** Entity updates now return populated
`old_version_uuid` / `new_version_uuid` fields and an `ETag` header, which
were null or absent while capture was off.
@@ -155,7 +200,10 @@ misrepresents the entity as unchanged.
kill-switch — not removed with the rollout toggles. Setting it to a falsy value
stops capture within a restart, without a revert-and-redeploy. Unlike the
soft-delete toggle, turning it off is a clean stop: existing version rows remain
readable and no entity state is altered.
readable and no entity state is altered. Restore is unavailable (404) while
capture is off. A full rollback also sets
`FEATURE_FLAGS = {"VERSION_HISTORY": False}` to hide the panel — capture off
with the panel left on shows an empty or stale history.
### Scheduled report execution now enforces one application deadline
@@ -617,9 +665,9 @@ ALTER TABLE tagged_object DROP CONSTRAINT <constraint_name>;
ALTER TABLE tagged_object DROP FOREIGN KEY <constraint_name>;
```
### Entity version-history infrastructure (gated off by default)
### Entity version-history infrastructure
Introduces the schema and SQLAlchemy-Continuum wiring that captures version history for charts, dashboards, and datasets, plus read-only `GET /api/v1/{chart,dashboard,dataset}/<uuid>/versions/` endpoints. This ships **inert**: a new config flag `ENABLE_VERSIONING_CAPTURE` defaults to `False`, so no save writes any version rows and the endpoints return empty. It is an operational kill-switch (a release toggle that becomes a permanent ops switch), not a feature flag — set it to `True` to enable capture once validated. The migration is additive; existing entity `PUT` responses gain `old_version_uuid` / `new_version_uuid` body fields and an `ETag` header (both null/absent when capture is off).
Introduces the schema and SQLAlchemy-Continuum wiring that captures version history for charts, dashboards, and datasets, plus read-only `GET /api/v1/{chart,dashboard,dataset}/<uuid>/versions/` endpoints. Capture is governed by the `ENABLE_VERSIONING_CAPTURE` config value — an operational kill-switch (a release toggle that became a permanent ops switch), not a feature flag; see "Version history is on by default" above for the shipped default. With capture off, no save writes version rows; the endpoints continue to serve already-captured rows read-only. The migration is additive; existing entity `PUT` responses gain `old_version_uuid` / `new_version_uuid` body fields and an `ETag` header (both null/absent when capture is off).
A few save- and import-path internals change **unconditionally** (independent of the flag), because the versioned mappers must behave correctly whether or not capture is enabled:
@@ -640,7 +688,7 @@ A read-only companion to the version-history endpoints: each entity type gains a
| `q` | string | — | Case-insensitive search over the full history, applied before pagination (so `count` reflects matches) |
| `page` / `page_size` | integer | `0` / `25` | Pagination (`page_size` clamped to 200) |
Authorization reuses the resource's `can_read` permission and per-object `raise_for_access`; related-entity rows are visibility-filtered to what the caller may see. The stream is empty unless version capture is on (`ENABLE_VERSIONING_CAPTURE`).
Authorization reuses the resource's `can_read` permission and per-object `raise_for_access`; related-entity rows are visibility-filtered to what the caller may see. The stream reflects captured history; with capture off it remains readable but stops accruing new entries.
### Version-history retention (pruning)
@@ -660,7 +708,7 @@ Purging is **live by default** (`SOFT_DELETE_PURGE_DRY_RUN=False`), so the reten
Deployments that replace the default `CELERY_CONFIG` must ensure workers register `superset.tasks.deletion_retention` and schedule the `deletion_retention.purge_soft_deleted` task themselves. The shipped Docker development config uses `imports` and includes both entries. While `SOFT_DELETE` is statically enabled, a missing beat entry logs a startup warning; when the override explicitly defines `imports`, a missing purge module is also reported.
Operators can immediately erase a specific entity for compliance (GDPR) via `superset deletion-retention force-purge --uuid <uuid>`; this applies legacy hard-delete semantics — a live chart referencing a force-purged dataset is left without a datasource until re-pointed (the chart is not modified), and it purges the named entity even when it was never soft-deleted. Every scheduled evaluation writes a provisional, content-free record to the new `purge_audit_log` table before the cascade starts. Meaningful retained outcomes survive the entity they name. Consecutive scheduled evaluations with the same blocked outcome suppress only the redundant current provisional record; completed outcomes, outcome transitions, and every force-purge attempt remain independent and immutable. The **scheduled** purge fails closed when its provisional record cannot be written, while **force-purge** proceeds even if the audit write fails — the operator is present and deletion outranks audit for a compliance erasure. Operators can monitor `deletion_retention.blocked_audit_suppressed` and `deletion_retention.blocked_audit_dedupe_fallback` to verify suppression and fail-safe fallback behavior without changing the existing blocked-workload gauge.
Operators can immediately erase a specific entity for compliance (GDPR) via `superset deletion-retention force-purge --uuid <uuid>`; this applies legacy hard-delete semantics — a live chart referencing a force-purged dataset is left without a datasource until re-pointed (the chart is not modified), and it purges the named entity even when it was never soft-deleted. Every scheduled evaluation writes a provisional, content-free record to the new `purge_audit_log` table before the cascade starts. Meaningful retained outcomes survive the entity they name. Blocked audit records carry a stable machine-readable `reason` code (`report_schedule`, `user_attribute`, or `cascade_integrity_failure` for an unexpected cascade failure caused by a database integrity constraint) so the audit table alone answers why an entity was not purged; records finalized before the column existed keep a NULL reason. Apply the migration before rolling out the new code: the audit model declares the column, so a worker on the new code with an un-migrated table fails its write-ahead write and the scheduled purge fails closed until the migration lands. During a rolling deploy, workers still on the old code write reason-less blocked rows and suppress on status alone; both effects are self-healing, since a NULL-reason record never matches a reason code and the next all-new-code run re-anchors the entity. Consecutive scheduled evaluations blocked with the same status **and reason** suppress only the redundant current provisional record — a reason change writes one new blocked record carrying the new code; completed outcomes, outcome transitions, and every force-purge attempt remain independent and immutable. Retained transition records are not automatically expired, so entities whose block reason changes repeatedly can accumulate multiple audit rows. The **scheduled** purge fails closed when its provisional record cannot be written, while **force-purge** proceeds even if the audit write fails — the operator is present and deletion outranks audit for a compliance erasure. Operators can monitor `deletion_retention.blocked_audit_suppressed` and `deletion_retention.blocked_audit_dedupe_fallback` to verify suppression and fail-safe fallback behavior without changing the existing blocked-workload gauge.
### Recently Archived view and permanent delete (purge) endpoints
@@ -856,7 +904,7 @@ The migration is transactional (all-or-nothing) and idempotent — it can be saf
### Soft delete and restore for datasets
**The soft-delete behavior in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `False`** (`@lifecycle: development`), so on a default deployment `DELETE /api/v1/dataset/<id>` continues to **hard-delete permanently** — nothing is recoverable. Enable `SOFT_DELETE` to get the behavior described below.
**The soft-delete behavior in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `True`** (`@lifecycle: testing`), so on a default deployment `DELETE /api/v1/dataset/<id>` uses the recoverable soft-delete behavior described below. Setting `SOFT_DELETE` to `False` restores legacy permanent hard-delete behavior for subsequent deletes.
**Flag-toggle caveat:** the soft-delete visibility filter is evaluated per query while the flag is on. If datasets are soft-deleted during a flag-on window and the flag is later turned **off**, those rows reappear as live datasets in all lists, lookups, and relationship loads (including charts that reference them). The `POST /<uuid>/restore` endpoint and the `dataset_deleted_state` list filter remain functional regardless of the flag, deliberately, so rows soft-deleted during a flag-on window stay discoverable and restorable after a rollback of the flag.
@@ -886,7 +934,7 @@ With the flag enabled: `DELETE /api/v1/dataset/<id>` no longer hard-deletes the
### Soft delete and restore for charts
**Everything in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `False`** (`@lifecycle: development`), so on a default deployment `DELETE /api/v1/chart/<id>` continues to **hard-delete permanently** — nothing is recoverable. Enable `SOFT_DELETE` to get the behavior described below.
**Everything in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `True`** (`@lifecycle: testing`), so on a default deployment `DELETE /api/v1/chart/<id>` uses the recoverable soft-delete behavior described below. Setting `SOFT_DELETE` to `False` restores legacy permanent hard-delete behavior for subsequent deletes.
**Flag-toggle caveat:** the soft-delete visibility filter is evaluated per query while the flag is on. If charts are soft-deleted during a flag-on window and the flag is later turned **off**, those rows reappear as live charts in all lists, lookups, and relationship loads (including dashboards that contained them). The `POST /<uuid>/restore` endpoint and the `chart_deleted_state` list filter remain functional regardless of the flag, deliberately, so rows soft-deleted during a flag-on window stay discoverable and restorable after a rollback of the flag.
@@ -910,7 +958,7 @@ With the flag enabled: `DELETE /api/v1/chart/<id>` no longer hard-deletes the ch
### Soft delete and restore for dashboards
**Everything in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `False`** (`@lifecycle: development`), so on a default deployment `DELETE /api/v1/dashboard/<id>` continues to **hard-delete permanently** — nothing is recoverable. Enable `SOFT_DELETE` to get the behavior described below.
**Everything in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `True`** (`@lifecycle: testing`), so on a default deployment `DELETE /api/v1/dashboard/<id>` uses the recoverable soft-delete behavior described below. Setting `SOFT_DELETE` to `False` restores legacy permanent hard-delete behavior for subsequent deletes.
**Flag-toggle caveat:** the soft-delete visibility filter is evaluated per query while the flag is on. If dashboards are soft-deleted during a flag-on window and the flag is later turned **off**, those rows reappear as live dashboards in all lists and lookups (including slug lookups — if a soft-deleted dashboard's slug was reused while the flag was on, both rows become visible with the same slug). The `POST /<uuid>/restore` endpoint and the `dashboard_deleted_state` list filter remain functional regardless of the flag, deliberately, so rows soft-deleted during a flag-on window stay discoverable and restorable after a rollback of the flag.
@@ -14,6 +14,20 @@ Users can configure automated alerts and reports to send dashboards or charts to
Alerts and reports are disabled by default. To turn them on, you'll need to change configuration settings and install a suitable headless browser in your environment.
## Content Format Options
When scheduling an alert or report, you can choose the format used to deliver the dashboard or chart:
- **PDF** a full-page screenshot rendered as a PDF attachment. Available for both dashboards and charts.
- **PNG** a screenshot delivered as an attachment. Emails embed the image inline in the message body; Slack and webhook recipients receive it as an uploaded file. Available for both dashboards and charts.
- **CSV** chart data attached as a `.csv` file. Available for charts only.
- **XLSX (Excel)** chart data attached as a `.xlsx` file. Available for charts only. If the chart's data spans multiple server-paginated files, email delivery detects the bundle and renames the attachment to `.zip`; Slack and webhook deliveries always name the file with an `.xlsx` extension even when the contents are a multi-file ZIP archive.
- **Text** chart data embedded directly in the email or Slack message body. Available only for charts using a text-based visualization type (e.g. Table, Pivot Table, Paired t-test).
Dashboard reports and alerts are limited to the PDF and PNG formats; the CSV, XLSX, and Text options are only available when scheduling a report or alert for an individual chart.
For alerts (not scheduled reports), PNG/PDF screenshots and chart CSV/XLSX data are only generated when the `ALERTS_ATTACH_REPORTS` [feature flag](/admin-docs/configuration/configuring-superset#feature-flags) is enabled (the default); with it disabled, an alert notification is still sent, but without the attachment.
## Requirements
### Commons
+21
View File
@@ -76,6 +76,27 @@ value defined in `DATA_CACHE_CONFIG`.
Note, that by setting the cache timeout to `-1`, caching for charting data can be disabled, either
per chart, dataset or database, or by default if set in `DATA_CACHE_CONFIG`.
Native filter option queries (the dropdown values for native filters) go through this same
chart-data cache, but their freshness needs often differ from regular chart queries, especially for
datasets whose visible values change frequently, including RLS-constrained datasets. Set
`NATIVE_FILTER_OPTIONS_CACHE_TIMEOUT` in `superset_config.py` to give these queries a dedicated
timeout, checked before the chart/dataset/database chain and the `DATA_CACHE_CONFIG` default above:
```python
NATIVE_FILTER_OPTIONS_CACHE_TIMEOUT = 60 # seconds
```
- `None` (default): native filter option queries fall through to the normal
chart/dataset/database/`DATA_CACHE_CONFIG` resolution chain.
- `-1`: disables caching for native filter option queries entirely.
- `0`: passed directly to the cache backend; behavior is backend-specific, so use `-1` if the intent
is to disable caching.
- A positive integer: cache native filter option queries for that many seconds.
This setting only applies to requests detected as native filter option queries. It takes precedence
over the per-chart/dataset/database timeouts, but not over an explicit per-request
`custom_cache_timeout` override (e.g. "Force refresh").
## Limiting Cached Result Size
Very large chart or SQL query results can flood the cache backend (Redis/Memcached), evicting many
@@ -83,6 +83,26 @@ The optional username flag **-u** sets the user used for the datasource import.
superset import_datasources -p <path / filename> -u 'admin'
```
## Importing a Directory of Assets
The `import_directory` command imports a directory of exported assets (databases, datasets,
dashboards, charts) in the same layout produced by the ZIP-based export. Saved queries and tags
included in a full export are not imported by this command:
```bash
superset import_directory <path / directory>
```
As with `import_datasources`, the optional username flag **-u** sets the user assigned as the
owner of the imported assets. The default is 'admin'. Example:
```bash
superset import_directory <path / directory> -u 'admin'
```
If the specified user does not exist, the command fails immediately with an error rather than
importing the assets without an owner.
## Dashboard Import Overwrite Behavior
When importing a dashboard ZIP with the **overwrite** option enabled, any existing charts that are part of the dashboard are **replaced** rather than duplicated. This applies to:
+75 -3
View File
@@ -505,6 +505,8 @@ All MCP settings go in `superset_config.py`. Defaults are defined in `superset/m
| `MCP_DEV_USERNAME` | -- | Superset username for development mode (no auth) |
| `MCP_RBAC_ENABLED` | `True` | Enforce Superset's role-based access control on MCP tool calls. When `True`, each tool checks that the authenticated user has the required FAB permission before executing. Disable only for testing or trusted-network deployments. |
| `MCP_DISABLED_TOOLS` | `set()` | Set of tool names to remove from the MCP server at startup. Disabled tools are never advertised to AI clients during tool discovery. Useful when a custom extension tool should replace a built-in Superset tool. See [Disabling built-in tools](#disabling-built-in-tools). |
| `MCP_DISABLED_CHART_PLUGINS` | `frozenset()` | Set of chart type plugin names (e.g. `"handlebars"`) to hide from `generate_chart`. Does not affect `get_chart_type_schema`. See [Disabling chart type plugins](#disabling-chart-type-plugins). |
| `MCP_CHART_PLUGIN_ENABLED_FUNC` | `None` | Callable `(chart_type: str) -> bool` evaluated per registry lookup for dynamic enable/disable decisions. Takes precedence over `MCP_DISABLED_CHART_PLUGINS` when set. See [Disabling chart type plugins](#disabling-chart-type-plugins). |
### Authentication
@@ -576,7 +578,7 @@ MCP_CACHE_CONFIG = {
| Key | Default | Description |
| -------------------- | --------- | ----------------------------------------------------------- |
| `enabled` | `False` | Enable response caching |
| `CACHE_KEY_PREFIX` | `None` | Optional prefix for cache keys (useful for shared Redis) |
| `CACHE_KEY_PREFIX` | `None` | Base prefix for shared Redis; Superset appends an internal response-contract namespace |
| `list_tools_ttl` | `300` | Cache TTL in seconds for `tools/list` |
| `list_resources_ttl` | `300` | Cache TTL for `resources/list` |
| `list_prompts_ttl` | `300` | Cache TTL for `prompts/list` |
@@ -718,6 +720,34 @@ Every MCP request passes through a middleware stack before reaching the tool fun
Additional middleware classes (`RateLimitMiddleware`, `FieldPermissionsMiddleware`, `PrivateToolMiddleware`) are implemented in `superset/mcp_service/middleware.py` but are not added to the default pipeline. They are available for operators who want to layer them in via a custom startup path.
### Tool Result Value Contract
Structured tool results preserve Superset domain values exactly. In particular,
string fields are not wrapped in trust delimiters, and text that resembles a
delimiter is returned as literal application data. This lets clients safely use a
read result as the basis for an update without persisting presentation markup.
All tool-result content should still be treated as user-controlled data with no
instruction authority. MCP clients should communicate that trust boundary through
their model instructions or presentation layer, outside the returned field values;
fixed or generated marker strings inside a value are ambiguous and must not be used
as a trust signal.
For compatibility, clients that supported the former
`<UNTRUSTED-CONTENT>` convention should stop recognizing or stripping those strings.
The response schemas and content types have not changed. Because marker-looking text
can be legitimate application data, a client cannot reliably distinguish a legacy
decorated response from a clean one. Pause automated read-modify-write workflows, or
route them only to upgraded instances, until every serving instance is upgraded.
Redis-backed MCP response caches include an internal response-contract namespace, so
an upgraded instance does not reuse responses cached by an older release. Older
instances can still return legacy values while they remain in service. After the
upgrade, review previously written values for wrapper text and both
`[ESCAPED-UNTRUSTED-CONTENT-OPEN]` and
`[ESCAPED-UNTRUSTED-CONTENT-CLOSE]`; do not remove these strings automatically,
because they may be intentional content.
### Error Sanitization
The `GlobalErrorHandlerMiddleware` automatically redacts sensitive information from all error messages before they reach the LLM client. The following are replaced with generic messages:
@@ -752,11 +782,20 @@ For a 3-pod Kubernetes deployment with the defaults above, expect up to 3 × (5
Enable response caching for read-heavy workloads (dashboards/datasets that don't change frequently). With the in-memory backend (default when `MCP_STORE_CONFIG` is disabled), caching is per-process. Use Redis-backed caching for consistent cache hits across multiple pods:
```python
MCP_CACHE_CONFIG = {"enabled": True, "call_tool_ttl": 3600}
MCP_CACHE_CONFIG = {
"enabled": True,
# Cache keys don't include the requesting principal and hits are served
# ahead of auth/RBAC, so a shared cache can return one caller's response
# to another. Required for caching to actually start -- only appropriate
# when every request is guaranteed to come from the same principal.
"dangerously_share_cache_across_principals": True,
"CACHE_KEY_PREFIX": "mcp_cache_",
"call_tool_ttl": 3600,
}
MCP_STORE_CONFIG = {"enabled": True, "CACHE_REDIS_URL": "redis://redis:6379/0"}
```
Mutating tools (`generate_chart`, `update_chart`, `execute_sql`, `generate_dashboard`) are always excluded from caching regardless of this setting.
Every tool with a side effect (create/update/delete/execute) is always excluded from caching regardless of this setting -- see the `excluded_tools` default in `superset/mcp_service/mcp_config.py` for the current list.
---
@@ -856,6 +895,39 @@ MCP_DISABLED_TOOLS = {"extensions.myorg.myextension.some_tool"}
Specifying a tool name that does not exist logs a warning at startup and is otherwise ignored — it will not prevent the server from starting.
:::
## Disabling chart type plugins
The `generate_chart` tool dispatches per chart type (`xy`, `table`, `pie`, `pivot_table`, `mixed_timeseries`, `handlebars`, `big_number`, `histogram`, `box_plot`, `waterfall`) to a registered chart type plugin. Two settings let operators enable or disable individual chart type plugins at runtime, without a code deploy.
### Static deny-list
Set `MCP_DISABLED_CHART_PLUGINS` in your `superset_config.py` to a set of chart type names:
```python
# superset_config.py
# Emergency kill switch: hide "handlebars" from all callers
MCP_DISABLED_CHART_PLUGINS = {"handlebars"}
```
Disabled chart types stay registered but are filtered out at lookup time: they're never listed in `generate_chart`'s supported chart types, and `generate_chart` calls for them are rejected. `get_chart_type_schema` consults its own static schema/example map rather than the registry filter, so a disabled chart type's schema remains queryable through that tool even though `generate_chart` will reject it.
### Dynamic predicate
For per-request control (A/B tests, gradual rollout, entitlement checks), set `MCP_CHART_PLUGIN_ENABLED_FUNC` to a callable. It's evaluated as `enabled_func(chart_type: str) -> bool` on every registry lookup, and it takes precedence over `MCP_DISABLED_CHART_PLUGINS` when set:
```python
# superset_config.py
from flask import g
def MCP_CHART_PLUGIN_ENABLED_FUNC(chart_type: str) -> bool:
flags = getattr(g, "feature_flags", {})
return flags.get(f"mcp_chart_{chart_type}", True)
```
The callable must be cheap and in-process (consult already-loaded feature flags or request-local context) -- do not perform network I/O per call. If it raises, the registry fails closed (the plugin is hidden) and logs a warning.
## Security Best Practices
- **Use TLS** for all production MCP endpoints -- place the server behind a reverse proxy with HTTPS
+21
View File
@@ -88,6 +88,27 @@ THEME_DARK = {
# - OS preference detection is automatically enabled
```
### Default Theme Mode
By default, Superset mimics the visitor's OS/browser preference (light or dark) for
sessions that don't have a saved user preference. Use `THEME_DEFAULT_MODE` to override
that starting point instance-wide for the standard application:
```python
# Default theme mode for sessions without a saved user preference.
# One of "default" (always light), "dark" (always dark), or "system" (mimic OS preference).
THEME_DEFAULT_MODE = "dark"
```
- `"system"` (the default) preserves the existing behavior of following the OS/browser
preference, provided a dark theme is configured (`THEME_DARK` is not `None`). If no dark
theme is available, Superset always starts in light mode regardless of this setting.
- `"default"` or `"dark"` forces that starting mode for first-time visitors; users can still switch themes manually afterward if both `THEME_DEFAULT` and `THEME_DARK` are available.
- A user's own saved preference, once they toggle the theme switcher, always takes precedence over `THEME_DEFAULT_MODE`.
- `THEME_DEFAULT_MODE` has no effect on embedded dashboards: the embed SDK sets the
starting mode via its own `themeMode` URL parameter, which takes precedence and falls
back to light mode when the parameter is absent.
### App Branding
The application name shown in the browser title bar and navigation can be
+58
View File
@@ -243,6 +243,42 @@ Each subject in the response includes flat scalar ids (`user_id`, `role_id`, `gr
than a nested object, so callers can match directly on whichever id they already have — only the
id field matching the subject's `type` is populated; the others are `null`.
#### Extending Editorship with EXTRA_EDITORS_RESOLVER
Deployments that grant edit access to a dashboard or chart through a mechanism outside
Superset's own Subject-based `editors` list — for example, a folder-permission system or an
internal directory service — can plug that logic in with `EXTRA_EDITORS_RESOLVER`:
```python
def extra_editors_resolver(resource):
# `resource` is the Dashboard or Slice instance being checked.
# Return Subject instances, raw subject ids, or dicts with an `id` key.
return [...]
EXTRA_EDITORS_RESOLVER = extra_editors_resolver
```
The resolver's result is unioned with the resource's own `editors` for editorship checks: it
feeds `is_editor`, `raise_for_editorship`, save-as, and soft-delete restore. When
`EXTRA_EDITORS_RESOLVER` is configured, the usual lockout-prevention behavior — automatically
re-adding a non-admin who removes themselves from a resource's editors — is skipped, since the
deployment has its own way of keeping the resource editable. This skip is global to the setting,
not per-resource: it still applies on a resource where the resolver currently returns no
subjects, so a resolver that can't guarantee an alternate editor for every resource can let the
last editor remove themselves and leave it uneditable by non-admins.
Resolved subject ids are also surfaced as `extra_editors` in the chart and dashboard `GET`
responses, so API clients can distinguish resolver-granted editorship from the resource's own
`editors` list. This field is attached after serialization and isn't part of the OpenAPI response
schema, so generated API clients won't see it as a typed field.
Because the resolver is arbitrary per-deployment Python rather than a SQL-expressible condition,
editorship it grants cannot be reflected in list-view filtering (for example, the soft-deleted
archive is scoped to editors via a SQL query). It does still run once per row on chart and
dashboard list responses to populate `extra_editors`, so a slow or unavailable external resolver
affects ordinary list requests, not just direct per-object checks.
### Dashboard Access Control
Access to dashboards is managed via editors (subjects that have edit permissions to the dashboard).
@@ -583,6 +619,28 @@ SELECT * FROM (
queries run against tables that have associated datasets with RLS filters will then have
the appropriate predicates injected automatically.
#### RLS Indicator in the Dataset List and Explore
When a dataset has one or more RLS filters that apply to it, Superset shows a lock
icon badge next to the dataset name in the **Datasets** list and next to the dataset
selector in **Explore**. Hovering over the badge shows a tooltip listing each
applicable filter's name, filter type (Regular or Base), group key (if any), assigned
subjects (labeled "Roles" in the tooltip, but may include users and groups too), and
clause.
This badge also surfaces filters that are inherited from the physical tables
referenced by a virtual (SQL-based) dataset, as described above. Inherited filters
are marked "from underlying table" in the tooltip, and a summary note is shown
whenever any of the listed filters are inherited rather than assigned directly to
the dataset. Inherited-filter detection depends on Superset's SQL parser being able
to identify the referenced tables and match them to a physical dataset by name,
schema, and database, so it's best-effort: unparseable or unmatched references won't
surface a filter on the badge even if one would apply at query time.
The badge is a visibility aid only — it does not change which filters are applied to
a query. Use the RLS REST API described below if you need to confirm exactly which
filters affect a dataset.
#### Checking RLS Filters via the API
You can use the RLS REST API to audit which filters are configured and which datasets
+1
View File
@@ -86,6 +86,7 @@
"Israel",
"Italy",
"Italy (regions)",
"Italy (regions and autonomous provinces)",
"Ivory Coast",
"Japan",
"Jordan",
@@ -493,8 +493,8 @@ Frontend assets (TypeScript, JavaScript, CSS, and images) must be compiled in or
First, be sure you are using the following versions of Node.js and npm:
- `Node.js`: Version 22 (LTS)
- `npm`: Version 10
- `Node.js`: Version 24 (see `superset-frontend/.nvmrc` for the exact version)
- `npm`: Version 11
We recommend using [nvm](https://github.com/nvm-sh/nvm) to manage your node environment:
@@ -507,8 +507,8 @@ export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
cd superset-frontend
nvm install --lts
nvm use --lts
nvm install
nvm use
```
Or if you use the default macOS starting with Catalina shell `zsh`, try:
@@ -129,27 +129,6 @@ chat.registerChat(
See [Chat](./extension-points/chat.md) for implementation details.
### Dashboard Components
Extensions can add first-class layout components to the dashboard builder — elements that live in the grid alongside charts, Markdown, and tabs. The host owns the drag/resize/delete chrome, so the extension only provides the component that renders the element's content. The built-in iframe component is implemented through this contribution point.
```tsx
import { dashboardComponents } from '@apache-superset/core';
import WeatherWidget from './WeatherWidget';
dashboardComponents.registerDashboardComponent(
{
id: 'my-org.weather',
name: 'Weather widget',
icon: 'CloudOutlined',
defaultMeta: { width: 4, height: 50 },
},
WeatherWidget,
);
```
See [Dashboard Components](./extension-points/dashboard-components.md) for implementation details.
## Backend
Backend contribution types allow extensions to extend Superset's server-side capabilities. Backend contributions are registered at startup via classes and functions imported from the auto-discovered `entrypoint.py` file.
@@ -1,155 +0,0 @@
---
title: Dashboard Components
sidebar_position: 4
---
<!--
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.
-->
# Dashboard Component Contributions
Extensions can add first-class **layout components** to the dashboard builder —
elements that sit in the grid alongside charts, Markdown, and tabs. The built-in
iframe component is itself implemented through this contribution point.
The host owns the surrounding **chrome** (the drag handle, the resize container,
and the delete affordance), so your component only renders its content and, in
edit mode, its own editor affordances. This keeps the contract small and stable.
> This supersedes the legacy `DashboardComponentsRegistry` / `DYNAMIC_TYPE`
> mechanism, which is deprecated.
## Overview
A dashboard component contribution is:
| Part | Role |
|------|------|
| **Definition** | A descriptor declaring the component's id, palette label, icon, and layout behavior (resizable, default size, nesting). |
| **Component** | A React component that renders the element's content and receives the [`DashboardComponentProps`](#component-contract) contract. |
## The Component Contract
Your component receives a small, stable set of props. It never deals with drag,
resize, or delete — the host renders it inside that chrome.
```ts
interface DashboardComponentProps {
/** The layout item id of this instance. */
id: string;
/** This instance's persisted meta (round-trips in the saved layout). */
meta: Record<string, unknown>;
/** Whether the dashboard is in edit mode. */
editMode: boolean;
/** Shallow-merge a patch into this instance's persisted meta. */
updateMeta: (patch: Record<string, unknown>) => void;
}
```
Persist any per-instance state in `meta` via `updateMeta`. It is saved with the
dashboard and rehydrated on load.
## Registering a Dashboard Component
Call `dashboardComponents.registerDashboardComponent` from your extension's entry
point with a definition and your component:
```tsx
import { dashboardComponents } from '@apache-superset/core';
import WeatherWidget from './WeatherWidget';
dashboardComponents.registerDashboardComponent(
{
id: 'my-org.weather',
name: 'Weather widget',
description: 'Shows the current weather for a city',
icon: 'CloudOutlined',
resizable: true,
defaultMeta: { width: 4, height: 50, city: 'Lisbon' },
},
WeatherWidget,
);
```
```tsx
// WeatherWidget.tsx
import type { dashboardComponents } from '@apache-superset/core';
type Props = dashboardComponents.DashboardComponentProps;
export default function WeatherWidget({ meta, editMode, updateMeta }: Props) {
const city = (meta.city as string) ?? '';
return editMode ? (
<input
value={city}
onChange={e => updateMeta({ city: e.target.value })}
placeholder="City"
/>
) : (
<Forecast city={city} />
);
}
```
The component appears in the dashboard builder's **Layout elements** palette and
can be dragged onto the grid like any built-in element.
## Definition Reference
| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Namespaced unique id, e.g. `my-org.weather`. Selects the component for each instance. |
| `name` | `string` | Label shown in the builder palette. |
| `description` | `string` | Optional longer description. |
| `icon` | `string` | A known Superset icon name (e.g. `CloudOutlined`). Falls back to a generic icon. |
| `resizable` | `boolean` | Whether instances can be resized. Defaults to `true`. |
| `defaultMeta` | `object` | `meta` seeded onto a new instance (e.g. `width`, `height`, and your own keys). |
| `isUserContent` | `boolean` | Whether an instance counts as content for "is this dashboard empty?" detection. Defaults to `true`. |
| `minWidth` | `number` | Minimum width in grid columns. Defaults to `1`. |
| `validParents` | `string[]` | Restrict which container types may hold the component (e.g. `['GRID', 'TAB']`). Defaults to standard content-leaf placement (grid, row, column, tab). |
| `wrapInRow` | `boolean` | Whether a drop into the grid or a tab auto-wraps the component in a row. Defaults to `true`. |
The layout-relevant behavior fields are seeded onto each instance's `meta` at
creation, so the dashboard honors them — and they round-trip in the saved layout
even if the extension later becomes unavailable.
## Graceful Degradation
If a saved dashboard references a component whose extension is disabled or not
yet loaded, the host renders a non-destructive placeholder in its place and
preserves the instance's `meta` on save. Re-enabling the extension restores the
component.
## Dashboard Components API Reference
All methods are available on the `dashboardComponents` namespace from
`@apache-superset/core`:
| Method / Event | Description |
|----------------|-------------|
| `registerDashboardComponent(definition, component)` | Register a component. Returns a `Disposable` to unregister. Registering the same id again replaces the previous registration. |
| `getDashboardComponent(id)` | Returns the registered component for `id`, or `undefined`. |
| `getDashboardComponents()` | Returns all registered components. |
| `onDidRegisterDashboardComponent(listener)` | Subscribe to registration events. Returns a `Disposable`. |
| `onDidUnregisterDashboardComponent(listener)` | Subscribe to unregistration events. Returns a `Disposable`. |
## Next Steps
- **[Contribution Types](../contribution-types.md)** — Explore other contribution types
- **[Development](../development.md)** — Set up your development environment
-1
View File
@@ -49,7 +49,6 @@ module.exports = {
'extensions/extension-points/sqllab',
'extensions/extension-points/editors',
'extensions/extension-points/chat',
'extensions/extension-points/dashboard-components',
],
},
'extensions/development',
@@ -406,6 +406,18 @@ ECharts option overrides bypass Superset's validation layer. Invalid option keys
When the **Search Box** is visible in a Table chart, the **Download** action exports only the rows currently visible after the search filter is applied — not the full underlying dataset. This matches the visual output and is intentional. To export the full dataset regardless of search state, use the **Download as CSV** option from the chart's three-dot menu in the dashboard or from the Explore chart toolbar before applying a search filter.
### Exporting a Chart as an Image or PDF
Alongside the raw-data export options (CSV, JSON, Excel), a chart's three-dot menu — in a dashboard or from the Explore chart toolbar — offers a few ways to export a visual snapshot of the chart:
- **Export screenshot (jpeg)** — a single-click JPEG screenshot of the chart.
- **Export screenshot (png)** — opens a submenu with **Transparent background** and **Solid background** options. The solid option uses the current theme's background color. PNG produces a higher-quality image than the JPEG export.
- **Export as PDF** — downloads the chart as a PDF file.
The dropdown menu is briefly hidden while the screenshot or PDF is being captured so it doesn't appear in the exported file. In Explore, these image and PDF options are available from the **Export All Data** submenu, and also from the **Export current view** submenu when the chart type supports current-view export.
These menu items respect your permissions: the dashboard export menu only appears if you can download, and the image/PDF options are disabled if you lack image-export permission.
### Sharing a Specific Tab
When a dashboard has tabs, each tab gets its own shareable URL. Navigate to the tab you want to share and copy the URL from your browser's address bar — the tab anchor is encoded in the URL so that anyone opening the link lands directly on that tab.
+15
View File
@@ -88,6 +88,21 @@ embedDashboard({
If the callback returns `null` or is not provided, Superset uses its own permalink URL as a fallback.
### Permalink origin rewriting
This rewrite only applies to the non-embedded permalink path — it has no effect on embedded dashboards. When Superset is not embedded, it rewrites the origin of any permalink URL it generates to `window.location.origin` before showing it to the user, which keeps a proxied or subdirectory-deployed Superset from handing out a permalink that points at an internal hostname the user's browser can't reach.
When Superset **is** embedded, this rewrite is skipped entirely regardless of the flag below: a `resolvePermalinkUrl` callback's return value is used as-is, and if no callback is provided (or it fails), the backend-supplied URL is also returned as-is.
If your reverse proxy correctly forwards `X-Forwarded-Host` and you'd rather non-embedded permalinks carry the backend's literal origin, opt out of the rewrite with `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE`:
```python
# superset_config.py
EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE = True
```
This defaults to `False` (rewrite enabled) and only affects non-embedded permalinks. Flipping the default would regress the common proxied/subdirectory deployment by exposing an unreachable internal host in copied permalinks.
---
## Feature Flags for Embedded Mode
@@ -11,6 +11,11 @@ When soft-delete is enabled, deleting a chart, dashboard, or dataset archives it
instead of removing it permanently. The **Recently Archived** view lets owners
and admins find archived objects and restore them.
A chart used by an alert or report cannot be archived while that dependency
exists. In the chart list view, the archive confirmation lists the alerts and
reports that use the chart; a blocked attempt names them and asks you to
detach or delete them first.
:::note
This view is gated by the `SOFT_DELETE` feature flag. When the flag is off the
@@ -21,7 +26,8 @@ page and its menu entry are hidden, and deletes are permanent as before.
## Finding archived objects
Open **Recently Archived** and pick a type — **Chart**, **Dashboard**, or
**Dataset** — from the Type selector. The view shows one type at a time; each
**Dataset** (shown as **Datasource** when semantic layers are enabled) — from
the Type selector. The view shows one type at a time; each
type is read from its own list endpoint, so the same row-level access rules that
govern the normal lists apply here.
@@ -248,11 +248,12 @@ Ask your admin for the MCP server URL and any authentication tokens you need.
### Datasets
| Tool | Description |
| ------------------------ | ------------------------------------------------ |
| `list_datasets` | List datasets with filtering and search |
| `get_dataset_info` | Get dataset metadata (columns, metrics, filters) |
| `create_virtual_dataset` | Create a virtual dataset from a SQL query |
| Tool | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `list_datasets` | List datasets with filtering and search |
| `get_dataset_info` | Get dataset metadata (columns, metrics, schema details) |
| `create_virtual_dataset` | Create a virtual dataset from a SQL query |
| `update_dataset_metric` | Update a saved metric's expression, name, verbose_name, or format (affects every chart using it; requires dataset ownership) |
### Charts
+12 -12
View File
@@ -15,29 +15,29 @@ description of what changed — "Chart renamed to Q3 Revenue", "Added filter on
'Region'" — rather than a raw diff. You can search the history and filter it
down to changes on the entity itself or on the things it depends on.
## Enabling it
Two switches are involved, and both matter.
## Enabling and disabling it
| Setting | Type | Effect |
| --- | --- | --- |
| `VERSION_HISTORY` | Feature flag | Shows the version history UI |
| `ENABLE_VERSIONING_CAPTURE` | Config value | Records versions as entities are saved |
Both default to on. To turn the feature off:
```python
# superset_config.py
FEATURE_FLAGS = {"VERSION_HISTORY": True}
ENABLE_VERSIONING_CAPTURE = True
FEATURE_FLAGS = {"VERSION_HISTORY": False}
ENABLE_VERSIONING_CAPTURE = False
```
Both default to off. They are separate because capture is the expensive half:
an operator may want to start recording history before exposing the UI, so that
there is something to show when they do.
Restart Superset and its workers for the capture change to take effect. Existing
history remains readable while capture is off, but **Restore** is unavailable
(404).
Turning the UI on without capture gives a panel that reports "No history yet"
and never fills, so enable capture first — or at the same time. History only
accrues from the moment capture is switched on; earlier edits are not
reconstructed.
Disable them together: capture off with the UI left on gives a panel that
stops filling — an empty or stale history misrepresents the entity as
unchanged. History only accrues while capture is on; edits made while it was
off are not reconstructed.
## Viewing history
+9 -9
View File
@@ -58,15 +58,15 @@
"@fontsource/inter": "^5.3.0",
"@mdx-js/react": "^3.1.1",
"@saucelabs/theme-github-codeblock": "^0.3.0",
"@storybook/addon-docs": "^10.5.7",
"@storybook/addon-docs": "^10.5.10",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.47",
"antd": "^6.6.0",
"baseline-browser-mapping": "^2.11.13",
"@swc/core": "^1.16.1",
"antd": "^6.6.1",
"baseline-browser-mapping": "^2.11.16",
"caniuse-lite": "^1.0.30001809",
"docusaurus-plugin-openapi-docs": "^5.2.0",
"docusaurus-theme-openapi-docs": "^5.2.0",
"js-yaml": "^5.2.3",
"js-yaml": "^5.3.0",
"json-bigint": "^1.0.0",
"prism-react-renderer": "^2.4.1",
"react": "^18.3.1",
@@ -77,8 +77,8 @@
"react-table": "^7.8.0",
"remark-import-partial": "^0.0.2",
"reselect": "^5.2.0",
"storybook": "^10.5.7",
"swagger-ui-react": "^5.32.13",
"storybook": "^10.5.10",
"swagger-ui-react": "^5.32.14",
"swc-loader": "^0.2.7",
"tinycolor2": "^1.4.2",
"unist-util-visit": "^5.1.0"
@@ -93,8 +93,8 @@
"@typescript-eslint/parser": "^8.67.0",
"eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
"globals": "^17.10.0",
"oxfmt": "^0.63.0",
"globals": "^17.11.0",
"oxfmt": "^0.64.0",
"typescript": "~6.0.3",
"typescript-eslint": "^8.67.0",
"webpack": "^5.109.2"
@@ -0,0 +1,236 @@
<!--
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.
-->
# SIP: System-wide MEDIAN, Sample Standard Deviation, and Sample Variance metric aggregates
## [DRAFT — proposal for discussion]
This document now has an accompanying implementation in this PR, for the
proposed mechanism plus a first, empirically-verified engine set (Postgres,
MySQL, DuckDB, Redshift by inheritance). It is intended to seed discussion on
whether this is the right shape and scope before it goes up for a formal SIP
vote, not to pre-empt that discussion, the code exists so reviewers have a
concrete design to react to rather than a description of one.
## Motivation
Before #41184 (SIP-216, the non-additive-totals fix), the Pivot Table chart
exposed an "Aggregation function" control with 18 choices, including
`Median`, `Sample Standard Deviation`, `Sample Variance`, `First`, `Last`,
`Count Unique Values`, and `List Unique Values`. #41184 deleted that control
wholesale, and deliberately so: it re-aggregated already-aggregated cell
values to compute totals/subtotals, which is exactly the class of bug
SIP-216 fixed (summing per-group averages, averaging per-group medians, etc.
produces silently wrong totals). #42761 subsequently restored the one piece
of that control's functionality that was cleanly separable from the
correctness bug, the "show as % of row/column/total" display option,
redesigned as a decoupled, post-hoc-only `showValuesAs` control.
A user has since noticed that several of the other pre-#41184 options never
came back. Checking today's metric aggregate list (`AVG, COUNT,
COUNT_DISTINCT, MAX, MIN, SUM`, see
`superset-frontend/packages/superset-ui-core/src/query/types/Metric.ts`),
most of these have a reasonable equivalent already: `Count Unique Values`
maps to `COUNT_DISTINCT`; `Count`/`Average`/`Max`/`Min` are already standard
aggregates; the two "fraction of" variants are already covered by
`showValuesAs`. But `Median`, `Sample Standard Deviation`, and `Sample
Variance` have no equivalent today anywhere in Superset, not just in Pivot
Table, in any chart type, since the aggregate list is shared across the
whole app.
This is a real, currently-live gap, not a hypothetical one:
`superset/mcp_service/chart/chart_utils.py`, `schemas.py`, and
`prompts/create_chart_guided.py` already treat `STDDEV`, `VAR`, `MEDIAN`,
and `PERCENTILE` as valid aggregate values in their own validation and
documentation, but those values are never recognized by
`superset/connectors/sqla/models.py`'s `sqla_aggregations` dict (the actual
mapping from aggregate name to SQL), so an AI agent using the MCP tool to
build a chart with `"aggregate": "STDDEV"` today creates a chart that
**errors at query time** with "Adhoc metric aggregate is invalid." This SIP
proposes closing that gap for real, at the source, rather than patching
around it in MCP.
## Proposed change
Add `MEDIAN`, `STDDEV_SAMP`, and `VAR_SAMP` as first-class, system-wide
metric aggregates, available anywhere a metric aggregate is chosen (every
chart type, SQL Lab metric picker, MCP), not as a Pivot-Table-specific
control.
**Why this is safe with respect to SIP-216, and needs no Pivot-Table-specific
code at all:** Pivot Table's non-additive-totals machinery
(`superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/utilities.ts`)
already classifies any metric aggregate not in `ADDITIVE_AGGREGATES = {SUM,
COUNT, MIN, MAX}` as non-additive, which routes totals/subtotals through the
correct DB-`GROUPING SETS`-rollup path rather than client-side
re-aggregation (`AVG` and `COUNT_DISTINCT` already go through this path
today). `MEDIAN`/`STDDEV_SAMP`/`VAR_SAMP` fall into that bucket
automatically, with zero changes needed to the additivity logic. So once
these are valid, buildable SQL aggregates, Pivot Table (and every other
chart) gets correct behavior for free. This is the version of "restore the
control" that does not reopen the bug that was just fixed.
**Where the actual change needs to land, and what this PR does:**
1. **Done.** `superset-frontend/packages/superset-ui-core/src/query/types/Metric.ts`,
extended the `Aggregate` type.
2. **Done.** `superset-frontend/src/explore/constants.ts`, added to `AGGREGATES`
(drives `AGGREGATES_OPTIONS`, the dropdown in `AdhocMetricEditPopover`).
3. **Done**, but not consolidated. `superset/connectors/sqla/models.py`
(`sqla_aggregations`) and `superset/models/helpers.py`
(`ExploreMixin.sqla_aggregations`) are both wired to consult the new
`BaseEngineSpec.get_extended_aggregation_func`, in addition to their
existing 6-aggregate dict, so neither's original, already-tested behavior
changed. They remain two separate dicts, consolidating them into one
source of truth is left as a follow-up (see Open questions).
4. **Done**, and it surfaced a second, smaller bug on top of the one this SIP
opened with: MCP's own aggregate names (`STDDEV`, `VAR`) never matched any
real Superset aggregate, before or after this PR, they were always going
to error regardless of what this SIP does. `superset/mcp_service/chart/*`
now accepts the old shorthand as an alias, normalized to the real,
unambiguous names (`STDDEV_SAMP`, `VAR_SAMP`) this PR ships, and the guided
prompt text points at the correct names going forward. `MEDIAN`/
`PERCENTILE` were already spelled correctly in MCP; `PERCENTILE` remains
unimplemented (it needs a parameter this schema has no field for) and is
unchanged by this PR, out of scope here.
**The part that needs real engineering care, this must not be a blind
`sa.func.MEDIAN` / `sa.func.STDDEV_SAMP` / `sa.func.VAR_SAMP`:**
`sqla_aggregations` today is a flat, engine-unaware dict (`sa.func.AVG`,
etc., SQLAlchemy emits whatever function name it is given, with zero
validation that the target dialect actually has it). Superset already has
precedent for exactly this class of per-engine capability difference:
`BaseEngineSpec.supports_grouping_sets` and `_time_grain_expressions`, both
introduced by #41184 itself. This SIP proposes the same shape, a new
per-engine-overridable mechanism (for example
`BaseEngineSpec.get_aggregate_sql(aggregate, column)` with a sensible
default, overridden per engine spec where the default does not hold),
rather than a single hardcoded dict.
Verified findings so far (via `sqlglot.transpile`, cross-checked against
known engine docs; **not** exhaustively tested against live databases, that
is necessary follow-up work this SIP alone cannot complete):
| Engine | `MEDIAN(x)` | `STDDEV_SAMP(x)` | `VAR_SAMP(x)` |
|---|---|---|---|
| Postgres | `PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY x)` | native | native |
| MySQL | no native equivalent, needs explicit "unsupported" handling, not a blind emit | native | MySQL's `VARIANCE()` is an alias for `VAR_POP` (population), not `VAR_SAMP` (sample); a naive dialect-name substitution would silently compute the wrong statistic and needs an explicit, verified expression instead |
| SQLite | only if the specific build was compiled with the (non-default) `SQLITE_ENABLE_PERCENTILE` extension (added in SQLite 3.43, 2023), cannot be assumed available | not available in core SQLite | not available in core SQLite |
| BigQuery / Snowflake / DuckDB / Redshift / Oracle / T-SQL / Databricks / Spark | native `MEDIAN(x)` | native | native on BigQuery/Snowflake/Databricks/Spark, where `VARIANCE` is correctly sample variance; T-SQL has no function named `VARIANCE` at all and needs `VAR(x)` instead |
| Trino / Presto / Hive | `PERCENTILE_CONT` / `approx_percentile` (dialect- and exactness-dependent) | native | `variance` is correctly sample variance per Trino/Presto docs |
This table is deliberately not exhaustive, Superset has roughly 75
`db_engine_specs` files. The proposed default (`BaseEngineSpec`) should be
the safe choice (mark unsupported, surface a clear user-facing error) rather
than an optimistic one, with individual engine specs opting in once
verified. Ship for the handful of engines above first, extend
opportunistically.
**`Count Unique Values`, `First`, `Last`, `List Unique Values`, explicitly
out of scope for this SIP:**
- `Count Unique Values` needs no work, it is already `COUNT_DISTINCT`.
- `First`/`Last` have no well-defined, unambiguous meaning as a plain
`GROUP BY` aggregate without an explicit ordering; most engines only
support this via window functions (`FIRST_VALUE`/`LAST_VALUE` `OVER
(ORDER BY ...)`) or do not support it as a simple aggregate at all
(Postgres has neither built in). Restoring this properly would mean
designing an "order by" sub-control on the metric, a real, separate
feature, not a one-line aggregate addition. Proposed as a follow-up SIP if
there is demand.
- `List Unique Values` maps to the `STRING_AGG`/`GROUP_CONCAT`/`LISTAGG`/
`ARRAY_AGG(DISTINCT ...)` family, real dialect differences, plus an open
UX question (unbounded cell content for high-cardinality columns).
Proposed as a follow-up SIP.
## New or changed public interfaces
- New `Aggregate` values (`MEDIAN`, `STDDEV_SAMP`, `VAR_SAMP`) selectable
anywhere the standard metric control appears, every chart type, not just
Pivot Table.
- New `BaseEngineSpec` extensibility point for per-engine aggregate SQL
generation (exact shape TBD in implementation, likely mirrors
`_time_grain_expressions`).
- No REST API surface changes beyond the existing metric aggregate field
accepting new values.
## Migration plan and compatibility
No new tables/columns needed for the aggregate addition itself.
Restoring prior chart settings, the way #42761 restored `show_values_as` for
charts that had it before #41184, is murkier here than it was for that PR
and needs its own design pass: the old `aggregate_function` was a single
Pivot-Table-level setting applied uniformly to every metric on the chart,
not a per-metric property. A chart that had `aggregate_function: Median`
before #41184, with a metric of `SUM(sales)`, was already silently wrong
under the old architecture (that is the bug that was fixed); mechanically
rewriting its metric to `MEDIAN(sales)` on upgrade would change what the
chart's leaf cells display, not just its totals, which may not match user
intent. This SIP proposes a best-effort, flagged-for-review migration
(surface affected charts to admins rather than silently rewriting them)
rather than a fully automatic one-to-one restoration.
## Rejected alternatives
- **Restoring the old `aggregateFunction` Pivot-Table control as-is.**
Rejected: this is the literal mechanism SIP-216 removed because it
reintroduces incorrect totals for non-additive metrics. Any fix has to go
through the metric's own aggregate, not a separate pivot-level override.
- **Routing all metric SQL generation through `sqlglot` expression-building
instead of SQLAlchemy's `sa.func`.** More architecturally thorough (would
give correct dialect syntax for free across more of the roughly 75 engine
specs), but a much larger, more invasive change to a hot path used by
every chart query. Noted as a possible future direction, not this SIP's
scope; this SIP proposes the smaller, `supports_grouping_sets`-shaped
extensibility point instead.
## Open questions
- **Resolved for this PR, worth confirming as the community's preferred
shape:** implemented as `BaseEngineSpec._extended_aggregations` (a
`{aggregate_name: sqla_column -> sqla_column}` dict) plus a
`get_extended_aggregation_func` accessor, set on the concrete or shared
base engine spec class per engine (e.g. on `PostgresBaseEngineSpec` so
Redshift inherits it, but *not* on `PrestoBaseEngineSpec` so Hive/Spark/
Databricks don't silently inherit unverified behavior, mirroring how
`supports_grouping_sets` is opted into per-concrete-engine there today).
Did not route through the `superset/sql/dialects/` sqlglot-based layer;
that layer is for SQL Lab parsing, wiring it into chart-metric query
building felt like a separate, larger change from this SIP's scope.
- **Still open, not addressed in this PR:** how aggressively should
`MEDIAN` degrade on engines without a native or exact equivalent?
Trino/Presto/Hive were left unimplemented (unsupported) specifically to
avoid silently answering this with an approximate function
(`approx_percentile`) that changes the semantics of what a user asked
for. If someone wants `MEDIAN` on those engines, this needs a real
decision: require explicit opt-in, show a UI warning, or keep it
disallowed.
- **Resolved for this PR:** left the two `sqla_aggregations` dicts
(`connectors/sqla/models.py` and `models/helpers.py`) unconsolidated,
both now separately wired to the same new `get_extended_aggregation_func`
hook. Consolidating them into one source of truth is real but unrelated
cleanup, not bundled here to keep the diff reviewable.
- **New, from implementation:** only Postgres, MySQL (partial), DuckDB, and
Redshift (by inheritance, unverified) ship enabled. BigQuery, Snowflake,
Trino, Presto, Hive, Spark, Databricks, Oracle, and T-SQL all have
documented (not live-verified) support per the table above but are not
yet wired up, each needs the same live-instance verification treatment
before being enabled, this PR intentionally didn't guess.
+1 -1
View File
@@ -67,7 +67,7 @@ const communityLinks = [
'Join our monthly virtual meetups and register for any upcoming events on Meetup',
},
{
url: 'https://github.com/apache/superset/blob/master/RESOURCES/INTHEWILD.md',
url: 'https://superset.apache.org/inTheWild/',
title: 'Organizations',
description:
'A list of some of the organizations using Superset in production.',
+18 -18
View File
@@ -21,12 +21,6 @@
"lifecycle": "development",
"description": "Enables experimental chart plugins"
},
{
"name": "CSP_RUNTIME_ALLOWLIST",
"default": false,
"lifecycle": "development",
"description": "Allow users with the \"can write on CSPAllowlist\" permission (Admins by default) to punch holes in the Content Security Policy at runtime, e.g. to allow a new domain to be embedded in a dashboard iframe component. When disabled, the CSP is purely static/deploy-time and the allowlist is ignored."
},
{
"name": "CSV_UPLOAD_PYARROW_ENGINE",
"default": false,
@@ -99,12 +93,6 @@
"lifecycle": "development",
"description": "Enable semantic layers and show semantic views alongside datasets"
},
{
"name": "SOFT_DELETE",
"default": true,
"lifecycle": "development",
"description": "Temporary rollout / kill-switch gate for soft delete (off = legacy hard delete). An emergency stop, not a clean rollback: flipping ON->OFF resurrects already-soft-deleted rows. Retained through this release as the move-back lever; removed (along with its two gate points \u2014 BaseDAO.delete routing and the do_orm_execute visibility listener) once post-flip confidence is established."
},
{
"name": "TABLE_V2_TIME_COMPARISON_ENABLED",
"default": false,
@@ -116,12 +104,6 @@
"default": false,
"lifecycle": "development",
"description": "Enables the tagging system for organizing assets"
},
{
"name": "VERSION_HISTORY",
"default": true,
"lifecycle": "development",
"description": "Enables the version history panel on Explore and Dashboard pages. History only accrues while ``ENABLE_VERSIONING_CAPTURE`` is also on; with capture off the panel renders but stays empty, so the two ship with matching defaults and should be changed together."
}
],
"testing": [
@@ -138,6 +120,12 @@
"lifecycle": "testing",
"description": "Enables filter functionality in Alerts and Reports"
},
{
"name": "ALERT_REPORTS_RETRY",
"default": false,
"lifecycle": "testing",
"description": "Enables automatic retry functionality for failed report executions"
},
{
"name": "ALERT_REPORT_SLACK_V2",
"default": true,
@@ -239,6 +227,12 @@
"lifecycle": "testing",
"description": "Apply RLS rules to SQL Lab queries. Requires query parsing/manipulation. May break queries or allow RLS bypass. Use with care!"
},
{
"name": "SOFT_DELETE",
"default": true,
"lifecycle": "testing",
"description": "Temporary rollout / kill-switch gate for soft delete (off = legacy hard delete). An emergency stop, not a clean rollback: flipping ON->OFF resurrects already-soft-deleted rows. Retained through this release as the move-back lever; removed (along with its two gate points \u2014 BaseDAO.delete routing and the do_orm_execute visibility listener) once post-flip confidence is established."
},
{
"name": "SSH_TUNNELING",
"default": false,
@@ -251,6 +245,12 @@
"default": false,
"lifecycle": "testing",
"description": "Use analogous colors in charts"
},
{
"name": "VERSION_HISTORY",
"default": true,
"lifecycle": "testing",
"description": "Enables the version history panel on Explore and Dashboard pages. History only accrues while ``ENABLE_VERSIONING_CAPTURE`` is also on; with capture off the panel renders empty or stale history, so the two ship with matching defaults and should be changed together."
}
],
"stable": [
+343 -336
View File
@@ -2145,10 +2145,10 @@
utility-types "^3.10.0"
webpack "^5.88.1"
"@emnapi/core@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.11.1.tgz#b9e1064f3a6b1631e241e638eb48d736bfd372a6"
integrity sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==
"@emnapi/core@1.11.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.11.0.tgz#8a655042dbbb10d0266670c9903c34a7001c705b"
integrity sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==
dependencies:
"@emnapi/wasi-threads" "1.2.2"
tslib "^2.4.0"
@@ -2169,10 +2169,10 @@
"@emnapi/wasi-threads" "1.2.2"
tslib "^2.4.0"
"@emnapi/runtime@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.11.1.tgz#58f1f3d5d81a9b12f793ab688c96371901027c24"
integrity sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==
"@emnapi/runtime@1.11.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.11.0.tgz#ce16b3674ff7266bbf50f9668bde8a04f3014d4e"
integrity sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==
dependencies:
tslib "^2.4.0"
@@ -2934,13 +2934,20 @@
"@emnapi/runtime" "^1.5.0"
"@tybys/wasm-util" "^0.10.1"
"@napi-rs/wasm-runtime@^1.1.4", "@napi-rs/wasm-runtime@^1.1.6":
"@napi-rs/wasm-runtime@^1.1.4":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz#ed33806d0f9be98dc76d0c3d4fd872fda701b5d5"
integrity sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==
dependencies:
"@tybys/wasm-util" "^0.10.3"
"@napi-rs/wasm-runtime@^1.1.5":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz#97e3d45d7424dc5da1d4e32f3bf3b292f6c1b44c"
integrity sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==
dependencies:
"@tybys/wasm-util" "^0.10.3"
"@noble/hashes@1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426"
@@ -3076,199 +3083,199 @@
resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.127.0.tgz#8374fcdfb4a641861218daa5700c447c00b66663"
integrity sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==
"@oxc-resolver/binding-android-arm-eabi@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.23.0.tgz#31b3087c2c8a9d100ae22aced377c03befc87efa"
integrity sha512-8IJyWRLVAyhTfe9/TIEbQqSQnl5rUqYJrUOS6Dkr+Mq9FGHMxDGeiEmwkBqCvDP5KckpPh/GYSgbag66O6JsCw==
"@oxc-resolver/binding-android-arm-eabi@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.2.tgz#ef832062e3a2f0c7604a8312c34eacf23a7bd7e3"
integrity sha512-xQoCRv+gKax9KTdwdaQNnAFOai8neay7g3jExDIORzhbrejwGSJaZNTdOJHR5ziLg2joMxOCMOFMo4zuxza2uQ==
"@oxc-resolver/binding-android-arm64@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.23.0.tgz#744d8c82d37189ddb76f911b6fbd9915d8090a9b"
integrity sha512-pprVojnNhHxupwTT2gdeUlkxll6XEvWWBk3oVicOSNVWQC99OBnDhMQDoirqnzrE1bScQSMS2JgPpqdlrhz/Fg==
"@oxc-resolver/binding-android-arm64@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.2.tgz#be6093b76621981c57833172a7fa9b5b0753daef"
integrity sha512-HF5oiE2L05yInPYCFD/4uxSrEZW4SuIfn99Y6L1xnJnzl066JR+MJs2rIdstw8A2MPlAKH+13dpFPNycjqzvGg==
"@oxc-resolver/binding-darwin-arm64@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.23.0.tgz#a57f71c22d3d39da3ce08fbd672a25673ad38364"
integrity sha512-mbIrWIMAJeytyee36OyUP5XH92TP7FaKaQ2m5AjokKy7STgjrhRt7SMXqpqLjhGm6Xn721Xmsg6H3Rtd9YQETw==
"@oxc-resolver/binding-darwin-arm64@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.2.tgz#b5ec0b6774ef9c60bb3e9237b39b603aac39f75f"
integrity sha512-UX4u49CVCAD8QZNELaW8eMGgMAGwFWYEPbvNsh+3r/gs4NX3KfpiACMVwRQT0EuH3uat9hM5Zl+Ppm9pJD8tgg==
"@oxc-resolver/binding-darwin-x64@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.23.0.tgz#ca28168b5aca8003c2a114731ffc556f04ac40e4"
integrity sha512-UnIphmZ1LazUCr9DXWaKYWtKDefPMbgLsywaoYxRqVCNHhq4MM6d2q1Nz1i9Vzxt5i+cE2nRUYpAUHr/lijNYA==
"@oxc-resolver/binding-darwin-x64@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.2.tgz#79dbeeca048fda1de9c71169b08924535369a14d"
integrity sha512-J9xPx7YBkrRmJ+xl561ztnMWEc1aOyjEIxBiGX1dVb3u7bGSnfObfcZk+Pd+uM0HZAPNsQ1xvD8j52A/uOSqNQ==
"@oxc-resolver/binding-freebsd-x64@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.23.0.tgz#b956c5b3eda65e1302e291c185d7fca10fbfd7fb"
integrity sha512-aaZ/cSEYFkSxgS2hOrobT6RQcsWNviOX8dW6CEkVx2/UYkAf9MeHbjl3W0usWV53rVV//ndBdn2nb1y7jsu4lw==
"@oxc-resolver/binding-freebsd-x64@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.2.tgz#8d49e6cb2269597b6ef0bc90c5b23e68d946f8c3"
integrity sha512-fRlt7OvSaQkWj6+EDTVxawVxOlqJB2QSnBfkeCyK4RTvsGctbw3BiH2Tb7DzMs7bikc4BRBpvWP5zF9K8b54Zg==
"@oxc-resolver/binding-linux-arm-gnueabihf@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.23.0.tgz#f59878badc556b023a12980f8fd4ca363f8065e2"
integrity sha512-IoJLvO5SjLSVMaq83BNTrPCb1FppvoJc1IhZ5CoUVl3PykUBku7D+LK1j0GSurhJcIc6zfjghsvaZNpq5ev6Mg==
"@oxc-resolver/binding-linux-arm-gnueabihf@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.2.tgz#84413d558915ecf81da5914336b9fe9a70a84acd"
integrity sha512-gK+vPUcPQITkGwBKpZGrcDHSlU6eDGl7AQacxS2CEKAZIBHWkOVFeJwLZ4tYnA1acJqRM5lt7yYwPCVqGHIJ7A==
"@oxc-resolver/binding-linux-arm-musleabihf@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.23.0.tgz#19e4cd124f6fb0f17c2693525422be493edabf39"
integrity sha512-vskFpwg44T/LFsfjSCnVZ5ygcuqzPC1yUzVEiKa8BgHAQz0+QLQQW3EGWLPVi8EXFghzjR4EtgPBtOhCjU4jdw==
"@oxc-resolver/binding-linux-arm-musleabihf@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.2.tgz#af2554d8984592926ec92fd838bd01e3e14f2f53"
integrity sha512-L/Rgas7SrOKy/z7IH+HxSFRqVO4PuLDKLEGKvnhoCBBq3UJ0YzGBou3qMzaAGgkJwsIvX2pBF+7ojzfUhLiZxQ==
"@oxc-resolver/binding-linux-arm64-gnu@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.23.0.tgz#770c64d6f976c6374de3314942616b6c8f2afb05"
integrity sha512-//TcHVhrChyw5RYtgts6WO7KcWq9387c1Z5Zvhqpk/ktAbyaRYgBZrpSY1GDCFq50ASt6B6jhh+JxB1rB45IAg==
"@oxc-resolver/binding-linux-arm64-gnu@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.2.tgz#453a2e5d1d858d7e5bff0ddde584ec46560a37b6"
integrity sha512-CUEYvlX1Fk7E9kUMzuswru1J9HLxMwnpDeQGjQuI4ZH+iNCoa2X9T+pvyzrbsgl7WnIeTFTlNlHsRfVdf5g9/g==
"@oxc-resolver/binding-linux-arm64-musl@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.23.0.tgz#88640f695e6ca9f71641b59f2996d5bb3eaaea42"
integrity sha512-ZFqlwiTf7CXLLSGyAR9tYiO33LiaeIEXW+xm42d8mnUGpDgPltyrCGYtQezyMMEXvjhOgCz1X+i7sbDTJEx+bg==
"@oxc-resolver/binding-linux-arm64-musl@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.2.tgz#4e04fb8d43aa3baa037f88a292b0eee386dfd742"
integrity sha512-ViN1ZibQyxwC67GpoP33oo+S9UyUnkog13vzQb9+v9bCNvrVzJuk0MRdacjtv/9xfRMVF/eqHFyl8YOeykI10Q==
"@oxc-resolver/binding-linux-ppc64-gnu@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.23.0.tgz#7c2a0c45ab65354b89f8777c806fcfebe09f12e8"
integrity sha512-oZ5LeN5+H1R19dRjTAxKrxQguH+AsemHcnthEfFxf4OjmBSty2doHLeSmMunKy3zpTHJQ3lh3Af+dNS+W6dYeA==
"@oxc-resolver/binding-linux-ppc64-gnu@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.2.tgz#662de504b88a9d7c745808514bfdb2a4e9f53760"
integrity sha512-CU1sCqWnhGqYD5I1HedHk5pujrn7ssDkNB/AEQd/pd3D/EojVSgJUlpbafwIbyhia3PgIfkvdFpRPWSALzVumA==
"@oxc-resolver/binding-linux-riscv64-gnu@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.23.0.tgz#fb502eb40ca7576d826672ba9f6c6ba9bb87094d"
integrity sha512-O4ciFDyX5ebQd0qkb1bjAIg8IEfiLT03GbSeylwlwlUMK9KwBWaALwrxSbc0Msaz4U6iPj+T9eRXpD5mxBfmvA==
"@oxc-resolver/binding-linux-riscv64-gnu@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.2.tgz#72e7d1edb906d42a2ad339a3a6be5b097c00a0a0"
integrity sha512-jWVyZtIHca4Gb96x7dag+y69vlei7ffjrsveLkmf2ZhqEAz6ZSBnY1GWvgZUaZlwZP62A3xD092BW97Q5VGc+g==
"@oxc-resolver/binding-linux-riscv64-musl@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.23.0.tgz#2159fe2807e1356d9149a151d06cd9a0c7253224"
integrity sha512-P3o8Y9kISYjcxadmbO+94ThRwLhwGuDAbA7dcdd4+YLpfeF+mmobz8fXf4NmSdfSqjyRSkceJDBRZha9NVYkiQ==
"@oxc-resolver/binding-linux-riscv64-musl@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.2.tgz#cf5788f4dd858ae6a27d1252b0b662b39a4bbf2d"
integrity sha512-LF29obFqNgBUgDX7rmUK7M4D0JQG5LxhYzn3xXmECcHU9aQAdWG7NiY052qybtesEdwHQXKNTWYQ7mTsybNvWg==
"@oxc-resolver/binding-linux-s390x-gnu@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.23.0.tgz#1fb7c19af499a5701d53f2aa38e841f153d7cf86"
integrity sha512-oj03m1E3RmTFczKhcKJDzHaEDKJnPIsDcQFVxBJsSdXGSuIPdt5TvcM332FfMQgzI6yDJqyl4InrnFfXrmUTKQ==
"@oxc-resolver/binding-linux-s390x-gnu@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.2.tgz#ec3933ccb443b68eb9b879b7f25489fcde11cc53"
integrity sha512-+NYcm+cCHBbtdQQ3A4phQTSuVRYnNHz7wrl9XRAPEovcdoqi0mb1K5ZOl+jN54ZD+q1zz3V0vltbFJmzecJKmw==
"@oxc-resolver/binding-linux-x64-gnu@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.23.0.tgz#580644712d78ebe1c1d4b7077db992ce149d46e8"
integrity sha512-BqJxbSC8FdP7mSuSpRePTGHm0hXWV+dfz//f7SjsteZncLaBgWTBmi/OZNv7sX6CyG/Pt/eJkPorP+DkMOhMwQ==
"@oxc-resolver/binding-linux-x64-gnu@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.2.tgz#9cb87e203ef40631d7ff2cd1a8a6c145a5595054"
integrity sha512-UQqZDdG2r2HhAOsZEgufkIWHPQ886IUyuJQkoZByvzhW8j51R4UNzGBJFkTiTnLhQnggwJRdJgFWK4uY6ZVIMw==
"@oxc-resolver/binding-linux-x64-musl@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.23.0.tgz#709b86ff4c0a43bfeb4d7e5cc7334ee77780c5ae"
integrity sha512-utmw+VmUrW4K8LI5/6jhg4aGYKJHOIjQ9syYOOA6pF3w7haKu4r4enTe2U0C04/HbUvkq/Zif43xFsKW1Pnq9w==
"@oxc-resolver/binding-linux-x64-musl@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.2.tgz#a1ebb07307eccb2b150dc6bcac5a575cf3b647bc"
integrity sha512-3Q9PMRjalWkT6NZ4jfujuqTCFwoWErg3y3BnOgb544B8IMw4PktiwWOigMfOHNRLMghZeJ7hpfpZf4CP7rV7Og==
"@oxc-resolver/binding-openharmony-arm64@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.23.0.tgz#580331f88fbdbc7b518a93e2ab43a7692e24f7f4"
integrity sha512-V6lbRrthHa4TbvsLjPtg+EkXT1tRY+s4I8rYLXUfiHlZzGx3sLv1EH9CEOOevjvUYHLsbe/gqCIc73XnQfPb9A==
"@oxc-resolver/binding-openharmony-arm64@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.2.tgz#aba2e14c823e3356acfb8bce06be97d5aa5fcab8"
integrity sha512-Eljeq3ndtyKhM+Es8LITi4Zl2htzuRZcrPMF3kMCsrILztvU6AjZ3FuEhHHKobPUB9rMpzLpJP5bDqXI7r+iog==
"@oxc-resolver/binding-wasm32-wasi@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.23.0.tgz#4d5f1300d1a4ef18b933329ad7c6b395bac28580"
integrity sha512-gRoOxQPdnAmIAjxcuQNBxfihvx+wjTaQM/9/eP12xwnGNawOG/+Zz9RHN4WNSxT45b5CrscK4NB8aPh+oZQXAQ==
"@oxc-resolver/binding-wasm32-wasi@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.2.tgz#8840346c1b8a3028b1b3575abb2e45fed529d3d6"
integrity sha512-HGDbNsIywqc4LxU38+CTJNnB/6BF7rheWOJ259b4eE0aEnelYblC8x+1tEd63bp31fYne63RQz9Jb4yVnC7Yig==
dependencies:
"@emnapi/core" "1.11.1"
"@emnapi/runtime" "1.11.1"
"@napi-rs/wasm-runtime" "^1.1.6"
"@emnapi/core" "1.11.0"
"@emnapi/runtime" "1.11.0"
"@napi-rs/wasm-runtime" "^1.1.5"
"@oxc-resolver/binding-win32-arm64-msvc@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.23.0.tgz#87145652fdf60741d07b893150d61fa06e2a1d15"
integrity sha512-CgTGMYsJVe1eUiCdJTpGw21svXw79ITsemN1h0hcNkiswasDbN5MoibSLY+gRMWP5syfEz5iffrjZnwEP8xeUA==
"@oxc-resolver/binding-win32-arm64-msvc@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.2.tgz#c987ab635ffe553e6230255a07c0576686f5ab45"
integrity sha512-iWx25CBEgH49iE9q5coEGI/jb1jl5kkCY9z6U5Og67xCkQ/WFMDc2J5U78+AE91SUxM2NqSLhJC8/PLfWnImww==
"@oxc-resolver/binding-win32-x64-msvc@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.23.0.tgz#8b66dbfa7b796139e719063fc0e44084e80a1c15"
integrity sha512-gUGJpr+Rn6zMxm5juApV0K3U845i8t47o8k+rbO0BHbi4PoJIfSPeQmrE2dgohQm2g5k6iviNFyXCGqvmaYUpw==
"@oxc-resolver/binding-win32-x64-msvc@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.2.tgz#dbdf12721396ef4c31899d0963b002df32f57050"
integrity sha512-VPoCAhKvCQTlG7vxqaBXcmuvbh77BfnGXj8g0pbvVXpm1F/R8rDVwqIWcEbMzrI1JvJlm8v7T9uMVQb6UctMRg==
"@oxfmt/binding-android-arm-eabi@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.63.0.tgz#136176dc94fdc41e21415cc770d86f5066282e0f"
integrity sha512-YmRth4ZPGgEXcgmkhvANbC9uD67dxmSobW7DQuyt5tOBOKvPnIpk5SVHBj88E+7wMNRI2FhqaDbOhQFBix+b8A==
"@oxfmt/binding-android-arm-eabi@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.64.0.tgz#e14e25c032f6d8a6b025eb5ee7bb606c3cbdd10e"
integrity sha512-o6uzh/jTOQeAY5TdkAeXdqv7MBRcPxiRA08zrcBtkKj5cSu/FMu0Hl7Q6Fi1KCKyCWZ6lJVjBzdsJvsKltUsGQ==
"@oxfmt/binding-android-arm64@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.63.0.tgz#10bc42457179210061c801122a64304619e3bdab"
integrity sha512-icbahX8X2X3sRamOMecvdYeZXWjPDazRDIfvWfy7Ca1nc/ZDT2Y9k5Nt7s46EqFd7NQPdgk+CM3/SgIT5LPCaQ==
"@oxfmt/binding-android-arm64@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.64.0.tgz#294a15b8402eedde0e0a467748e3efadf61bf523"
integrity sha512-jRGSUeeP7p3Gynw2YaCVtjBIA6ZxY6bEB/ES5i54OhqmRTyuVg7ZgstEtzgq6GOAJd+2QZ5pvf+bFfmW5Mp9cw==
"@oxfmt/binding-darwin-arm64@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.63.0.tgz#5f9084d9a760a1836387f8970a7f9d614ec3d909"
integrity sha512-WV+Ze5v5gI2qoj8jpAovt8KBTW8pjEz/AiMXXjeTQS+Bmf/MmZXTS40S8xNPDszX+W8WDv2Bbk6qKrMTtUGu1A==
"@oxfmt/binding-darwin-arm64@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.64.0.tgz#d55b1a5d5d97d4ccde8e4be7b63e06e4e56f2d13"
integrity sha512-JINwtU2lW7nOFSqi+H2qplipNUqah9Gc1jgGmB82kTD4UnZrZIVxCJ9qEmFiKfjNq27gYLFhrUb0to86aCwMjw==
"@oxfmt/binding-darwin-x64@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.63.0.tgz#badd4a02218a9a62319817d5c337b30159a54a21"
integrity sha512-CJGSBdDxXOWIpoFXHpverimCvz084KA7L483rqJ44c3jDtzv6d4qOSoR/V9ywSHfV+Ks1lwIj2P49BFhunLNAA==
"@oxfmt/binding-darwin-x64@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.64.0.tgz#1c9673270ed597ba9456d40fa0607d50e81158ea"
integrity sha512-gCmuswrgrOSajV4HCRFkVCGIruPq8bjYuPYgSE2WQB3mD6XrdyZ3JMSRZCkQ8zCxOyGWriBo6QoZ5nmMHQ1BfA==
"@oxfmt/binding-freebsd-x64@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.63.0.tgz#a17261e95c8ebef1f76d8aaac746a64fdb6ba51e"
integrity sha512-BDfKY+KhL2078cgswBBFQPAYuxCy93bS/iC5frdSeSbTLcGrR6VC2hsuPTanoJmg84+wSyWl0wWC1eR+uTnkRg==
"@oxfmt/binding-freebsd-x64@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.64.0.tgz#9e8f8b3a5a558043c664d43d54e441756af30c56"
integrity sha512-Ab8g7a38pT0MMImjh7anRSTve6buWBIlcXIFBYa5xl4s6UxEgKSc2xOOhbGtLwvXnEi2PsEDGoJh3oUU7xkehQ==
"@oxfmt/binding-linux-arm-gnueabihf@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.63.0.tgz#baeee34bb08e0769af878623f442e83bc0aacd7a"
integrity sha512-Ov1cQEXT4mj7cojAokWSS1eoxkoyvbDfAbxNsGIKY2o36kvdAaFzPxRN6NxFRk9fD72B8oCoTTX/NuYTUWlpsg==
"@oxfmt/binding-linux-arm-gnueabihf@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.64.0.tgz#cfe552538c9e9402ca64d7b83b1ccf02457ef391"
integrity sha512-BgvS3CoQ+Xy2deoZqEN8JVKabcCZi2RxA3yant8G9OAv9KuPJ9TCjHkqigzdHUVwErZxEP5d2bzLIEyKYyBDLg==
"@oxfmt/binding-linux-arm-musleabihf@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.63.0.tgz#e70d5697ec4b6bb5f87a3f019e01b3f956b8e44b"
integrity sha512-0LE7ro3+6L79jcMANycAZfRaC7zxr9YZ2+vEL5uMD9QlEep+rS/r1kSJsnuLl991NXJZD60euh0PC1GHrR20vw==
"@oxfmt/binding-linux-arm-musleabihf@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.64.0.tgz#1944e367da59e8b1770c5ba96465d0c7e640053e"
integrity sha512-QXpNxwoMj0YvnceCNZadNSden3bIcnvjn/sDp/rwZhRoZoZYGpHvtPyhGsdJz9uvT9GkaMW7SsLddurU56dt8w==
"@oxfmt/binding-linux-arm64-gnu@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.63.0.tgz#638a8ed4f3d256c50aeb6d2c19cfc65792c902e1"
integrity sha512-izPk+2Z4gjuZK32Fqh5qXoMpT/2NXzLh++ob57HiEiVSQZ1iYXu8EKMzb+K5AvWyIEXhdDIt7ADjGGtFhkT9Bw==
"@oxfmt/binding-linux-arm64-gnu@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.64.0.tgz#510386113bf6a128cf3106d612471dbd1a13b0f4"
integrity sha512-BBgH3I1ppDsI5pZ4Pdhw0ceYxwVCfbU/bZEBCeZ6caRS9x0ZabErxubP7riGUn11PXZBhe8DYdjkDKP1FlVQ5w==
"@oxfmt/binding-linux-arm64-musl@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.63.0.tgz#af5a9b787f5233f27a3360ad56235fc1b011f760"
integrity sha512-alPmbOuWXFXiSo+lOtv6X71C7SYMEDW2WVvywOvf9BwKgEhSNGhMTLeFVSjKUMCamcjbbgVdsWF8GN1uy8xshg==
"@oxfmt/binding-linux-arm64-musl@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.64.0.tgz#7235405901cb0368b659eb42b362a817fc3330a3"
integrity sha512-v19HSjC/BGXdt26qEvKZtwAHgGmQ2Agcap2kQP+KIqoRZqivVzYth3ui2dJA1i+6/fjpjga85lIOaJJjQ/bOOw==
"@oxfmt/binding-linux-ppc64-gnu@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.63.0.tgz#c1a211206134a5577e355a495989e0d733218d60"
integrity sha512-BdzCPvolJc4AWZ+YMzgUDJcDzbQWrFjYuqBHoNHNqP1aCaluQRJNs4k3vNU5IG7vTpjf9zeD73D7MFM1TecZpg==
"@oxfmt/binding-linux-ppc64-gnu@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.64.0.tgz#1f0563c530dfa634682ffa32d16830404b95a8c6"
integrity sha512-PElLnOo4xFTBZrxPhgTIj0eHqZXwEBQoNWtb7facUV170T0B0FRET0iNbb3LUeLWTybkUW+vsdyv4ihOdyXGyw==
"@oxfmt/binding-linux-riscv64-gnu@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.63.0.tgz#4863f0311e5c1b88f75ef822959b3ca4fd938937"
integrity sha512-7sIgfLzqtNKSkMGsGVyRpHwpjNezRg2XONvUOheFZs95TSZpM0JAuPpA8KrQFsWc4wPU95roX2O69JgH8igOgw==
"@oxfmt/binding-linux-riscv64-gnu@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.64.0.tgz#36f55e955c5b38b587470f181146c9a11cf8bdb1"
integrity sha512-Qzsg15n4F5CH+MorcRW4MkAEMiLzXmeG+DiDSbP/bBTqCmWOH3K9DHryNrve+JHlV0txS+B6Z9P5Xz+cmWeL+g==
"@oxfmt/binding-linux-riscv64-musl@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.63.0.tgz#ad05a017d12553e2f544743c4940adb552aa1d1c"
integrity sha512-9Tcg0y0WcVa6Mm9AgcgFMseDS+VkFJZpKZ8We9SpDY4gg5jewSwln+0sO04QLcTS1BtfDl9MwR+NfID8L7PUTg==
"@oxfmt/binding-linux-riscv64-musl@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.64.0.tgz#bb9c6c3860c8832fe271623eb6131ea5f5e094cd"
integrity sha512-/GZ358wnQ/Ez4UVnCcZIi56JkY0sOdZ+B108pqXKqZz3jLS59F4KEAB1Qv3fRlObrFEk+3L2vUQ/xoPx+3vjXw==
"@oxfmt/binding-linux-s390x-gnu@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.63.0.tgz#2803f539db15bc66db115888fa8f84d6531ed2b9"
integrity sha512-qWKC1pEOpx1qYhXaugPhHUeXwSfqEOk2wJH2LqVXGPV5iQYfdAZdt+d2XDiX4DTSWA2QDMUcFB+wEORh3Xn/sA==
"@oxfmt/binding-linux-s390x-gnu@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.64.0.tgz#7d736d923f3c7f88743f26479a49903c6dbaf818"
integrity sha512-/C9We3DXegowfLXtVCYHeNiU9azwCDr5cQkEtCVlc74vyn+lLQSPApJ1CZmxAduqeq/Oi3gQ+IVptyhCaTMtkQ==
"@oxfmt/binding-linux-x64-gnu@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.63.0.tgz#c22a06a60ae2d6b3de522095e0c50a816040a033"
integrity sha512-S9wXYOiGSqYGS4Fx/TFsY+xDd/7dE5s+rUgbA4TsHiVF9e8J3ZcKmP7dsP/7iqLI9Wz7Ic7TzEr3mdthRCTdrA==
"@oxfmt/binding-linux-x64-gnu@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.64.0.tgz#34dfe2bde9ed124324b45aae078618456e850452"
integrity sha512-91KM2CeRWscIEHlj1NsW2WSnzGeq1Ehq+39bfDowTdkn+fcvK/x4Y1RcyqT7glyBjZio0ldkeCG6Usj3v7ASog==
"@oxfmt/binding-linux-x64-musl@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.63.0.tgz#48d3eeaf8e3757f638cf92de5ee4858befc9c0a3"
integrity sha512-5eGyTJuMZNwBSHCivXt8Yuta6GeTYksOPXRk2MIhajiyFGQx7bjaHIwY+ZusAoFHhT157A9x6sktLjYo9D5oMQ==
"@oxfmt/binding-linux-x64-musl@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.64.0.tgz#b5edc644409aff9715279650767d34d2fb65d59a"
integrity sha512-gw7uEk9I+7zoT1EYLra1eWArIzNcz8e3jkv+Noo2+o2T7wPvsNSQbfoa4DSfZlvn1i6mJ05RiZ4/omaXPDNhQg==
"@oxfmt/binding-openharmony-arm64@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.63.0.tgz#02be9e140ae35ba30f52bdce27612fece4a01ab3"
integrity sha512-Rz7hx+Dv3DoW/S6pwVAyjfFXp7/trdQ1zg+vNmsdsdDNlUccugp4XNqambSuEAeP0DaG9k72AtNyfDXCEg0AGw==
"@oxfmt/binding-openharmony-arm64@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.64.0.tgz#6b1d9c662e08bf5fbc1e9ccdb45ed28c004b90c4"
integrity sha512-HYHFf616FHSPSO07c09mjmXBfQ73wIVM3m0txOiooa5XZkGoxFd6B14PVj0LB0DXIqJ6wAO/dDR/NX/5UUaqnw==
"@oxfmt/binding-win32-arm64-msvc@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.63.0.tgz#2226eaf52b6345a2cb926499216b2486cf0dbec2"
integrity sha512-T/IuizKN9mr4Xw6YYnptkXRNdLkyIlUZ7c8zfTOBpoytZyJ1BAsMUvsMDEx0X4YvSMpaivm+DR8112rQfzC25g==
"@oxfmt/binding-win32-arm64-msvc@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.64.0.tgz#bc5a005e159a8f9af4168eed2e61fe477f4029db"
integrity sha512-uQjFp081IZSWD6VAofX2iO2z01awAdHmfC+NrieWIPKrT2hZKQDyq/U18M7ifC0sm0Wz8aHY/p6+FDYIzs/CrQ==
"@oxfmt/binding-win32-ia32-msvc@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.63.0.tgz#58d263bb5ecd7330c02f9dcd8cda10f66e42e74b"
integrity sha512-XjrO5FJ5Wl9vsAxtCP1G/eaeT6y1K2s9CICUHGE42cEjou32/J6S+B1KnrOAboj6E7uhJnwPbRSvznWcxNdA0g==
"@oxfmt/binding-win32-ia32-msvc@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.64.0.tgz#88e90b96f7b39e4b6f75178c94c52d464fa58b53"
integrity sha512-lNM6byTAQ881jugzFu8juJTbNRgsUTlswMA6pJmwi1XDvmIqnnb49lcUAs5gz94fCJLrVN+/X3s3jOKqx23WIQ==
"@oxfmt/binding-win32-x64-msvc@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.63.0.tgz#02a166c8a8049c55d0096d1ba9d8e73f3a4d26a7"
integrity sha512-sgsHCQy432OTQH4Ikk3tZptp3GqwnhwUDuY0loBH41zyHWfMZY9v8Dy78wsnSofHejvFozZGgJgBB1A0LQRwMQ==
"@oxfmt/binding-win32-x64-msvc@0.64.0":
version "0.64.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.64.0.tgz#788c7fe26f89e57269f79e8f8a34e9b1497bc674"
integrity sha512-BtmbtL/QjMtF1a6C3CqoDluH2IfB6fJt62E+B9RFfUPtFk4Iz9PFS6+y/SzzOvSxc7aUk2Kphwg7Dh8lMbwu6g==
"@parcel/watcher-android-arm64@2.5.6":
version "2.5.6"
@@ -3789,7 +3796,7 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/select@~1.10.0":
"@rc-component/select@~1.10.0", "@rc-component/select@~1.10.1":
version "1.10.1"
resolved "https://registry.yarnpkg.com/@rc-component/select/-/select-1.10.1.tgz#323b2f458a637e8e752f8341094783741c613c34"
integrity sha512-H+yQsl+qED9NilQ3g6zdpsMwUgwVjrcMTkNHAWRVU/MoNCYgTbDgU+MIMgZDK+rVdd2JUfI/MkysMcZZ0cyQKw==
@@ -3824,7 +3831,7 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/table@~1.11.0":
"@rc-component/table@~1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@rc-component/table/-/table-1.11.1.tgz#7b5c2a7c26fd37b6a403082029b5a72fcb330a4d"
integrity sha512-OWdS6DMmeWb7bJBGqPxYZpQbzBlBiXZUu2sqo6Ii7Sjs9GeK1IsrXrWk26SL2c6KEseabswdxrRj7WUm9LdECw==
@@ -3866,7 +3873,7 @@
"@rc-component/util" "^1.7.0"
clsx "^2.1.1"
"@rc-component/tree-select@~1.16.0":
"@rc-component/tree-select@~1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@rc-component/tree-select/-/tree-select-1.16.1.tgz#dcaea96e396e98108cb29cc051840d4fbdda38cc"
integrity sha512-a1Oi6EJhqAhdOxxupdJi6fP0RPHMKn5TcfkX2+llaQ4lF4nwfH7b6SCHcnsybaa2s+pk1yZYwVyeOYkDnEBRdg==
@@ -4122,23 +4129,23 @@
resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b"
integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
"@storybook/addon-docs@^10.5.7":
version "10.5.7"
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.7.tgz#6d599c94fc871c248ce06a5c081f57655c83f40a"
integrity sha512-KNARJfjICaizinsR3INMEiipZm1ObYo+xw+E26gteu50Bcy2dIZUtk5uHY5XdtardU3AXX6yRXoBZ2HCY3lbHA==
"@storybook/addon-docs@^10.5.10":
version "10.5.10"
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.10.tgz#c65d1d4a6d1e2de50f0decaf82ea57583f220b30"
integrity sha512-06JoK3/a7FWI/6GzuidJP9iHp1/Vejboe6lzS1jW+d8ItpecriBt+oXh1VNmUM7i7PjI6pZnet+j51QnLyeOoQ==
dependencies:
"@mdx-js/react" "^3.0.0"
"@storybook/csf-plugin" "10.5.7"
"@storybook/csf-plugin" "10.5.10"
"@storybook/icons" "^2.0.2"
"@storybook/react-dom-shim" "10.5.7"
"@storybook/react-dom-shim" "10.5.10"
react "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
react-dom "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
ts-dedent "^2.0.0"
"@storybook/csf-plugin@10.5.7":
version "10.5.7"
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.7.tgz#bc73f164d1b5f8e2931b2774f4b389a06453cf6e"
integrity sha512-IaX8FlM0H36HNFhJ2+4L9bCldqfvHGqcLg841SJNyK/DhfMlM7JsvY/GDH2ZFuWrUf8FSOx96GRRnHq6XfRKag==
"@storybook/csf-plugin@10.5.10":
version "10.5.10"
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.10.tgz#c65da1cdfe0a11795a14c518e9f8f828b8d54b3b"
integrity sha512-TaCLBrqVEr767+w58QDotDUiCTuE5cyRJuRcDlsKQyUIyGBv+lYD3lu8wBiVCYxgIjB/gu9HmqiC+0yx1rHzaw==
dependencies:
unplugin "^2.3.5"
@@ -4152,10 +4159,10 @@
resolved "https://registry.yarnpkg.com/@storybook/icons/-/icons-2.1.0.tgz#edfc2450a39c5e780f28c6cbc49acd7bff59b41a"
integrity sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==
"@storybook/react-dom-shim@10.5.7":
version "10.5.7"
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.7.tgz#9a5aa0e0f89c09e71c6cbfc6bb1abeb537e5aabf"
integrity sha512-lxOkyh+wu/MiBXvYQHjZfD+DRKOa4bHBzbuGuiHXnHXmdOcTRdcrQTsoeN2FPtfugmmOG66cZUEgDwNX+k5eRA==
"@storybook/react-dom-shim@10.5.10":
version "10.5.10"
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.10.tgz#8ddcad879d8804a9bbc09a5d0ea10f8356f01ed6"
integrity sha512-rbu62ILo/VE3iXKmu+kWXFpD1H1Lwi0f19q/x7JnDsD2dxKS9w5znLEqPIq2qxpzi/wjjIb2iUP1cRG1d/9W5A==
"@superset-ui/core@^0.20.4":
version "0.20.4"
@@ -4855,86 +4862,86 @@
dependencies:
apg-lite "^1.0.4"
"@swc/core-darwin-arm64@1.15.47":
version "1.15.47"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.47.tgz#345ce6a1bf4033da189c2e3eff1244190195d15b"
integrity sha512-GsoMtan3ojGGMGFbl31mmRu5ctZ56re8grGE8mO/OHJ8O+JRkzod02fe7X6ZQ8JvamA3imkEkx/h3u+vsOgPgA==
"@swc/core-darwin-arm64@1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.1.tgz#f6f6983e2268888558cdbe043001d82449445def"
integrity sha512-zlJblJ8ncErD43lKdxjbUaUskJQf+LxiPXYcWXD8/8ZMV+7uuAT+CwjciLXpyZBd5Pq/S726bMpeeAwSeL1hhg==
"@swc/core-darwin-x64@1.15.47":
version "1.15.47"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.47.tgz#f3debf50b5c1602bf392acb412bd33fd6d7e4f98"
integrity sha512-leTi7Rx3KF4zcC637iqWgk9SoV8VXAD8ppQYXsep63px5A/UftOcxLN1pmr8Z1si/YvX90ompP/rHgpYkgwXWg==
"@swc/core-darwin-x64@1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.16.1.tgz#98b61e8c7ffe9f6263a08677353ba5606f6992de"
integrity sha512-IN0BmPWb0YAh/17mmlWB/HDBtTw2MfuW4hulf/tQAgTQBRH17l+z499bNJLK6LizSjqs0P7V+jU38Zj+vJC1DA==
"@swc/core-linux-arm-gnueabihf@1.15.47":
version "1.15.47"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.47.tgz#14a247a12c6d3de1ee63fa4fdbf5a4302936b5d6"
integrity sha512-hBqHuoWKKIsKmDBn9qVeWqj5GWZhtlcczVaqQmNRXsDfq+voR5CxKRfamA367QjJXtceYuliLFfEL8QsskRM2g==
"@swc/core-linux-arm-gnueabihf@1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.1.tgz#afc245521cd43a65a87cdd87fe99fb9e4f4eaa58"
integrity sha512-EYgrx2YOCQ2Twz2S793kqNjPkpvYVUPzzR95bIb7by+VQcyaai4lZZ2iz/tZvcFVKSNcN3/JTKwx+aBn2ZL52A==
"@swc/core-linux-arm64-gnu@1.15.47":
version "1.15.47"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.47.tgz#3b8d09c481ae51c7b72d98fb6ce98f7b90065a1a"
integrity sha512-TBxvRz+B4K205TWHHZxWVxkC2RFNP/Mz3PNcECBos5PsKwxjg3QSJzdoebr0VCf0Bfh8HOPldKxAP/8XkFe9gA==
"@swc/core-linux-arm64-gnu@1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.1.tgz#c44ca749af555ef8127795de141094cd28da9714"
integrity sha512-moyKm0YZlHdHohzm1YwgAyesqnE853rO0REMfJLFAova51wF9BNi+3ZW2PeS7Vqvn6HeJuepLpAHbBdZctxpHA==
"@swc/core-linux-arm64-musl@1.15.47":
version "1.15.47"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.47.tgz#7ff2baa16e67b29017fdf7c6b69e40de7920ce1a"
integrity sha512-3Yu3Uq/VgytqsPjTMbkPU1ExADytbdWbruJYhA584E9jrpE2Ki+R6VVPoZCeAVk1Cb7QxcRTgblw6bSa6a/R+w==
"@swc/core-linux-arm64-musl@1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.1.tgz#a1a3d15d5fb074c474c9a60a14488ec16124253f"
integrity sha512-kKGBO9wdapiSzuf5ZzZ2fYtlu1BNSYtIIUxvH1ir/gcelTOREEHGDCLTDFx/2Knf878nU11A40z7LxwasEFxqA==
"@swc/core-linux-ppc64-gnu@1.15.47":
version "1.15.47"
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.47.tgz#a3841982fe2eb2d889648c8e212b6d821db316d6"
integrity sha512-wfdMi5IaOaNtmh2/6geRoxIdNfqylUZFdtzTKS655y1axWfIWyx7As74vv0wVdjeCIZ3WmCI9odDd4rUttXOSQ==
"@swc/core-linux-ppc64-gnu@1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.1.tgz#7eb33976ece5e45e63f9c9c1ab0da9405df76f7f"
integrity sha512-nZ6qahtLxC3PM54cWOQZHxt4lTCF/3J4LIoWWzz6v7A+rLs8Dx54anYQf7mH3eIi8KlNpgKci/ie8ZSqFN8O7A==
"@swc/core-linux-s390x-gnu@1.15.47":
version "1.15.47"
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.47.tgz#edbfd705d6285f7dce48915871478bc9603904c3"
integrity sha512-3hHYBY0yx8Ez7GMRrkhXHQzMdR5IZA6Wq5Ee4svlgwvSECLpnAJ9+0AimEGUFDvuLwE7nV/2+PYe8+Nm4rvNcQ==
"@swc/core-linux-s390x-gnu@1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.1.tgz#f02f2687d2ee1c8f59430ef638c63714862c9389"
integrity sha512-4ji5PNzhYq193Z4/4xUaSoNJza6iCkDJSzhetrbB6KOYxsr+kxtQr8ePWhMJUiMt6JUWtXaZ1PYT8FhtED+nGA==
"@swc/core-linux-x64-gnu@1.15.47":
version "1.15.47"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.47.tgz#e7f61a7771d6a9b5b274521ba61809b3d7644325"
integrity sha512-TjfhjgP/jGCfFHYC3JQPhJA1HwErbIJ9JfREDc1KNkvY6P0LodCgKVIlQ5deeTbkG7ih3bF5PHJLuLpaZjdRyQ==
"@swc/core-linux-x64-gnu@1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.1.tgz#af4c571bbe07044ee0bec49ade1e53c1022d4979"
integrity sha512-VJQxqrisHV+B394IgrOu8YsIIXZgffnf5tO+yc9Z/hoUpuZEvuQTjWwlnpZdpyD+0nx6LTD1/3k646JYm43yJA==
"@swc/core-linux-x64-musl@1.15.47":
version "1.15.47"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.47.tgz#7c1ef8305444bcc7894de177fe225f2d8f3be609"
integrity sha512-CQpS8Ge/avfjZd0UEwG/sds83Uu32deQXcV1Jo3jD0mmvQQqtYAjpsDZXugmheeAwmt+YIuoVtVHro8LMYHqsQ==
"@swc/core-linux-x64-musl@1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.1.tgz#113eb36a1d3bd21bbf4a48a22fad97dc1c7cc91c"
integrity sha512-r9oV1mwxxsIGcLV1IQ/tw76MW3doatKze1QFWuC+a7QqJUkhY/bKTSVk6NpKKUGm2LDsE33Va8VqSClfA7vSiQ==
"@swc/core-win32-arm64-msvc@1.15.47":
version "1.15.47"
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.47.tgz#953856d26b28956d1a18ef10e5f221202b2cb8f1"
integrity sha512-0W8IKHsUTYiT7G2RqtOoVWk+89yzZikIiDUb/sCK6BmQDBhN91hQSfyUtW12jhEWLzYgcfmisfsZrmZE+84U1A==
"@swc/core-win32-arm64-msvc@1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.1.tgz#7cc6cfde26ad7e15fe93de98033e7c1892bcf127"
integrity sha512-6huNRessoBLxWEqBm5zJXyCQ27TO7anvkdiuQ5MDO4CJni0nOXEqKtV9RllQ2TdyENKKsUMXVnIfW2hIXx/R5Q==
"@swc/core-win32-ia32-msvc@1.15.47":
version "1.15.47"
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.47.tgz#2743a5bccc49f252c23bad3135193640cbdcef3a"
integrity sha512-ZIp49d2Z4/ka2jO9otOg4hDvTdPmp86kVOgS2M5FCPI7eKKZ1W0boxWn+8XeZrfERtFGW0AlMRm4JhlJa7l3NA==
"@swc/core-win32-ia32-msvc@1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.1.tgz#2330c734f4129c2064b8848fb956501788aab9a3"
integrity sha512-OVKJFUzphrGmsh+BGtcZDesx0YryV7/Yvy5XGgTqnrZfjnyfcr5uaqYQugCckdIlupc5Vs3XtDjRAj12z4ZPlw==
"@swc/core-win32-x64-msvc@1.15.47":
version "1.15.47"
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.47.tgz#9674ad0c9187b7cbe5cc3080b31b960d3ee688b9"
integrity sha512-2h8Iek95vnixkBRCo+H8p09+Q5ll2NgSMFrWTy0iKt7+/t+8/T5mBpiT6c0ZxSS7wcWjwZ9sGZkK70tTSYHdDw==
"@swc/core-win32-x64-msvc@1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.1.tgz#04825a3f9e6fbe390825ff02708a5ebdd3a9841b"
integrity sha512-Bt+VIhWYCGk4urklnkkteLUOeLv1VxigwTCeB/xC6rBZxY6IIKdDwCJf6on3E3SUGsIqmQS6QqtuJQc1VxF4Aw==
"@swc/core@^1.15.40", "@swc/core@^1.15.47":
version "1.15.47"
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.47.tgz#6226e842160e247eb79a9aeac1095ebddb56639f"
integrity sha512-FbsO5JcfOjfH38W/rohBRBweJeERsAuIP4f377lmkmxTcq9exjtx4SkRuZY5CdfhR2CBVwDIJegBpJDffwNsOg==
"@swc/core@^1.15.40", "@swc/core@^1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.16.1.tgz#5ea7ff32f3b352c871aa47195efd4932f709a569"
integrity sha512-nUaeu91O5QZKrQdaDCHd402ogUIoNOOjpkZNq0UomWK0G6gDaGmLhvddF1/3BXf5O8aLyo6ZPY/aMDWvaJQ/hg==
dependencies:
"@swc/counter" "^0.1.3"
"@swc/types" "^0.1.27"
"@swc/types" "^0.1.28"
optionalDependencies:
"@swc/core-darwin-arm64" "1.15.47"
"@swc/core-darwin-x64" "1.15.47"
"@swc/core-linux-arm-gnueabihf" "1.15.47"
"@swc/core-linux-arm64-gnu" "1.15.47"
"@swc/core-linux-arm64-musl" "1.15.47"
"@swc/core-linux-ppc64-gnu" "1.15.47"
"@swc/core-linux-s390x-gnu" "1.15.47"
"@swc/core-linux-x64-gnu" "1.15.47"
"@swc/core-linux-x64-musl" "1.15.47"
"@swc/core-win32-arm64-msvc" "1.15.47"
"@swc/core-win32-ia32-msvc" "1.15.47"
"@swc/core-win32-x64-msvc" "1.15.47"
"@swc/core-darwin-arm64" "1.16.1"
"@swc/core-darwin-x64" "1.16.1"
"@swc/core-linux-arm-gnueabihf" "1.16.1"
"@swc/core-linux-arm64-gnu" "1.16.1"
"@swc/core-linux-arm64-musl" "1.16.1"
"@swc/core-linux-ppc64-gnu" "1.16.1"
"@swc/core-linux-s390x-gnu" "1.16.1"
"@swc/core-linux-x64-gnu" "1.16.1"
"@swc/core-linux-x64-musl" "1.16.1"
"@swc/core-win32-arm64-msvc" "1.16.1"
"@swc/core-win32-ia32-msvc" "1.16.1"
"@swc/core-win32-x64-msvc" "1.16.1"
"@swc/counter@^0.1.3":
version "0.1.3"
@@ -5021,10 +5028,10 @@
"@swc/html-win32-ia32-msvc" "1.15.43"
"@swc/html-win32-x64-msvc" "1.15.43"
"@swc/types@^0.1.27":
version "0.1.27"
resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.27.tgz#12080b0c426dea450634f202d9a3c82ac396e793"
integrity sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==
"@swc/types@^0.1.28":
version "0.1.28"
resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.28.tgz#e3cd892383fba3b8904c40518bbe1265a50753f2"
integrity sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==
dependencies:
"@swc/counter" "^0.1.3"
@@ -6181,10 +6188,10 @@ ansis@^3.2.0:
resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.17.0.tgz#fa8d9c2a93fe7d1177e0c17f9eeb562a58a832d7"
integrity sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==
antd@^6.6.0:
version "6.6.0"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.6.0.tgz#8acb84c54b36594b5c1a9084c8acb6a03b79961b"
integrity sha512-UDwWIbpmrCHB9ZQ+bPh4vQfB6DTI2ulIyoQ0Tc9xxalFblttiNGHl3ySBD9SyV/8+gUjFzfSx1+iU1Fog2i46w==
antd@^6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.6.1.tgz#3235d76413b525b1f3287b87bdaf6ba0e7148521"
integrity sha512-QHIHYoUk9N9nJy1T9fyxWKjY0qApdTEDd/6lzqYng8Uryv9FejNmbhKvYF7obGqB+TuLXQsPVF7fOVgyzM1KrQ==
dependencies:
"@ant-design/colors" "^8.0.1"
"@ant-design/cssinjs" "^2.1.2"
@@ -6217,16 +6224,16 @@ antd@^6.6.0:
"@rc-component/rate" "~1.0.1"
"@rc-component/resize-observer" "^1.1.2"
"@rc-component/segmented" "~1.3.0"
"@rc-component/select" "~1.10.0"
"@rc-component/select" "~1.10.1"
"@rc-component/slider" "~1.1.1"
"@rc-component/steps" "~1.2.2"
"@rc-component/switch" "~1.0.3"
"@rc-component/table" "~1.11.0"
"@rc-component/table" "~1.11.1"
"@rc-component/tabs" "~1.12.0"
"@rc-component/tooltip" "~1.5.0"
"@rc-component/tour" "~2.4.0"
"@rc-component/tree" "~1.4.0"
"@rc-component/tree-select" "~1.16.0"
"@rc-component/tree-select" "~1.16.1"
"@rc-component/trigger" "^3.10.1"
"@rc-component/upload" "~1.1.1"
"@rc-component/util" "^1.12.0"
@@ -6522,10 +6529,10 @@ base64-js@^1.3.1, base64-js@^1.5.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
baseline-browser-mapping@^2.10.38, baseline-browser-mapping@^2.11.13, baseline-browser-mapping@^2.9.19:
version "2.11.13"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz#660073103c1bee93e54df55f117b7528adf6af19"
integrity sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==
baseline-browser-mapping@^2.10.38, baseline-browser-mapping@^2.11.16, baseline-browser-mapping@^2.9.19:
version "2.11.16"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.16.tgz#0fa19a4ece2e34439ecaa3fdca8a59acfbd287fb"
integrity sha512-H/bNPUFHewJHyCTdjn1n3Pit5+2GmWT6mmeHImPX+8MA9NA6b67jO4gYmi4jTbCJb2otq34KMZnovndDPqJwhQ==
batch@0.6.1:
version "0.6.1"
@@ -9174,10 +9181,10 @@ globals@^14.0.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
globals@^17.10.0:
version "17.10.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-17.10.0.tgz#f9dbd847ae99e236f98b13095e2426ac3b25a45c"
integrity sha512-V0kztuWST2k8A/VbxAY8+L+7+Rgo3fyA24IHRLrZp7HOzJjV0gHSaZUjK9lpP/IrBSNite2tZ1prhRkinRu1CA==
globals@^17.11.0:
version "17.11.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-17.11.0.tgz#d643485bb30220d7751e511cf4f68c73d3870d87"
integrity sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==
globalthis@^1.0.4:
version "1.0.4"
@@ -10291,10 +10298,10 @@ js-yaml@4.1.0, js-yaml@=4.3.1, js-yaml@^4.1.0, js-yaml@^4.1.1, js-yaml@^4.2.0, j
dependencies:
argparse "^2.0.1"
js-yaml@^5.2.3:
version "5.2.3"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-5.2.3.tgz#0942ae8f507e22eb0e54624871789cd477106e54"
integrity sha512-n+mUVyUX5bVv7G/G2zyIHOhdxfuU1dY2NOFzTQUWiMUbFss8b57NFlgCCaggU78wSw5KVS9cllzeLyzyR+n5nw==
js-yaml@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-5.3.0.tgz#526430a6da31065127528ae695ce168cfc5f91f0"
integrity sha512-muutsYr+e2+d3rTgUGslq5rxbBlUy3cJ61IsHag2QNDQV+7zXWjkUpmALIajhrlLlrgRUiymj6U3zUr/TMK84Q==
dependencies:
argparse "^2.0.1"
@@ -11849,10 +11856,10 @@ neotraverse@0.6.15:
resolved "https://registry.yarnpkg.com/neotraverse/-/neotraverse-0.6.15.tgz#dc4abb64700c52440f13bc53635b559862420360"
integrity sha512-HZpdkco+JeXq0G+WWpMJ4NsX3pqb5O7eR9uGz3FfoFt+LYzU8iRWp49nJtud6hsDoywM8tIrDo3gjgmOqJA8LA==
neotraverse@=0.6.18:
version "0.6.18"
resolved "https://registry.yarnpkg.com/neotraverse/-/neotraverse-0.6.18.tgz#abcb33dda2e8e713cf6321b29405e822230cdb30"
integrity sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==
neotraverse@=1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/neotraverse/-/neotraverse-1.0.1.tgz#7c89b43f6504ef85928c718f578c68621576d194"
integrity sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==
no-case@^3.0.4:
version "3.0.4"
@@ -12237,57 +12244,57 @@ oxc-parser@^0.127.0:
"@oxc-parser/binding-win32-ia32-msvc" "0.127.0"
"@oxc-parser/binding-win32-x64-msvc" "0.127.0"
oxc-resolver@^11.19.1:
version "11.23.0"
resolved "https://registry.yarnpkg.com/oxc-resolver/-/oxc-resolver-11.23.0.tgz#bb9e32fa028dbde0584b5964dbc59de2878ac171"
integrity sha512-f0+l598CJMOLnYPXsXxttJALH0ljtivdRMKtvHhxRuWa5FYmw5+qODARl8oYjMC/brpzKcrpdORsOBrTqhBZ9A==
oxc-resolver@11.21.2:
version "11.21.2"
resolved "https://registry.yarnpkg.com/oxc-resolver/-/oxc-resolver-11.21.2.tgz#06f49557c98adb97133d85362797c460ef028057"
integrity sha512-w5tLwYN3Zo24w5EeWJjJWZOwhYqTtC8PS2B1tIt7BZUuqTIcU07sQValbDw+rq7+AuAGzOHklgK+ifsy4lpXfw==
optionalDependencies:
"@oxc-resolver/binding-android-arm-eabi" "11.23.0"
"@oxc-resolver/binding-android-arm64" "11.23.0"
"@oxc-resolver/binding-darwin-arm64" "11.23.0"
"@oxc-resolver/binding-darwin-x64" "11.23.0"
"@oxc-resolver/binding-freebsd-x64" "11.23.0"
"@oxc-resolver/binding-linux-arm-gnueabihf" "11.23.0"
"@oxc-resolver/binding-linux-arm-musleabihf" "11.23.0"
"@oxc-resolver/binding-linux-arm64-gnu" "11.23.0"
"@oxc-resolver/binding-linux-arm64-musl" "11.23.0"
"@oxc-resolver/binding-linux-ppc64-gnu" "11.23.0"
"@oxc-resolver/binding-linux-riscv64-gnu" "11.23.0"
"@oxc-resolver/binding-linux-riscv64-musl" "11.23.0"
"@oxc-resolver/binding-linux-s390x-gnu" "11.23.0"
"@oxc-resolver/binding-linux-x64-gnu" "11.23.0"
"@oxc-resolver/binding-linux-x64-musl" "11.23.0"
"@oxc-resolver/binding-openharmony-arm64" "11.23.0"
"@oxc-resolver/binding-wasm32-wasi" "11.23.0"
"@oxc-resolver/binding-win32-arm64-msvc" "11.23.0"
"@oxc-resolver/binding-win32-x64-msvc" "11.23.0"
"@oxc-resolver/binding-android-arm-eabi" "11.21.2"
"@oxc-resolver/binding-android-arm64" "11.21.2"
"@oxc-resolver/binding-darwin-arm64" "11.21.2"
"@oxc-resolver/binding-darwin-x64" "11.21.2"
"@oxc-resolver/binding-freebsd-x64" "11.21.2"
"@oxc-resolver/binding-linux-arm-gnueabihf" "11.21.2"
"@oxc-resolver/binding-linux-arm-musleabihf" "11.21.2"
"@oxc-resolver/binding-linux-arm64-gnu" "11.21.2"
"@oxc-resolver/binding-linux-arm64-musl" "11.21.2"
"@oxc-resolver/binding-linux-ppc64-gnu" "11.21.2"
"@oxc-resolver/binding-linux-riscv64-gnu" "11.21.2"
"@oxc-resolver/binding-linux-riscv64-musl" "11.21.2"
"@oxc-resolver/binding-linux-s390x-gnu" "11.21.2"
"@oxc-resolver/binding-linux-x64-gnu" "11.21.2"
"@oxc-resolver/binding-linux-x64-musl" "11.21.2"
"@oxc-resolver/binding-openharmony-arm64" "11.21.2"
"@oxc-resolver/binding-wasm32-wasi" "11.21.2"
"@oxc-resolver/binding-win32-arm64-msvc" "11.21.2"
"@oxc-resolver/binding-win32-x64-msvc" "11.21.2"
oxfmt@^0.63.0:
version "0.63.0"
resolved "https://registry.yarnpkg.com/oxfmt/-/oxfmt-0.63.0.tgz#c7338e6c43a68d5cf8dc61c08b617d77cb54e323"
integrity sha512-kgdDwv35wvVf6554U2Ab8Jnd0zTM+TsEQWwaB70RAjK3gICFAFGO+2Hd3Be27GMoXj3XRL9IKSNRVl7KBQL6iw==
oxfmt@^0.64.0:
version "0.64.0"
resolved "https://registry.yarnpkg.com/oxfmt/-/oxfmt-0.64.0.tgz#666a5148cdf7385007cd46e35e8ff8f94ecfd96b"
integrity sha512-XZ4GFBN/PLbXKq+0zrgpQfPKYuJlUuj+nzZJY7UpIbFMNyefNLCdN9EwViycNqnYcv0wrn0jXcQLlqJp8RCKBg==
dependencies:
tinypool "2.1.0"
optionalDependencies:
"@oxfmt/binding-android-arm-eabi" "0.63.0"
"@oxfmt/binding-android-arm64" "0.63.0"
"@oxfmt/binding-darwin-arm64" "0.63.0"
"@oxfmt/binding-darwin-x64" "0.63.0"
"@oxfmt/binding-freebsd-x64" "0.63.0"
"@oxfmt/binding-linux-arm-gnueabihf" "0.63.0"
"@oxfmt/binding-linux-arm-musleabihf" "0.63.0"
"@oxfmt/binding-linux-arm64-gnu" "0.63.0"
"@oxfmt/binding-linux-arm64-musl" "0.63.0"
"@oxfmt/binding-linux-ppc64-gnu" "0.63.0"
"@oxfmt/binding-linux-riscv64-gnu" "0.63.0"
"@oxfmt/binding-linux-riscv64-musl" "0.63.0"
"@oxfmt/binding-linux-s390x-gnu" "0.63.0"
"@oxfmt/binding-linux-x64-gnu" "0.63.0"
"@oxfmt/binding-linux-x64-musl" "0.63.0"
"@oxfmt/binding-openharmony-arm64" "0.63.0"
"@oxfmt/binding-win32-arm64-msvc" "0.63.0"
"@oxfmt/binding-win32-ia32-msvc" "0.63.0"
"@oxfmt/binding-win32-x64-msvc" "0.63.0"
"@oxfmt/binding-android-arm-eabi" "0.64.0"
"@oxfmt/binding-android-arm64" "0.64.0"
"@oxfmt/binding-darwin-arm64" "0.64.0"
"@oxfmt/binding-darwin-x64" "0.64.0"
"@oxfmt/binding-freebsd-x64" "0.64.0"
"@oxfmt/binding-linux-arm-gnueabihf" "0.64.0"
"@oxfmt/binding-linux-arm-musleabihf" "0.64.0"
"@oxfmt/binding-linux-arm64-gnu" "0.64.0"
"@oxfmt/binding-linux-arm64-musl" "0.64.0"
"@oxfmt/binding-linux-ppc64-gnu" "0.64.0"
"@oxfmt/binding-linux-riscv64-gnu" "0.64.0"
"@oxfmt/binding-linux-riscv64-musl" "0.64.0"
"@oxfmt/binding-linux-s390x-gnu" "0.64.0"
"@oxfmt/binding-linux-x64-gnu" "0.64.0"
"@oxfmt/binding-linux-x64-musl" "0.64.0"
"@oxfmt/binding-openharmony-arm64" "0.64.0"
"@oxfmt/binding-win32-arm64-msvc" "0.64.0"
"@oxfmt/binding-win32-ia32-msvc" "0.64.0"
"@oxfmt/binding-win32-x64-msvc" "0.64.0"
p-cancelable@^3.0.0:
version "3.0.0"
@@ -13583,7 +13590,7 @@ react-modal@^3.16.3:
react-lifecycles-compat "^3.0.0"
warning "^4.0.3"
react-redux@^9.2.0:
react-redux@^9.2.0, react-redux@^9.3.0:
version "9.3.0"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.3.0.tgz#a30113bb6d95c0a715d54dda4308d450fca6ce09"
integrity sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==
@@ -14783,10 +14790,10 @@ stop-iteration-iterator@^1.1.0:
es-errors "^1.3.0"
internal-slot "^1.1.0"
storybook@^10.5.7:
version "10.5.7"
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.7.tgz#adfc465e51f337291c095278c23f1b8024ef2da7"
integrity sha512-oiKvWIwIoOhFP1i6dASYyMXwPHKEtVZMshqSB7EvIVYjWRh0l9H7gHEt1z4Gh2rLGFMekWdsm4s94rvwpR7gkg==
storybook@^10.5.10:
version "10.5.10"
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.10.tgz#97e9b4a71b4df7732e82d64edffe07f9e8d70083"
integrity sha512-Rz8k9ejFHsi7lbtJTaxZlhCUz4GkbJIKEoKDjXeLfr/ZhXip73E6keKxW0KH8iGeKiCqHAbJCV4YIQrxTOLiig==
dependencies:
"@storybook/global" "^5.0.0"
"@storybook/icons" "^2.0.2"
@@ -14800,7 +14807,7 @@ storybook@^10.5.7:
jsonc-parser "^3.3.1"
open "^10.2.0"
oxc-parser "^0.127.0"
oxc-resolver "^11.19.1"
oxc-resolver "11.21.2"
recast "^0.23.5"
semver "^7.7.3"
use-sync-external-store "^1.5.0"
@@ -15057,10 +15064,10 @@ svgo@^3.0.2, svgo@^3.2.0:
picocolors "^1.0.0"
sax "^1.5.0"
swagger-client@^3.37.8:
version "3.37.8"
resolved "https://registry.yarnpkg.com/swagger-client/-/swagger-client-3.37.8.tgz#26c24c89cbfda7459f6afb53bdfcb6d8dbe9ac82"
integrity sha512-uoKwfq+8DvWVDhoALDrEtex9f26Yi2VkvEFjsrMHd8Gl+TcApJkVXtNiE35p5JQjMsvwkvr1eLVlOFNF4GL1bQ==
swagger-client@^3.38.0:
version "3.38.0"
resolved "https://registry.yarnpkg.com/swagger-client/-/swagger-client-3.38.0.tgz#542431f02d809b49115272ff8b9e48d545b9f53c"
integrity sha512-n7aykm1BEdQ3fKePJJx63UGjYe8/5fuxFMi3qZP4OJGZvzljKvmhxNwIF/MB71sF/lop9NeWZReKvPib9CY+2g==
dependencies:
"@babel/runtime-corejs3" "^7.22.15"
"@scarf/scarf" "=1.4.0"
@@ -15074,7 +15081,7 @@ swagger-client@^3.37.8:
deepmerge "~4.3.0"
fast-json-patch "^3.0.0-1"
js-yaml "^4.2.0"
neotraverse "=0.6.18"
neotraverse "=1.0.1"
node-abort-controller "^3.1.1"
openapi-path-templating "^2.2.1"
openapi-server-url-templating "^1.3.0"
@@ -15103,10 +15110,10 @@ swagger-client@^3.37.8:
"@swagger-api/apidom-parser-adapter-openapi-yaml-3-2" "^1.12.0"
"@swagger-api/apidom-parser-adapter-yaml-1-2" "^1.12.0"
swagger-ui-react@^5.32.13:
version "5.32.13"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.32.13.tgz#04c96140b0a2d4ea01ebec4d4cfc655d5ed9a500"
integrity sha512-XIDl+Ny6kE1N8wpSPiOFrjPfAevs4GR4XmV6BT6NLMikkMFIbIVocWbA8pnKYyYXQe8Rccfli5o2zDfySw0FnQ==
swagger-ui-react@^5.32.14:
version "5.32.14"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.32.14.tgz#31b69b0f6910e87dbcc81886208061ca1f72e034"
integrity sha512-6LAVBeC78DplbJ7kutm/YeBYo22nPzGOca4bIZAvQG4w2eSetnYDdazaUfY0qzQUlg/H90HnYZX3rg67EmENOw==
dependencies:
"@babel/runtime-corejs3" "^7.27.1"
"@scarf/scarf" "=1.4.0"
@@ -15129,7 +15136,7 @@ swagger-ui-react@^5.32.13:
react-immutable-proptypes "2.2.0"
react-immutable-pure-component "^2.2.0"
react-inspector "^6.0.1"
react-redux "^9.2.0"
react-redux "^9.3.0"
react-syntax-highlighter "^16.0.0"
redux "^5.0.1"
redux-immutable "^4.0.0"
@@ -15137,7 +15144,7 @@ swagger-ui-react@^5.32.13:
reselect "^5.1.1"
serialize-error "^8.1.0"
sha.js "^2.4.12"
swagger-client "^3.37.8"
swagger-client "^3.38.0"
url-parse "^1.5.10"
xml "=1.0.1"
xml-but-prettier "^1.0.1"
+32 -17
View File
@@ -94,16 +94,16 @@ dependencies = [
"parsedatetime",
"paramiko>=3.4.0, <4.0", # 4.0 removed DSSKey, still referenced by sshtunnel
"pgsanity",
"Pillow>=11.0.0, <13",
"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.8.0",
"pyparsing>=3.3.2, <4",
"python-dateutil",
"python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies
"pygeohash",
"pyarrow>=24.0.0, <26", # before upgrading pyarrow, check that all db dependencies support this, see e.g. https://github.com/apache/superset/pull/34693
"pyarrow>=25.0.1, <26", # before upgrading pyarrow, check that all db dependencies support this, see e.g. https://github.com/apache/superset/pull/34693
"pyyaml>=6.0.3, <7.0.0",
"PyJWT>=2.4.0, <3.0",
"PyJWT>=2.13.0, <3.0", # raise floor to match resolved pin; closes SCA false-positive on 2.4.x-range CVEs already fixed in 2.13.0
"redis>=5.0.0, <9.0",
"rison>=2.0.1, <3.0",
@@ -111,10 +111,10 @@ dependencies = [
"sshtunnel>=0.4.0, <0.5",
"simplejson>=4.1.1",
"slack_sdk>=3.43.0, <4",
"sqlalchemy>=2.0.0, <2.1",
"sqlalchemy>=2.0.52, <2.1",
"sqlalchemy-continuum>=1.6.0, <2.0.0",
"sqlalchemy-utils>=0.42.1, <0.43", # expanding lowerbound to work with pydoris
"sqlglot>=30.16.0, <31", # 30.16.0 adds Trino inline UDF IF/CASE routine statement parsing
"sqlglot>=30.17.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",
@@ -141,8 +141,18 @@ bigquery = [
"sqlalchemy-bigquery>=1.17.2",
"google-cloud-bigquery>=3.42.3",
]
clickhouse = ["clickhouse-connect>=1.6.0, <2.0"]
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
clickhouse = ["clickhouse-connect>=1.7.2, <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
# 2.0). sqlalchemy-cockroachdb is the actively maintained replacement,
# already linked from CockroachDbEngineSpec.metadata's docs_url, and
# registers the same `cockroachdb` SQLAlchemy dialect entry point.
# sqlalchemy-cockroachdb depends only on SQLAlchemy itself, not on a DBAPI
# driver, so psycopg2-binary is pinned alongside it (matching the `postgres`
# extra) to keep this extra self-contained -- CockroachDB speaks the
# PostgreSQL wire protocol, so psycopg2 is what actually opens connections.
cockroachdb = ["sqlalchemy-cockroachdb>=2.0.0, <3", "psycopg2-binary==2.9.12"]
crate = ["sqlalchemy-cratedb>=0.43.1, <1"]
# sqlalchemy-d1's only release (0.1.0, Nov 2025) pins sqlalchemy<2,>=1.4,
# explicitly excluding SQLAlchemy 2.0. See superset/db_engine_specs/d1.py's
@@ -185,11 +195,12 @@ excel = ["xlrd>=2.0.2, <2.1"]
# installing this extra is only required to actually run exports.
excel-export = ["boto3"]
fastmcp = [
"fastmcp>=3.4.6,<4.0",
"fastmcp>=3.4.7,<4.0",
"mcp>=1.29.1,<3.0",
# tiktoken backs the response-size-guard token estimator. Without
# it, the middleware falls back to a coarser character-based
# heuristic that under-counts JSON-heavy MCP responses.
"tiktoken>=0.13.0,<1.0",
"tiktoken>=0.14.0,<1.0",
]
# sqlalchemy-firebird >=2.0.0 unconditionally requires SQLAlchemy 2.0 on
# Python >=3.8 (which covers Superset's >=3.11 floor), with no dual-compat
@@ -197,7 +208,7 @@ fastmcp = [
# landed (discussion #40273).
firebird = ["sqlalchemy-firebird>=2.2.0"]
firebolt = ["firebolt-sqlalchemy>=1.1.2, <2"]
gevent = ["gevent>=26.7.0"]
gevent = ["gevent>=26.8.0"]
gsheets = ["shillelagh[gsheetsapi]>=1.4.5, <2"]
hana = ["hdbcli==2.29.25", "sqlalchemy_hana==3.0.3"]
hive = [
@@ -218,8 +229,12 @@ motherduck = ["apache-superset[duckdb]"]
mysql = ["mysqlclient>=2.2.8, <3"]
ocient = [
# Closed-source vendor package with no public changelog; permissive
# unpinned sqlalchemy>=1.4 declared, but SQLAlchemy 2.0 support is
# unverified. Lower confidence than the other bumps in this PR.
# unpinned sqlalchemy>=1.4 declared. Verified compatible with SQLAlchemy
# 2.0 against pyocient>=3.9.0 (discussion #40273): dialect construction,
# error extraction, and GIS-type sanitization all pass under 2.0.52. Note
# pyocient 3.9.0 relocated its geo-type classes from private top-level
# names (pyocient._STPoint) to public ones under pyocient.api
# (pyocient.api.STPoint), which is unrelated to the SQLAlchemy bump.
"sqlalchemy-ocient>=3.0.0, <4",
"pyocient>=3.9.0, <4",
"shapely",
@@ -231,8 +246,8 @@ pinot = ["pinotdb>=5.0.0, <10.0.0"]
playwright = ["playwright>=1.62.0, <2"]
postgres = ["psycopg2-binary==2.9.12"]
presto = ["pyhive[presto]>=0.6.5"]
trino = ["trino>=0.338.0"]
prophet = ["prophet>=1.3.0, <2"]
trino = ["trino>=0.339.0"]
prophet = ["prophet>=1.4.0, <2"]
# sqlalchemy-redshift cuts hard from SQLAlchemy 1.4-only (0.8.x) to 2.0-only
# (>=1.0.0) with no dual-compat release. Bumped now that Superset's own
# SQLAlchemy 2.0 core bump has landed (discussion #40273).
@@ -245,7 +260,7 @@ risingwave = ["sqlalchemy-risingwave>=2.0.0"]
shillelagh = ["shillelagh[all]>=1.4.5, <2"]
singlestore = ["sqlalchemy-singlestoredb>=1.2.1, <2"]
snowflake = ["snowflake-sqlalchemy>=1.11.0, <2"]
sqlite = ["syntaqlite>=0.7.0,<0.8.0"]
sqlite = ["syntaqlite>=0.9.0,<0.10.0"]
spark = [
"pyhive[hive_pure_sasl]>=0.7",
"tableschema",
@@ -255,7 +270,7 @@ tdengine = [
"taospy>=2.8.10",
"taos-ws-py>=0.7.0"
]
teradata = ["teradatasql>=20.0.0.64"]
teradata = ["teradatasql>=20.0.0.66"]
thumbnails = [] # deprecated, will be removed in 7.0
vertica = ["sqlalchemy-vertica-python>= 0.6.3, < 0.7"]
netezza = ["nzalchemy>= 11.1.2, < 11.2"]
@@ -289,7 +304,7 @@ development = [
"ruff",
"sqloxide",
"statsd",
"syntaqlite>=0.7.0,<0.8.0",
"syntaqlite>=0.9.0,<0.10.0",
]
[project.urls]
+1 -1
View File
@@ -30,7 +30,7 @@ cryptography>=50.0.0,<51.0.0
# Security: Snyk - XSS vulnerability in Mako templates
mako>=1.4.1,<2.0.0
# Security: CVE-2024-52338 (CRITICAL) - Deserialization of untrusted data in IPC/Parquet readers
pyarrow>=24.0.0,<26.0.0
pyarrow>=25.0.1,<26.0.0
# Security: CVE-2026-27459 - pyopenssl certificate validation
pyopenssl>=26.0.0,<27.0.0
# Security: CVE-2026-25645 (MEDIUM) - Insecure Temporary File
+6 -7
View File
@@ -168,11 +168,11 @@ greenlet==3.5.4
# apache-superset (pyproject.toml)
# shillelagh
# sqlalchemy
gunicorn==26.0.0
gunicorn==26.2.0
# via apache-superset (pyproject.toml)
hashids==1.3.1
# via apache-superset (pyproject.toml)
holidays==0.102
holidays==0.103
# via apache-superset (pyproject.toml)
humanize==4.12.3
# via apache-superset (pyproject.toml)
@@ -222,7 +222,7 @@ markupsafe==3.0.2
# mako
# werkzeug
# wtforms
marshmallow==4.3.0
marshmallow==4.3.1
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
@@ -263,7 +263,6 @@ packaging==25.0
# apache-superset (pyproject.toml)
# apispec
# deprecation
# gunicorn
# kombu
# limits
# shillelagh
@@ -287,7 +286,7 @@ prison==0.2.1
# via flask-appbuilder
prompt-toolkit==3.0.51
# via click-repl
pyarrow==25.0.0
pyarrow==25.0.1
# via
# -r requirements/base.in
# apache-superset (pyproject.toml)
@@ -381,7 +380,7 @@ six==1.17.0
# wtforms-json
slack-sdk==3.43.0
# via apache-superset (pyproject.toml)
sqlalchemy==2.0.51
sqlalchemy==2.0.52
# via
# apache-superset (pyproject.toml)
# alembic
@@ -399,7 +398,7 @@ sqlalchemy-utils==0.42.1
# apache-superset (pyproject.toml)
# apache-superset-core
# flask-appbuilder
sqlglot==30.16.0
sqlglot==30.17.0
# via
# apache-superset (pyproject.toml)
# apache-superset-core
+1 -1
View File
@@ -16,5 +16,5 @@
# specific language governing permissions and limitations
# under the License.
#
-e .[development,bigquery,druid,duckdb,fastmcp,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]
-e .[development,bigquery,cockroachdb,druid,duckdb,fastmcp,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]
-e ./superset-extensions-cli[test]
+18 -16
View File
@@ -337,7 +337,7 @@ geopy==2.4.1
# via
# -c requirements/base-constraint.txt
# apache-superset
gevent==26.7.0
gevent==26.8.0
# via apache-superset
google-api-core==2.33.0
# via
@@ -391,7 +391,7 @@ grpcio==1.83.0
# grpcio-status
grpcio-status==1.60.1
# via google-api-core
gunicorn==26.0.0
gunicorn==26.2.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -403,7 +403,7 @@ hashids==1.3.1
# via
# -c requirements/base-constraint.txt
# apache-superset
holidays==0.102
holidays==0.103
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -434,8 +434,6 @@ importlib-metadata==8.7.0
# via
# keyring
# opentelemetry-api
importlib-resources==6.5.2
# via prophet
iniconfig==2.0.0
# via pytest
isodate==0.7.2
@@ -530,7 +528,7 @@ markupsafe==3.0.2
# mako
# werkzeug
# wtforms
marshmallow==4.3.0
marshmallow==4.3.1
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -549,8 +547,10 @@ matplotlib==3.9.0
# via prophet
mccabe==0.7.0
# via pylint
mcp==1.24.0
# via fastmcp-slim
mcp==1.29.1
# via
# apache-superset
# fastmcp-slim
mdurl==0.1.2
# via
# -c requirements/base-constraint.txt
@@ -624,7 +624,6 @@ packaging==25.0
# duckdb-engine
# fastmcp-slim
# google-cloud-bigquery
# gunicorn
# kombu
# limits
# matplotlib
@@ -693,7 +692,7 @@ prompt-toolkit==3.0.51
# via
# -c requirements/base-constraint.txt
# click-repl
prophet==1.3.0
prophet==1.4.0
# via apache-superset
proto-plus==1.25.0
# via google-api-core
@@ -711,7 +710,7 @@ psycopg2-binary==2.9.12
# via apache-superset
py-key-value-aio==0.4.4
# via fastmcp-slim
pyarrow==25.0.0
pyarrow==25.0.1
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -950,7 +949,7 @@ slack-sdk==3.43.0
# apache-superset
sniffio==1.3.1
# via anyio
sqlalchemy==2.0.51
sqlalchemy==2.0.52
# via
# -c requirements/base-constraint.txt
# alembic
@@ -962,10 +961,13 @@ sqlalchemy==2.0.51
# marshmallow-sqlalchemy
# shillelagh
# sqlalchemy-bigquery
# sqlalchemy-cockroachdb
# sqlalchemy-continuum
# sqlalchemy-utils
sqlalchemy-bigquery==1.17.2
# via apache-superset
sqlalchemy-cockroachdb==2.0.4
# via apache-superset
sqlalchemy-continuum==1.7.0
# via
# -c requirements/base-constraint.txt
@@ -976,7 +978,7 @@ sqlalchemy-utils==0.42.1
# apache-superset
# apache-superset-core
# flask-appbuilder
sqlglot==30.16.0
sqlglot==30.17.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -995,13 +997,13 @@ starlette==1.3.1
# mcp
statsd==4.0.1
# via apache-superset
syntaqlite==0.7.1
syntaqlite==0.9.0
# via apache-superset
tabulate==0.10.0
# via
# -c requirements/base-constraint.txt
# apache-superset
tiktoken==0.13.0
tiktoken==0.14.0
# via apache-superset
tomli-w==1.2.0
# via apache-superset-extensions-cli
@@ -1011,7 +1013,7 @@ tqdm==4.67.1
# via
# cmdstanpy
# prophet
trino==0.338.0
trino==0.339.0
# via apache-superset
typing-extensions==4.16.0
# via
+1 -1
View File
@@ -93,7 +93,7 @@ def find_models(module: ModuleType) -> list[type[Model]]: # noqa: C901
# where the current model is out-of-sync with the existing table after a
# downgrade
sqlalchemy_uri = current_app.config["SQLALCHEMY_DATABASE_URI"]
engine = create_engine(sqlalchemy_uri, future=True)
engine = create_engine(sqlalchemy_uri)
Base = automap_base() # noqa: N806
Base.prepare(engine, reflect=True)
seen = set()
+1
View File
@@ -42,6 +42,7 @@ RETRYABLE_STATUS_CODES: frozenset[int] = frozenset({429})
PATTERNS = {
"python": [
r"^\.github/workflows/.*python",
r"^\.github/workflows/frontend-bundle-size-nightly\.yml$",
r"^\.github/workflows/scheduled-docker-image-refresh\.yml$",
r"^docker-compose-image-tag\.yml$",
r"^tests/",
@@ -14,3 +14,5 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Semantic layer contracts for extension authors."""
@@ -21,6 +21,7 @@ from abc import ABC, abstractmethod
from typing import Any, Generic, TypeVar
from pydantic import BaseModel
from superset_core.semantic_layers.view import SemanticView
ConfigT = TypeVar("ConfigT", bound=BaseModel)
+1
View File
@@ -0,0 +1 @@
../superset-frontend/.npmrc
+1
View File
@@ -0,0 +1 @@
min-release-age=3
+1 -1
View File
@@ -113,7 +113,7 @@
// === Import plugin rules ===
"import/named": "error",
"import/export": "error",
"import/no-named-as-default": "error",
"import/no-named-as-default": "warn",
"import/no-named-as-default-member": "error",
"import/no-mutable-exports": "error",
"import/no-amd": "error",
+526 -692
View File
File diff suppressed because it is too large Load Diff
+23 -23
View File
@@ -122,9 +122,9 @@
"@luma.gl/shadertools": "~9.2.5",
"@luma.gl/webgl": "~9.2.5",
"@reduxjs/toolkit": "^1.9.3",
"@rjsf/core": "^6.7.1",
"@rjsf/core": "^6.8.0",
"@rjsf/utils": "^6.6.2",
"@rjsf/validator-ajv8": "^6.7.1",
"@rjsf/validator-ajv8": "^6.8.0",
"@scarf/scarf": "^1.4.0",
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
"@superset-ui/core": "file:./packages/superset-ui-core",
@@ -158,12 +158,12 @@
"@visx/xychart": "^4.0.0",
"ag-grid-community": "36.1.0",
"ag-grid-react": "36.1.0",
"antd": "^6.6.0",
"antd": "^6.6.1",
"chrono-node": "^2.10.1",
"classnames": "^2.2.5",
"content-disposition": "^2.0.1",
"d3-scale": "^4.0.2",
"dayjs": "^1.11.21",
"dayjs": "^1.11.23",
"dom-to-image-more": "^3.10.2",
"dom-to-pdf": "^0.3.2",
"echarts": "^6.1.0",
@@ -176,8 +176,8 @@
"geostyler-openlayers-parser": "^5.7.1",
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^11.0.1",
"immer": "^11.1.16",
"google-auth-library": "^11.0.2",
"immer": "^11.1.18",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
"js-levenshtein": "^1.1.6",
@@ -185,7 +185,7 @@
"json-stringify-pretty-compact": "^4.0.0",
"lodash": "^4.18.1",
"lodash-es": "^4.18.1",
"mapbox-gl": "^3.28.1",
"mapbox-gl": "^3.29.0",
"markdown-to-jsx": "^9.10.2",
"match-sorter": "^8.3.0",
"memoize-one": "^6.0.0",
@@ -230,7 +230,7 @@
"use-event-callback": "^0.1.0",
"use-immer": "^0.11.0",
"use-query-params": "^2.2.2",
"uuid": "^14.0.1",
"uuid": "^14.0.2",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"yargs": "^18.1.0"
},
@@ -257,14 +257,14 @@
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.62.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.5.7",
"@storybook/addon-links": "10.5.7",
"@storybook/react-webpack5": "10.5.7",
"@storybook/addon-docs": "10.5.10",
"@storybook/addon-links": "10.5.10",
"@storybook/react-webpack5": "10.5.10",
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.47",
"@swc/plugin-emotion": "^14.19.0",
"@swc/plugin-transform-imports": "^12.5.0",
"@swc/core": "^1.16.1",
"@swc/plugin-emotion": "^15.0.0",
"@swc/plugin-transform-imports": "^13.0.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^15.0.0",
@@ -295,9 +295,9 @@
"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.13",
"baseline-browser-mapping": "^2.11.16",
"cheerio": "1.2.0",
"concurrently": "^10.0.4",
"concurrently": "^10.0.5",
"copy-webpack-plugin": "^14.0.0",
"cross-env": "^10.1.0",
"css-loader": "^7.1.4",
@@ -311,8 +311,8 @@
"eslint-plugin-lodash": "^8.0.0",
"eslint-plugin-no-only-tests": "^3.4.0",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
"eslint-plugin-storybook": "10.5.7",
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.2",
"eslint-plugin-storybook": "10.5.10",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
@@ -326,13 +326,13 @@
"jest-websocket-mock": "^2.5.0",
"js-yaml-loader": "^1.2.2",
"jsdom": "^30.0.1",
"lerna": "^10.0.0",
"lerna": "^10.0.1",
"lightningcss": "^1.33.0",
"mini-css-extract-plugin": "^2.10.2",
"minimizer-webpack-plugin": "^5.6.1",
"open-cli": "^9.0.0",
"oxfmt": "^0.63.0",
"oxlint": "^1.78.0",
"oxfmt": "^0.64.0",
"oxlint": "^1.79.0",
"po2json": "^0.4.5",
"postcss-styled-syntax": "^0.7.2",
"process": "^0.11.10",
@@ -343,7 +343,7 @@
"source-map": "^0.8.0",
"source-map-support": "^0.5.21",
"speed-measure-webpack-plugin": "^1.6.0",
"storybook": "10.5.7",
"storybook": "10.5.10",
"style-loader": "^4.0.0",
"stylelint": "^17.14.1",
"swc-loader": "^0.2.7",
@@ -392,7 +392,7 @@
"@luma.gl/shadertools": "~9.2.5",
"@luma.gl/webgl": "~9.2.5",
"core-js": "^3.38.1",
"dompurify": "^3.4.11",
"dompurify": "^3.4.13",
"esbuild": "^0.28.1",
"eslint-plugin-import": {
"eslint": "$eslint"
@@ -35,10 +35,6 @@
"types": "./lib/commands/index.d.ts",
"default": "./lib/commands/index.js"
},
"./dashboardComponents": {
"types": "./lib/dashboardComponents/index.d.ts",
"default": "./lib/dashboardComponents/index.js"
},
"./editors": {
"types": "./lib/editors/index.d.ts",
"default": "./lib/editors/index.js"
@@ -28,7 +28,6 @@
import { Chat } from '../chat';
import { Command } from '../commands';
import { DashboardComponentDefinition } from '../dashboardComponents';
import { View } from '../views';
import { Menu } from '../menus';
import { Editor } from '../editors';
@@ -91,9 +90,4 @@ export interface Contributions {
* chat at a time.
*/
chat?: Chat;
/**
* Dashboard layout components contributed by the extension. Each becomes a
* first-class, draggable element in the dashboard builder palette.
*/
dashboardComponents?: DashboardComponentDefinition[];
}
@@ -1,158 +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.
*/
/**
* @fileoverview Dashboard component contribution API for Superset extensions.
*
* A dashboard component is a first-class dashboard layout element (like the
* built-in Markdown or iframe) contributed by an extension. The extension
* provides a single React component that renders the element's *content*; the
* host owns the surrounding chrome (drag handle, resize, delete) so the
* contributed component stays small and the contract stable.
*
* This replaces the legacy `DashboardComponentsRegistry` / `DYNAMIC_TYPE`
* mechanism, which is deprecated.
*
* @example
* ```typescript
* import { dashboardComponents } from '@apache-superset/core';
*
* dashboardComponents.registerDashboardComponent(
* {
* id: 'acme.weather',
* name: 'Weather widget',
* icon: 'CloudOutlined',
* resizable: true,
* defaultMeta: { width: 4, height: 50 },
* },
* WeatherWidget,
* );
* ```
*/
import { ComponentType } from 'react';
import type { Disposable, Event } from '../common';
/**
* Props passed by the host to a contributed dashboard component. The host
* renders this component inside its own drag/resize/delete chrome, so the
* component only needs to render content (and, in edit mode, its own editor
* affordances). Persisted state lives in `meta`; mutate it via `updateMeta`.
*/
export interface DashboardComponentProps {
/** The layout item id of this component instance. */
id: string;
/** The component instance's persisted meta (round-trips in the layout). */
meta: Record<string, unknown>;
/** Whether the dashboard is in edit mode. */
editMode: boolean;
/** Shallow-merge a patch into this component's persisted meta. */
updateMeta: (patch: Record<string, unknown>) => void;
}
/**
* Declarative descriptor for a contributed dashboard component. The behavior
* fields replace what was historically hardcoded in the dashboard util maps
* (resizability, default sizing, nesting, etc.).
*/
export interface DashboardComponentDefinition {
/** Namespaced unique id, e.g. "acme.weather" or "superset.iframe". */
id: string;
/** Human-readable label shown in the builder palette. */
name: string;
/** Optional longer description. */
description?: string;
/** Icon id (a known Superset icon name) shown in the palette. */
icon?: string;
/** Whether instances can be resized. Defaults to true. */
resizable?: boolean;
/** Default `meta` seeded onto a newly created instance (e.g. width/height). */
defaultMeta?: Record<string, unknown>;
/**
* Whether an instance counts as user content for "is this dashboard empty?"
* detection. Defaults to true.
*/
isUserContent?: boolean;
/** Minimum width in grid columns. Defaults to 1. */
minWidth?: number;
/**
* Restrict which container types may hold this component (e.g.
* `['GRID', 'TAB']`). When omitted, the component is allowed wherever a
* standard content leaf is allowed (grid, row, column, tab).
*/
validParents?: string[];
/**
* Whether a drop into the grid or a tab auto-wraps the component in a row.
* Defaults to true (matching built-in content components).
*/
wrapInRow?: boolean;
}
/**
* The subset of a definition's behavior that is seeded onto each instance's
* `meta` at creation, so the dashboard layout engine can honor it (and so it
* round-trips in the saved layout even if the extension later becomes
* unavailable). Read by the dashboard util maps; not part of the rendered
* component's concern.
*/
export interface DashboardComponentBehaviorMeta {
extensionComponentId: string;
resizable?: boolean;
isUserContent?: boolean;
minWidth?: number;
validParents?: string[];
wrapInRow?: boolean;
}
/**
* A registered dashboard component: its definition plus the React component
* the host renders.
*/
export interface RegisteredDashboardComponent {
definition: DashboardComponentDefinition;
Component: ComponentType<DashboardComponentProps>;
}
/**
* Registers a dashboard component. Disposing the returned Disposable
* unregisters it. Registering a second component with the same id replaces the
* first.
*
* @param definition The component descriptor (id, name, behavior).
* @param component The React component rendering the element's content.
* @returns A Disposable that unregisters the component when disposed.
*/
export declare function registerDashboardComponent(
definition: DashboardComponentDefinition,
component: ComponentType<DashboardComponentProps>,
): Disposable;
/** Returns the registered component for `id`, or undefined. */
export declare function getDashboardComponent(
id: string,
): RegisteredDashboardComponent | undefined;
/** Returns all registered dashboard components. */
export declare function getDashboardComponents(): RegisteredDashboardComponent[];
/** Event fired when a dashboard component is registered. */
export declare const onDidRegisterDashboardComponent: Event<DashboardComponentDefinition>;
/** Event fired when a dashboard component is unregistered. */
export declare const onDidUnregisterDashboardComponent: Event<DashboardComponentDefinition>;
@@ -20,7 +20,6 @@ export * as common from './common';
export * as authentication from './authentication';
export * as chat from './chat';
export * as commands from './commands';
export * as dashboardComponents from './dashboardComponents';
export * as editors from './editors';
export * as extensions from './extensions';
export * as menus from './menus';
@@ -51,6 +51,20 @@ export interface MetricOptionProps {
shouldShowTooltip?: boolean;
}
/**
* `url` is an arbitrary caller-supplied string rendered as an href. Only
* http(s) and relative URLs become links; other schemes degrade to plain
* text.
*/
function isSafeHref(url: string): boolean {
try {
const { protocol } = new URL(url, window.location.origin);
return protocol === 'http:' || protocol === 'https:';
} catch {
return false;
}
}
export function MetricOption({
metric,
labelRef,
@@ -70,7 +84,7 @@ export function MetricOption({
`}
ref={labelRef}
>
{url ? (
{url && isSafeHref(url) ? (
<Typography.Link
href={url}
target={openInNewWindow ? '_blank' : ''}
@@ -122,6 +122,12 @@ export const timeComparisonControls: ({
}
return newState;
},
// Re-run this control's validation whenever `time_compare` changes so
// the "date required" error clears once a non-custom shift is picked.
// Without it the stale error survives in Redux (see the
// dependantControls path in exploreReducer's SET_FIELD_VALUE handler)
// and blocks further chart updates until a page refresh.
validationDependencies: ['time_compare'],
},
},
],
@@ -17,7 +17,7 @@
* under the License.
*/
import { QueryFormMetric } from '@superset-ui/core';
import { getTotalsMetrics } from './getTotalsMetrics';
import { getTotalsMetrics, toTotalsAggregate } from './getTotalsMetrics';
const simpleMetric = (aggregate: string): QueryFormMetric =>
({
@@ -76,4 +76,31 @@ describe('getTotalsMetrics', () => {
test('returns an empty array when given no metrics', () => {
expect(getTotalsMetrics([], 'AVG')).toEqual([]);
});
test("ORIGINAL keeps each metric's own aggregate", () => {
const metrics = [
simpleMetric('COUNT_DISTINCT'),
sqlMetric(),
savedMetric(),
];
const result = getTotalsMetrics(metrics, 'ORIGINAL');
expect(result).toBe(metrics);
expect(result[0]).toEqual(
expect.objectContaining({ aggregate: 'COUNT_DISTINCT' }),
);
});
});
describe('toTotalsAggregate', () => {
test.each(['SUM', 'AVG'] as const)('passes %s through', value => {
expect(toTotalsAggregate(value)).toBe(value);
});
test.each([undefined, null, '', 'MEDIAN', 'sum'])(
'falls back to ORIGINAL for %p',
value => {
expect(toTotalsAggregate(value)).toBe('ORIGINAL');
},
);
});
@@ -18,26 +18,46 @@
*/
import { isAdhocMetricSimple, QueryFormMetric } from '@superset-ui/core';
export type TotalsAggregate = 'SUM' | 'AVG';
/**
* How the "Show summary" totals row aggregates each metric.
*
* ``ORIGINAL`` keeps every metric's own aggregation. It is the default because
* overriding is not universally valid: ``SUM`` over a ``COUNT_DISTINCT`` of a
* non-numeric column (a uuid, say) is rejected outright by the database, and
* over a numeric id column it silently produces a meaningless number.
*/
export type TotalsAggregate = 'ORIGINAL' | 'SUM' | 'AVG';
/**
* Build the metrics for a chart's "Show summary" totals query, overriding
* each Simple (adhoc) metric's aggregate function with the user-chosen
* totals aggregate. The totals query has no GROUP BY, so the database
* evaluates each metric fresh over all rows -- swapping the aggregate here
* is a correct, independent computation, not a re-aggregation of
* already-aggregated per-row values.
* Build the metrics for a chart's "Show summary" totals query.
*
* Custom-SQL metrics and saved (string) metrics pass through unchanged:
* there is no safe way to rewrite an arbitrary SQL expression's aggregate
* function without parsing it, so the totals row keeps their own native
* aggregate for those.
* With SUM or AVG, each Simple (adhoc) metric is cloned with its aggregate
* replaced. The totals query has no GROUP BY, so the database evaluates each
* metric fresh over all rows -- that swap is an independent computation, not a
* re-aggregation of already-aggregated per-row values.
*
* Custom-SQL and saved (string) metrics always pass through unchanged: there is
* no safe way to rewrite an arbitrary SQL expression's aggregate without
* parsing it, so the totals row keeps their own native aggregate.
*/
export function getTotalsMetrics(
metrics: QueryFormMetric[],
aggregate: TotalsAggregate,
): QueryFormMetric[] {
if (aggregate === 'ORIGINAL') {
return metrics;
}
return metrics.map(metric =>
isAdhocMetricSimple(metric) ? { ...metric, aggregate } : metric,
);
}
/**
* Narrow a raw ``totals_aggregate`` form-data value to a TotalsAggregate.
*
* Anything other than an explicit SUM/AVG including charts saved before the
* control existed keeps each metric's own aggregation.
*/
export function toTotalsAggregate(value: unknown): TotalsAggregate {
return value === 'SUM' || value === 'AVG' ? value : 'ORIGINAL';
}
@@ -112,3 +112,26 @@ test('shows a Tooltip for the verbose metric name', () => {
const { getByTestId } = setup();
expect(getByTestId('mock-tooltip')).toBeInTheDocument();
});
test('does not render javascript: URLs as links', () => {
// Regression test: the url prop can be creator-authored and must
// never become a script-bearing href for other viewers.
const { queryByRole, getByText } = setup({
url: 'javascript:alert(document.domain)', // eslint-disable-line no-script-url
});
expect(queryByRole('link')).not.toBeInTheDocument();
expect(getByText(defaultProps.metric.verbose_name)).toBeInTheDocument();
});
test('does not render data: URLs as links', () => {
const { queryByRole } = setup({
url: 'data:text/html,<script>alert(1)</script>',
});
expect(queryByRole('link')).not.toBeInTheDocument();
});
test('renders relative URLs as links', () => {
const { getByRole } = setup({
url: '/superset/dashboard/1/',
});
expect(
getByRole('link', { name: defaultProps.metric.verbose_name }),
).toHaveAttribute('href', '/superset/dashboard/1/');
});
@@ -67,8 +67,8 @@
"d3-scale": "^4.0.2",
"d3-time": "^3.1.0",
"d3-time-format": "^4.1.0",
"dayjs": "^1.11.21",
"dompurify": "^3.4.13",
"dayjs": "^1.11.23",
"dompurify": "^3.4.14",
"fetch-retry": "^6.0.0",
"handlebars": "^4.7.9",
"jed": "^1.1.1",
@@ -79,7 +79,7 @@
"re-resizable": "^6.11.2",
"react-ace": "^14.0.1",
"react-draggable": "^4.7.1",
"react-error-boundary": "^6.1.2",
"react-error-boundary": "^6.1.3",
"react-js-cron": "^6.0.2",
"react-markdown": "^10.1.0",
"react-resize-detector": "^7.1.2",
@@ -0,0 +1,153 @@
/**
* 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 { useState } from 'react';
import { fireEvent, render, screen } from '@superset-ui/core/spec';
import { Input } from '../Input';
import { Modal } from './Modal';
const drag = (
target: Element,
from: [number, number],
to: [number, number],
) => {
fireEvent.mouseDown(target, { clientX: from[0], clientY: from[1] });
fireEvent.mouseMove(document, { clientX: to[0], clientY: to[1] });
fireEvent.mouseUp(document);
};
const isDragged = () => !!document.querySelector('.react-draggable-dragged');
describe('Modal draggable', () => {
test('dragging from the title bar moves the modal', () => {
render(
<Modal show onHide={() => {}} title="Edit Dataset" draggable name="test">
<Input data-test="field" defaultValue="value" />
</Modal>,
);
const trigger = document.querySelector('.draggable-trigger') as HTMLElement;
drag(trigger, [100, 50], [150, 90]);
expect(isDragged()).toBe(true);
});
test('dragging inside modal content does not move the modal', () => {
render(
<Modal show onHide={() => {}} title="Edit Dataset" draggable name="test">
<Input data-test="field" defaultValue="first_view_event" />
</Modal>,
);
const input = screen.getByTestId('field');
drag(input, [200, 400], [260, 430]);
expect(isDragged()).toBe(false);
});
test('dragging inside modal content does not move the modal, even after an unrelated re-render while the title was hovered', () => {
// Regression test: the title bar used to gate dragging with a
// hover-tracked boolean (mouseover/mouseout on `.draggable-trigger`)
// instead of react-draggable's own `handle` prop. Because the title
// element was defined as an inline component recreated on every
// render, any unrelated state change while the cursor was over the
// title (e.g. typing in any field) force-remounted it without a real
// mouseout ever firing, leaving dragging permanently enabled -- so
// selecting text anywhere in the modal dragged the whole modal
// instead.
function Harness() {
const [tick, setTick] = useState(0);
return (
<Modal
show
onHide={() => {}}
title="Edit Dataset"
draggable
name="test"
>
<button
type="button"
data-test="rerender"
onClick={() => setTick(tick + 1)}
>
rerender
</button>
<Input data-test="field" defaultValue="first_view_event" />
</Modal>
);
}
render(<Harness />);
const trigger = document.querySelector('.draggable-trigger') as HTMLElement;
fireEvent.mouseOver(trigger);
fireEvent.click(screen.getByTestId('rerender'));
const input = screen.getByTestId('field');
drag(input, [200, 400], [260, 430]);
expect(isDragged()).toBe(false);
});
test('dragging is disabled entirely when draggable is not set', () => {
render(
<Modal show onHide={() => {}} title="Edit Dataset" name="test">
<Input data-test="field" defaultValue="value" />
</Modal>,
);
expect(document.querySelector('.draggable-trigger')).toBeNull();
});
test('draggableConfig cannot re-enable dragging on a non-draggable modal', () => {
render(
<Modal
show
onHide={() => {}}
title="Edit Dataset"
name="test"
draggableConfig={{ disabled: false }}
>
<Input data-test="field" defaultValue="value" />
</Modal>,
);
expect(document.querySelector('.draggable-trigger')).toBeNull();
});
test('draggableConfig can still opt a draggable modal out of dragging', () => {
render(
<Modal
show
onHide={() => {}}
title="Edit Dataset"
draggable
name="test"
draggableConfig={{ disabled: true }}
>
<Input data-test="field" defaultValue="value" />
</Modal>,
);
const trigger = document.querySelector('.draggable-trigger') as HTMLElement;
drag(trigger, [100, 50], [150, 90]);
expect(isDragged()).toBe(false);
});
});
@@ -269,7 +269,6 @@ const CustomModal = ({
);
const draggableRef = useRef<HTMLDivElement>(null);
const [bounds, setBounds] = useState<DraggableBounds>({});
const [dragDisabled, setDragDisabled] = useState<boolean>(true);
const theme = useTheme();
const handleOnHide = () => {
@@ -339,19 +338,7 @@ const CustomModal = ({
}, [hideFooter, resizableConfig]);
const ModalTitle = () =>
draggable ? (
<div
className="draggable-trigger"
onMouseOver={() => dragDisabled && setDragDisabled(false)}
onMouseOut={() => !dragDisabled && setDragDisabled(true)}
onFocus={() => dragDisabled && setDragDisabled(false)}
onBlur={() => !dragDisabled && setDragDisabled(true)}
>
{title}
</div>
) : (
<>{title}</>
);
draggable ? <div className="draggable-trigger">{title}</div> : <>{title}</>;
return (
<StyledModal
@@ -378,13 +365,19 @@ const CustomModal = ({
modalRender={modal =>
resizable || draggable ? (
<Draggable
disabled={!draggable || dragDisabled}
bounds={bounds ?? false}
onStart={(event, uiData) => onDragStart(event, uiData)}
{...draggableConfig}
// `disabled` and `handle` are applied after the spread so callers
// can't use `draggableConfig` to re-enable dragging on a
// non-draggable modal or move the drag handle off the title bar.
// A caller opting a draggable modal out via
// `draggableConfig.disabled` is still honored.
disabled={!draggable || !!draggableConfig?.disabled}
handle={draggable ? '.draggable-trigger' : undefined}
// Pass nodeRef so react-draggable does not fall back to
// ReactDOM.findDOMNode (deprecated in React 18+ Strict Mode).
nodeRef={draggableRef}
{...draggableConfig}
>
{resizable ? (
<Resizable className="resizable" {...getResizableConfig}>
@@ -65,12 +65,16 @@ export type AntdExposedProps = Pick<
| 'onOpenChange'
| 'optionRender'
| 'placeholder'
| 'prefix'
| 'showArrow'
| 'showSearch'
| 'tokenSeparators'
| 'virtual'
| 'getPopupContainer'
| 'menuItemSelectedIcon'
// lets a caller with long option labels stop the popup inheriting the
// trigger's width, which otherwise truncates every option
| 'popupMatchSelectWidth'
>;
export type SelectOptionsType = Exclude<AntdProps['options'], undefined>;
@@ -246,6 +246,34 @@ test('wraps component with proper container div', () => {
expect(wrapper).toHaveAttribute('data-themed-ag-grid', 'true');
});
test('applies non-transparent backgrounds to native menus, tooltips and overlays', () => {
const customTheme = {
...supersetTheme,
colorBgElevated: '#f2f2f2',
};
render(
<ThemeProvider theme={customTheme}>
<ThemedAgGridReact rowData={mockRowData} columnDefs={mockColumnDefs} />
</ThemeProvider>,
);
const agGrid = screen.getByTestId('ag-grid-react');
const theme = JSON.parse(agGrid.getAttribute('data-theme') || '{}');
// ag-grid's own context/column menus, side bar, tooltips and overlays are
// rendered against these params rather than `backgroundColor` (which is
// intentionally 'transparent' so the surrounding app shows through the
// grid body). Without explicit values they inherit transparency too,
// making native menus/popups unreadable.
expect(theme.chromeBackgroundColor).toBe('#f2f2f2');
expect(theme.menuBackgroundColor).toBe('#f2f2f2');
expect(theme.menuBorder).toBe(true);
expect(theme.sideBarBackgroundColor).toBe('#f2f2f2');
expect(theme.tooltipBackgroundColor).toBe('#f2f2f2');
expect(theme.modalOverlayBackgroundColor).toBe('#f2f2f2');
});
test('handles missing theme gracefully', () => {
const incompleteTheme = {
...supersetTheme,
@@ -104,6 +104,17 @@ export const ThemedAgGridReact = forwardRef<
foregroundColor: theme.colorText,
browserColorScheme: isDarkMode ? 'dark' : 'light',
// Native menus, popups, side bar, tooltips and loading/no-rows overlays
// are rendered against these params rather than `backgroundColor`
// (which is intentionally transparent). Without explicit values they
// inherit transparency too, making them unreadable.
chromeBackgroundColor: theme.colorBgElevated,
menuBackgroundColor: theme.colorBgElevated,
menuBorder: true,
sideBarBackgroundColor: theme.colorBgElevated,
tooltipBackgroundColor: theme.colorBgElevated,
modalOverlayBackgroundColor: theme.colorBgElevated,
// Header styling
headerBackgroundColor: theme.colorFillTertiary,
headerTextColor: theme.colorTextHeading,
@@ -38,11 +38,21 @@ function formatMemory(
: ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'RB', 'QB'];
const base = binary ? 1024 : 1000;
const i = Math.min(
suffixes.length - 1,
Math.floor(Math.log(absValue) / Math.log(base)),
let i = Math.max(
0,
Math.min(
suffixes.length - 1,
Math.floor(Math.log(absValue) / Math.log(base)),
),
);
formatted = `${sign}${parseFloat((absValue / Math.pow(base, i)).toFixed(decimals))}${suffixes[i]}`;
let scaled = parseFloat((absValue / Math.pow(base, i)).toFixed(decimals));
if (scaled >= base && i < suffixes.length - 1) {
i += 1;
scaled = parseFloat((absValue / Math.pow(base, i)).toFixed(decimals));
}
formatted = `${sign}${scaled}${suffixes[i]}`;
}
if (transfer) {
@@ -64,9 +64,8 @@ export default function createSmartNumberFormatter(
description,
formatFunc: value => `${getSign(value)}${formatValue(value)}`,
id:
id || signed
? NumberFormats.SMART_NUMBER_SIGNED
: NumberFormats.SMART_NUMBER,
id ??
(signed ? NumberFormats.SMART_NUMBER_SIGNED : NumberFormats.SMART_NUMBER),
label: label ?? 'Adaptive formatter',
});
}
@@ -119,6 +119,20 @@ export function retrieveErrorMessage(
return statusError || parseStringResponse(str);
}
function getFirstValidationError(message: JsonObject): string | undefined {
const [firstError] = Object.values(message);
if (typeof firstError === 'string') {
return firstError;
}
if (Array.isArray(firstError)) {
return firstError.find((item): item is string => typeof item === 'string');
}
return undefined;
}
export function parseErrorJson(responseJson: JsonObject): ClientErrorObject {
let error = { ...responseJson };
// Backwards compatibility for old error renderers with the new error object
@@ -126,13 +140,12 @@ export function parseErrorJson(responseJson: JsonObject): ClientErrorObject {
error.error = error.description = error.errors[0].message;
error.link = error.errors[0]?.extra?.link;
}
// Marshmallow field validation returns the error message in the format
// of { message: { field1: [msg1, msg2], field2: [msg], } }
// Marshmallow field validation returns arrays for string messages, but
// serializes lazy translation messages as strings instead.
if (!error.error && error.message) {
if (typeof error.message === 'object') {
error.error =
Object.values(error.message as Record<string, string[]>)[0]?.[0] ||
t('Invalid input');
getFirstValidationError(error.message) || t('Invalid input');
}
if (typeof error.message === 'string') {
if (checkForHtml(error.message)) {
@@ -25,8 +25,11 @@ export type Aggregate =
| 'COUNT'
| 'COUNT_DISTINCT'
| 'MAX'
| 'MEDIAN'
| 'MIN'
| 'SUM';
| 'STDDEV_SAMP'
| 'SUM'
| 'VAR_SAMP';
export interface AdhocMetricBase {
hasCustomLabel?: boolean;
@@ -49,5 +49,6 @@ export { default as finestTemporalGrainFormatter } from './formatters/finestTemp
export { default as normalizeTimestamp } from './utils/normalizeTimestamp';
export { default as denormalizeTimestamp } from './utils/denormalizeTimestamp';
export { default as createTimeRangeFromGranularity } from './utils/createTimeRangeFromGranularity';
export * from './types';
@@ -28,10 +28,10 @@ export enum FeatureFlag {
AlertReportSlackV2 = 'ALERT_REPORT_SLACK_V2',
AlertReportWebhook = 'ALERT_REPORT_WEBHOOK',
AlertReportsFilter = 'ALERT_REPORTS_FILTER',
AlertReportsRetry = 'ALERT_REPORTS_RETRY',
AllowFullCsvExport = 'ALLOW_FULL_CSV_EXPORT',
ChartPluginsExperimental = 'CHART_PLUGINS_EXPERIMENTAL',
ConfirmDashboardDiff = 'CONFIRM_DASHBOARD_DIFF',
CspRuntimeAllowlist = 'CSP_RUNTIME_ALLOWLIST',
CssTemplates = 'CSS_TEMPLATES',
DashboardVirtualization = 'DASHBOARD_VIRTUALIZATION',
DashboardVirtualizationDeferData = 'DASHBOARD_VIRTUALIZATION_DEFER_DATA',
@@ -123,6 +123,25 @@ describe('isProbablyHTML', () => {
expect(isProbablyHTML('<canvas></canvas>')).toBe(true);
expect(isProbablyHTML('<iframe src="page.html"></iframe>')).toBe(true);
});
test('should return true for script-capable and foreign-content tags', () => {
expect(isProbablyHTML('<svg onload="alert(1)"></svg>')).toBe(true);
expect(isProbablyHTML('<math><mi>x</mi></math>')).toBe(true);
expect(
isProbablyHTML('<details open ontoggle="alert(1)">x</details>'),
).toBe(true);
expect(isProbablyHTML('<summary>x</summary>')).toBe(true);
expect(isProbablyHTML('<object data="x"></object>')).toBe(true);
expect(isProbablyHTML('<embed src="x">')).toBe(true);
expect(isProbablyHTML('<marquee>x</marquee>')).toBe(true);
expect(isProbablyHTML('<template>x</template>')).toBe(true);
expect(isProbablyHTML('<dialog open>x</dialog>')).toBe(true);
});
test('should return true for elements that parse into document.head', () => {
expect(isProbablyHTML('<style>body { display: none; }</style>')).toBe(true);
expect(isProbablyHTML('<title>injected</title>')).toBe(true);
});
});
describe('sanitizeHtmlIfNeeded', () => {
@@ -137,6 +156,24 @@ describe('sanitizeHtmlIfNeeded', () => {
const sanitizedString = sanitizeHtmlIfNeeded(plainText);
expect(sanitizedString).toEqual(plainText);
});
test('should sanitize svg/details/style payloads instead of passing them through', () => {
const svgPayload = '<svg onload="alert(document.cookie)"></svg>';
const sanitizedSvg = sanitizeHtmlIfNeeded(svgPayload);
expect(sanitizedSvg).not.toContain('<svg');
expect(sanitizedSvg).not.toContain('onload');
// `details` (with its `open` attribute) is in js-xss's default
// whitelist, so the tag itself survives sanitization; the fix is that
// the payload is now routed through FilterXSS at all, which strips the
// non-whitelisted `ontoggle` handler instead of returning it verbatim.
const detailsPayload = '<details open ontoggle="alert(1)">x</details>';
const sanitizedDetails = sanitizeHtmlIfNeeded(detailsPayload);
expect(sanitizedDetails).toEqual('<details open>x</details>');
const stylePayload = '<style>body { display: none; }</style>';
expect(sanitizeHtmlIfNeeded(stylePayload)).not.toContain('<style');
});
});
describe('safeHtmlSpan', () => {
@@ -154,6 +154,20 @@ const KNOWN_HTML_TAGS = new Set([
'html',
'head',
'body',
// Script-capable elements and foreign-content roots (SVG/MathML). These
// must be classified as HTML so that downstream sanitization is applied;
// omitting them makes the heuristic fail open — payloads such as
// `<svg onload=...>` or `<details open ontoggle=...>` would be classified
// "not HTML" and returned verbatim by sanitizeHtmlIfNeeded.
'svg',
'math',
'details',
'summary',
'object',
'embed',
'marquee',
'template',
'dialog',
]);
const HTML_TAG_PATTERN = new RegExp(
@@ -183,10 +197,15 @@ export function isProbablyHTML(text: string) {
const parser = new DOMParser();
const doc = parser.parseFromString(cleanedStr, 'text/html');
// Check if parsing created actual HTML elements (not just text nodes)
const elements = Array.from(doc.body.childNodes).filter(
node => node.nodeType === 1,
) as Element[];
// Check if parsing created actual HTML elements (not just text nodes).
// Some elements (e.g. <style>, <title>, <meta>, <link>) parse into
// document.head rather than document.body, so both must be inspected —
// otherwise a bare <style> payload is classified "not HTML" and skips
// sanitization.
const elements = [
...Array.from(doc.head.childNodes),
...Array.from(doc.body.childNodes),
].filter(node => node.nodeType === 1) as Element[];
// If no elements were created, it's not HTML
if (elements.length === 0) {
@@ -60,6 +60,31 @@ test('formats float bytes in human readable format with default options', () =>
expect(formatter(1200.666)).toBe('1.2kB');
});
test('formats values below one byte without dropping the unit', () => {
const formatter = createMemoryFormatter();
expect(formatter(0.5)).toBe('0.5B');
expect(formatter(0.004)).toBe('0B');
expect(formatter(-0.25)).toBe('-0.25B');
const binaryFormatter = createMemoryFormatter({ binary: true });
expect(binaryFormatter(0.5)).toBe('0.5B');
});
test('rolls over to the next unit when rounding reaches the base', () => {
const formatter = createMemoryFormatter();
expect(formatter(999999)).toBe('1MB');
expect(formatter(999995)).toBe('1MB');
expect(formatter(999994)).toBe('999.99kB');
expect(formatter(-999999)).toBe('-1MB');
const binaryFormatter = createMemoryFormatter({ binary: true });
expect(binaryFormatter(1024 * 1024 - 1)).toBe('1MiB');
// the largest unit has nothing to roll over into
const largest = createMemoryFormatter();
expect(largest(Math.pow(1000, 11))).toBe('1000QB');
});
test('formats bytes in human readable format with additional binary option', () => {
const formatter = createMemoryFormatter({ binary: true });
expect(formatter(0)).toBe('0B');
@@ -24,6 +24,12 @@ describe('createSmartNumberFormatter(options)', () => {
const formatter = createSmartNumberFormatter();
expect(formatter).toBeInstanceOf(NumberFormatter);
});
test('uses the supplied formatter id regardless of signed option', () => {
expect(createSmartNumberFormatter({ id: 'custom' }).id).toBe('custom');
expect(
createSmartNumberFormatter({ id: 'custom-signed', signed: true }).id,
).toBe('custom-signed');
});
describe('using default options', () => {
const formatter = createSmartNumberFormatter();
test('formats 0 correctly', () => {
@@ -244,6 +244,24 @@ test('parseErrorJson with message', () => {
});
});
test('parseErrorJson preserves string-valued validation messages', () => {
const calculatedColumnError =
'Custom SQL fields cannot be parsed as a single SQL statement.';
expect(
parseErrorJson({
message: {
'columns.0.expression': calculatedColumnError,
},
}),
).toEqual({
message: {
'columns.0.expression': calculatedColumnError,
},
error: calculatedColumnError,
});
});
test('parseErrorJson with HTML message', () => {
expect(
parseErrorJson({
@@ -96,16 +96,57 @@ export class Menu {
itemText: string,
options?: { timeout?: number },
): Promise<void> {
const popup = await this.openSubmenu(submenuText, {
timeout: options?.timeout,
itemText,
});
// Use dispatchEvent instead of click to bypass viewport and pointer interception
// issues. Ant Design renders submenu popups in a portal that can be positioned
// outside the viewport or behind chart content (e.g., large tables with z-index).
await popup.getByText(itemText, { exact: true }).dispatchEvent('click');
}
/**
* Opens a submenu and returns its popup locator, without selecting an item.
* Useful when the caller needs to read the popup's contents (e.g. the set of
* offered items) rather than clicking a known item.
*
* Uses hover as primary approach, falls back to keyboard then dispatchEvent -
* same fallback chain as {@link selectSubmenuItem}.
*
* @param submenuText - The text of the submenu to open (e.g., "Download")
* @param options - Optional timeout, an `itemText` to scope the popup lookup
* to (useful when multiple submenu popups could otherwise match), and a
* `popupSelector` override for submenus that render with an additional,
* more specific class than the generic Ant Design popup class.
*/
async openSubmenu(
submenuText: string,
options?: { timeout?: number; itemText?: string; popupSelector?: string },
): Promise<Locator> {
const timeout = options?.timeout ?? TIMEOUT.FORM_LOAD;
const matchPopup = (): Locator => {
const base = this.page.locator(
options?.popupSelector ?? Menu.SELECTORS.SUBMENU_POPUP,
);
return options?.itemText
? base.filter({ hasText: options.itemText })
: base;
};
// Try hover first (most natural user interaction)
let popup = await this.openSubmenuWithHover(submenuText, itemText, timeout);
let popup = await this.openSubmenuWithHover(
submenuText,
matchPopup,
timeout,
);
// Fallback to keyboard navigation
if (!popup) {
popup = await this.openSubmenuWithKeyboard(
submenuText,
itemText,
matchPopup,
timeout,
);
}
@@ -114,7 +155,7 @@ export class Menu {
if (!popup) {
popup = await this.openSubmenuWithDispatchEvent(
submenuText,
itemText,
matchPopup,
timeout,
);
}
@@ -125,10 +166,7 @@ export class Menu {
);
}
// Use dispatchEvent instead of click to bypass viewport and pointer interception
// issues. Ant Design renders submenu popups in a portal that can be positioned
// outside the viewport or behind chart content (e.g., large tables with z-index).
await popup.getByText(itemText, { exact: true }).dispatchEvent('click');
return popup;
}
/**
@@ -137,17 +175,14 @@ export class Menu {
*/
private async openSubmenuWithHover(
submenuText: string,
itemText: string,
matchPopup: () => Locator,
timeout: number,
): Promise<Locator | null> {
try {
const submenuTitle = this.getSubmenuTitle(submenuText);
await submenuTitle.hover();
// Find the popup that contains the expected item (scopes to correct popup)
const popup = this.page
.locator(Menu.SELECTORS.SUBMENU_POPUP)
.filter({ hasText: itemText });
const popup = matchPopup();
await popup.waitFor({ state: 'visible', timeout });
// Allow Ant Design's slide-in animation to complete before clicking.
@@ -166,7 +201,7 @@ export class Menu {
*/
private async openSubmenuWithKeyboard(
submenuText: string,
itemText: string,
matchPopup: () => Locator,
timeout: number,
): Promise<Locator | null> {
try {
@@ -174,9 +209,7 @@ export class Menu {
await submenuTitle.focus();
await this.page.keyboard.press('ArrowRight');
const popup = this.page
.locator(Menu.SELECTORS.SUBMENU_POPUP)
.filter({ hasText: itemText });
const popup = matchPopup();
await popup.waitFor({ state: 'visible', timeout });
return popup;
@@ -191,7 +224,7 @@ export class Menu {
*/
private async openSubmenuWithDispatchEvent(
submenuText: string,
itemText: string,
matchPopup: () => Locator,
timeout: number,
): Promise<Locator | null> {
try {
@@ -214,9 +247,7 @@ export class Menu {
);
});
const popup = this.page
.locator(Menu.SELECTORS.SUBMENU_POPUP)
.filter({ hasText: itemText });
const popup = matchPopup();
await popup.waitFor({ state: 'visible', timeout });
return popup;
@@ -22,7 +22,7 @@ import { Modal } from '../core/Modal';
/**
* Confirm Dialog component for Ant Design Modal.confirm dialogs.
* These are the "OK" / "Cancel" confirmation dialogs used throughout Superset.
* These are the "Confirm" / "Cancel" confirmation dialogs used throughout Superset.
* Uses getByRole with name to target specific confirm dialogs when multiple are open.
*/
export class ConfirmDialog extends Modal {
@@ -43,7 +43,7 @@ export class ConfirmDialog extends Modal {
}
/**
* Clicks the OK button to confirm.
* Clicks the Confirm button to confirm.
* @param options.timeout - If provided, silently returns if dialog doesn't appear
* within timeout. If not provided, waits indefinitely (strict mode).
*/
@@ -53,7 +53,7 @@ export class ConfirmDialog extends Modal {
state: 'visible',
timeout: options?.timeout,
});
await this.clickFooterButton('OK');
await this.clickFooterButton('Confirm');
await this.waitForHidden();
} catch (error) {
// Only swallow TimeoutError when timeout was explicitly provided
@@ -0,0 +1,133 @@
/**
* 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 { Locator, Page } from '@playwright/test';
import { Modal } from '../core';
/**
* The "Drill to detail" modal (`DrillDetailModal.tsx`), opened from a chart's
* "More Options" menu or its right-click context menu. Renders the chart's
* underlying sample rows, optionally scoped to a drilled-by value, via the
* `/datasource/samples` API.
*/
export class DrillDetailModal extends Modal {
private static readonly SELECTORS = {
CLOSE_BUTTON: '[data-test="close-drilltodetail-modal"]',
ROW_COUNT_LABEL: '[data-test="row-count-label"]',
METADATA_BAR: '[data-test="metadata-bar"]',
FILTER_COLUMN: '[data-test="filter-col"]',
FILTER_VALUE: '[data-test="filter-val"]',
PAGE_ITEM: '.ant-pagination-item',
ACTIVE_PAGE_ITEM: '.ant-pagination-item-active',
GRID_CELL: '.virtual-table-cell',
} as const;
private readonly specificLocator: Locator;
constructor(page: Page) {
super(page);
// Matched by accessible name rather than a data-test: the antd Modal's own
// data-test (`${name}-modal`) is derived from this same i18n'd `name`
// prop, so it isn't a locale-independent alternative. No data-test exists
// on the dialog root itself.
this.specificLocator = page.getByRole('dialog', {
name: /^Drill to detail:/,
});
}
override get element(): Locator {
return this.specificLocator;
}
/**
* The applied-filter value tags (`<col>=<val>`). Empty when the drill was
* whole-chart (no row/point-level filter applied).
*/
get filterValues(): Locator {
return this.element.locator(DrillDetailModal.SELECTORS.FILTER_VALUE);
}
/** The applied-filter chip(s); each is closable via its own "Close" icon. */
get filterColumns(): Locator {
return this.element.locator(DrillDetailModal.SELECTORS.FILTER_COLUMN);
}
/** Row-count label above the results grid, e.g. "1-50 of 500 rows". */
get rowCountLabel(): Locator {
return this.element.locator(DrillDetailModal.SELECTORS.ROW_COUNT_LABEL);
}
/** The metadata bar (column/row summary) shown once samples have loaded. */
get metadataBar(): Locator {
return this.element.locator(DrillDetailModal.SELECTORS.METADATA_BAR);
}
/** Pagination page-number items below the results grid. */
get pageItems(): Locator {
return this.element.locator(DrillDetailModal.SELECTORS.PAGE_ITEM);
}
/** The currently active pagination page-number item. */
get activePageItem(): Locator {
return this.element.locator(DrillDetailModal.SELECTORS.ACTIVE_PAGE_ITEM);
}
/** Cells of the virtualized results grid. */
get gridCells(): Locator {
return this.element.locator(DrillDetailModal.SELECTORS.GRID_CELL);
}
/**
* Removes the first applied filter by clicking its chip's Close icon,
* re-fetching the unfiltered samples.
*/
async clearFirstFilter(): Promise<void> {
await this.filterColumns.first().getByLabel('Close').click();
}
/** Navigates to the given 1-indexed pagination page. */
async goToPage(pageNumber: number): Promise<void> {
await this.pageItems.nth(pageNumber - 1).click();
}
/**
* Re-fetches the current samples query, resetting pagination to page 1.
*
* Matched by accessible name: the Reload icon carries an i18n'd
* `aria-label` (`t('Reload')`) and no data-test, so this breaks in
* non-English locales the same way `DrillDetailModal.tsx`'s dialog `name`
* does above; the predecessor Cypress test used the same English string.
*/
async reload(): Promise<void> {
await this.element.getByRole('button', { name: 'Reload' }).click();
}
/**
* Closes the modal via its footer Close button.
*
* Targets the button by data-test rather than Modal.clickFooterButton,
* which finds buttons by their visible text. The button label is i18n'd
* ("Close" / "Fermer" / ), so name-based lookups break in non-English
* locales; see DeleteConfirmationModal.clickDelete for the same rationale.
*/
async close(): Promise<void> {
await this.element.locator(DrillDetailModal.SELECTORS.CLOSE_BUTTON).click();
await this.waitForHidden();
}
}
@@ -21,6 +21,7 @@
export { ChartPropertiesModal } from './ChartPropertiesModal';
export { ConfirmDialog } from './ConfirmDialog';
export { DeleteConfirmationModal } from './DeleteConfirmationModal';
export { DrillDetailModal } from './DrillDetailModal';
export { DuplicateDatasetModal } from './DuplicateDatasetModal';
export { EditDatasetModal } from './EditDatasetModal';
export { ImportDatasetModal } from './ImportDatasetModal';
@@ -20,6 +20,7 @@
import { Page, Download, Locator, expect } from '@playwright/test';
import { Button, Input, Menu, Tabs } from '../components/core';
import { DashboardFilterBar } from '../components/dashboard';
import { DrillDetailModal } from '../components/modals';
import { gotoWithRetry } from '../helpers/navigation';
import { html5DragAndDrop } from '../helpers/dnd';
import { TIMEOUT } from '../utils/constants';
@@ -454,4 +455,124 @@ export class DashboardPage {
return { heightBefore: boxBefore.height, heightAfter: boxAfter.height };
}
// ---------------------------------------------------------------------------
// Drill to detail
//
// Charts that implement the DRILL_TO_DETAIL behavior expose two entry points:
// the chart's "More Options" header menu, and a right-click context menu on
// the chart body (a cell, the big-number value, or a canvas data point). Both
// open the same DrillDetailModal, which renders the underlying sample rows for
// the (optionally filtered) chart by calling the `/datasource/samples` API.
// ---------------------------------------------------------------------------
/**
* Open the "Drill to detail" item from a chart's "More Options" header menu.
* This is the whole-chart entry point (no row-level filters applied).
*/
async openDrillToDetailFromMenu(chartId: number): Promise<void> {
const moreOptions = new Button(
this.page,
this.getChart(chartId).getByLabel('More Options', { exact: true }),
);
await moreOptions.click();
await this.page
.getByRole('menuitem', { name: 'Drill to detail', exact: true })
.click();
}
/**
* The DrillDetailModal dialog (titled "Drill to detail: <chart name>").
*/
drillModal(): DrillDetailModal {
return new DrillDetailModal(this.page);
}
/**
* Click the plain "Drill to detail" item in an open chart context menu
* (whole chart, no row-level filter).
*/
async contextMenuDrillToDetail(): Promise<void> {
await this.page
.getByRole('menuitem', { name: 'Drill to detail', exact: true })
.click();
}
/**
* The "Drill to detail by" submenu parent (title) in an open context menu.
* Targeted by its submenu-title element rather than role+name because antd
* appends the arrow-icon name ("right") to the accessible name, and the leaf
* items ("Drill to detail by boy") would otherwise match a role+name lookup.
*/
drillBySubmenuTitle(): Locator {
return this.page.locator('.ant-dropdown-menu-submenu-title', {
hasText: 'Drill to detail by',
});
}
/**
* The chart context menu's Menu component, scoped to the open context
* menu's root. Used to open the "Drill to detail by" submenu robustly:
* plain hover is not reliably picked up by Ant Design's submenu trigger in
* headless Chromium, so this falls back to keyboard and dispatchEvent - see
* {@link Menu.openSubmenu}.
*/
private contextMenu(): Menu {
return new Menu(this.page, '[data-test="chart-context-menu"]');
}
/**
* Opens the "Drill to detail by" submenu and returns its popup, containing
* the leaf value items (e.g. "Drill to detail by boy").
*/
private openDrillBySubmenu(): Promise<Locator> {
return this.contextMenu().openSubmenu('Drill to detail by', {
popupSelector: '.chart-context-submenu',
});
}
/**
* From an open chart context menu, open the "Drill to detail by" submenu and
* click the entry for a specific value (e.g. "boy", "1965", "all").
*/
async contextMenuDrillToDetailBy(value: string): Promise<void> {
const popup = await this.openDrillBySubmenu();
// Use dispatchEvent instead of click to bypass viewport and pointer
// interception issues - see Menu.selectSubmenuItem.
await popup
.getByRole('menuitem', {
name: `Drill to detail by ${value}`,
exact: true,
})
.dispatchEvent('click');
}
/**
* From an open chart context menu, open "Drill to detail by" and return the
* concrete values offered by the submenu (e.g. ["1965", "boy"]), skipping the
* aggregate "all" entry. Used by canvas charts where the value under the
* cursor is data-dependent: the test drills by whatever the menu actually
* offers and asserts that same value round-trips into the modal, which keeps
* the assertion independent of exact pixel/slice geometry.
*
* Reads rendered (HTML-stripped) menu text rather than the item's
* `aria-label`, which carries the raw, unstripped formatted value
* (`useDrillDetailMenuItems`). The two only diverge for formatted values
* that contain HTML markup; callers pass the returned value both to
* `contextMenuDrillToDetailBy` (accessible-name lookup) and to a
* displayed-text assertion on the modal's filter chip, so a value straddling
* both uses only works when it's markup-free. Every value currently offered
* by this dashboard's charts is a plain string, so this hasn't been
* reachable in practice; revisit if a test starts exercising HTML-formatted
* dimension values.
*/
async drillByOfferedValues(): Promise<string[]> {
const popup = await this.openDrillBySubmenu();
const items = popup.locator('[role="menuitem"]');
await items.first().waitFor();
const labels = await items.allInnerTexts();
return labels
.map(l => l.replace(/^Drill to detail by\s*/i, '').trim())
.filter(v => v.length > 0 && v.toLowerCase() !== 'all');
}
}
@@ -0,0 +1,747 @@
/**
* 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.
*/
/**
* E2E migration of the Cypress "Drill to detail modal" suite
* (dashboard/drilltodetail.test.ts).
*
* Drill to detail lets a viewer open a modal of the underlying sample rows for a
* chart optionally filtered to a single data point by either the chart's
* "More Options" header menu or a right-click context menu on the chart body.
* The modal calls the real `/datasource/samples` API, so this is genuinely
* end-to-end: each test API-builds a hermetic dashboard from the `birth_names`
* dataset, renders it in the browser, drives the real menus, and asserts the
* resulting backend round-trip (the samples POST and the filter the modal
* applies).
*
* Why the original suite was fully `describe.skip`:
* "it has issues with autoscrolling and the locked title flakes intricately
* when the rightClick is obstructed by the title."
* That failure mode is Cypress-specific Cypress auto-scrolls the target under
* the sticky chart header before every action. Playwright scrolls once and the
* target stays put, so the entry points are portable here.
*
* What is migrated, and how it is kept deterministic:
* - Modal mechanics (open from header menu, pagination, reload-resets-page)
* and the no-filter big-number drill use stable DOM elements.
* - Table and Pivot drills right-click real DOM cells (no canvas pixels).
* - Canvas (echarts) charts Pie, Line, Scatter, generic/smooth/step
* time-series, Mixed, Box plot, Funnel, Gauge, Treemap DID rely on
* hard-coded pixel coordinates in Cypress to land on a specific slice/point.
* Instead of reproducing those brittle pixels, these tests scan a stable
* region of the canvas (see `rightClickCanvasDatum`), read whichever value
* the drill submenu actually offers for the point under the cursor, drill by
* that value, and assert the SAME value round-trips into the modal filter.
* This exercises the full canvas contextmenu datum samples pipeline
* while staying independent of exact geometry. `Big Number with Trendline`
* drills the whole chart (no datum filter), like `Big Number`.
*
* Excluded (kept out, matching the original's own `describe.skip`s): Bar, Area,
* World Map, Radar skipped upstream for chart-specific reasons.
*/
import {
testWithAssets,
expect,
type TestAssets,
} from '../../helpers/fixtures';
import type { Page, TestInfo } from '@playwright/test';
import { TIMEOUT } from '../../utils/constants';
import { DashboardPage } from '../../pages/DashboardPage';
import { createDashboardWithCharts } from './dashboard-test-helpers';
const DATASET_NAME = 'birth_names';
/**
* Parse a RowCountLabel value ("75.7k rows", "1,234 rows") into a number so
* tests can assert the *invariant* (filtered < unfiltered) without hard-coding
* the dataset-specific totals the original Cypress suite baked in.
*/
function parseRowCount(text: string): number {
const m = text.match(/([\d.,]+)\s*([kKmM]?)/);
if (!m) return NaN;
let n = parseFloat(m[1].replace(/,/g, ''));
const suffix = m[2].toLowerCase();
if (suffix === 'k') n *= 1e3;
if (suffix === 'm') n *= 1e6;
return n;
}
interface ChartSpec {
vizType: string;
chartNamePrefix: string;
params: Record<string, unknown>;
}
/**
* API-build a hermetic single-chart dashboard from birth_names and return its
* dashboard and chart ids. Thin single-chart wrapper around
* `createDashboardWithCharts`, the build helper shared by the other migrated
* dashboard specs reused here rather than hand-rolling position-json and id
* extraction again.
*/
async function buildSingleChartDashboard(
page: Page,
testAssets: TestAssets,
testInfo: TestInfo,
spec: ChartSpec,
): Promise<{ dashboardId: number; chartId: number }> {
const { dashboardId, charts } = await createDashboardWithCharts(
page,
testAssets,
testInfo,
{
datasetName: DATASET_NAME,
chartNamePrefix: spec.chartNamePrefix,
dashboardTitlePrefix: spec.chartNamePrefix,
chartSpecs: [{ viz_type: spec.vizType, params: spec.params }],
},
);
return { dashboardId, chartId: charts[0].id };
}
/**
* Right-click an echarts canvas until a data point is hit i.e. until the
* context menu offers an *enabled* "Drill to detail by" submenu (a miss renders
* that item disabled, as a plain menu item rather than a submenu title).
*
* echarts renders to a single canvas, so there is no per-datum DOM element to
* target and the exact pixel of a mark depends on chart geometry (donut hole,
* legend size, axis padding). Rather than hard-code Cypress's brittle pixel
* coordinates, this scans a small set of candidate points a radial ring for
* pie/radial charts, a grid for cartesian charts and stops at the first that
* lands on a mark. The drill value is then whatever that mark represents, so the
* caller asserts a value round-trip rather than a specific geometry.
*/
async function rightClickCanvasDatum(
page: Page,
dashboard: DashboardPage,
canvas: ReturnType<Page['locator']>,
pattern: 'ring' | 'grid' | 'dense',
): Promise<void> {
const box = await canvas.boundingBox();
if (!box) throw new Error('canvas has no bounding box');
const ringPoints = (): Array<{ x: number; y: number }> => {
const pts: Array<{ x: number; y: number }> = [];
const cx = box.width / 2;
const cy = box.height / 2;
const minSide = Math.min(box.width, box.height);
for (const rf of [0.3, 0.22, 0.38]) {
for (let a = 0; a < 360; a += 45) {
const rad = (a * Math.PI) / 180;
pts.push({
x: cx + Math.cos(rad) * minSide * rf,
y: cy + Math.sin(rad) * minSide * rf,
});
}
}
return pts;
};
const gridPoints = (): Array<{ x: number; y: number }> => {
const pts: Array<{ x: number; y: number }> = [];
for (const yf of [0.5, 0.4, 0.6, 0.3, 0.7]) {
for (const xf of [0.3, 0.45, 0.6, 0.2, 0.75]) {
pts.push({ x: box.width * xf, y: box.height * yf });
}
}
return pts;
};
// 'dense' merges both scans for radial/stacked shapes (gauge, funnel, box
// plot) whose drillable marks don't fall neatly on a single ring or grid.
let candidates: Array<{ x: number; y: number }>;
if (pattern === 'ring') candidates = ringPoints();
else if (pattern === 'grid') candidates = gridPoints();
else candidates = [...gridPoints(), ...ringPoints()];
// The submenu *title* element only exists when "Drill to detail by" is an
// enabled submenu (a real datum was hit); a miss renders a disabled item.
const enabledDrillBy = dashboard.drillBySubmenuTitle();
const contextMenu = page.locator('[data-test="chart-context-menu"]');
for (const pt of candidates) {
await canvas.click({ button: 'right', position: pt });
const hit = await enabledDrillBy
.waitFor({ state: 'visible', timeout: 400 })
.then(() => true)
.catch(() => false);
if (hit) return;
await page.keyboard.press('Escape');
// Wait for the portal to actually close before the next right-click;
// otherwise a still-open (or mid-close-animation) menu can make the
// next click/locator behave nondeterministically on slower/contended CI.
await contextMenu
.waitFor({ state: 'hidden', timeout: 400 })
.catch(() => {});
}
throw new Error(
`no drillable datum found on canvas after scanning ${candidates.length} points`,
);
}
/** A samples POST fired (proves the modal hit the real backend). */
function expectSamplesPost(page: Page) {
return page.waitForResponse(
r =>
r.url().includes('/datasource/samples') &&
r.request().method() === 'POST',
{ timeout: TIMEOUT.API_RESPONSE },
);
}
async function loadDashboardWithChart(
dashboard: DashboardPage,
dashboardId: number,
chartId: number,
): Promise<void> {
await dashboard.gotoById(dashboardId);
await dashboard.waitForLoad();
await dashboard
.getChart(chartId)
.locator('[data-test="chart-container"]')
.first()
.waitFor({ state: 'visible', timeout: TIMEOUT.QUERY_EXECUTION });
await dashboard.waitForChartsToLoad();
}
/**
* From an already-open "Drill to detail by" submenu, drill by the first
* offered value and assert that same value lands in the modal filter. The
* shared tail of every "drill by whatever value is under the cursor" test
* canvas charts and the pivot table alike, which differ only in how they open
* the submenu in the first place.
*/
async function drillByFirstOfferedValueAndAssert(
page: Page,
dashboard: DashboardPage,
): Promise<void> {
const offered = await dashboard.drillByOfferedValues();
expect(offered.length).toBeGreaterThan(0);
const [value] = offered;
const samples = expectSamplesPost(page);
await dashboard.contextMenuDrillToDetailBy(value);
await samples;
await expect(dashboard.drillModal().element).toBeVisible();
await expect(dashboard.drillModal().filterValues.first()).toContainText(
value,
);
}
/**
* Full canvas-drill round-trip for an echarts (canvas-rendered) chart: build a
* hermetic single-chart dashboard, render it, right-click a real datum, drill by
* whatever value the submenu offers under the cursor, and assert that same value
* lands in the modal filter. Geometry-independent see rightClickCanvasDatum.
* Reused across every canvas viz type so each migrated chart is a thin caller.
*/
async function expectCanvasDrillByValueRoundTrips(
page: Page,
testAssets: TestAssets,
testInfo: TestInfo,
spec: ChartSpec,
pattern: 'ring' | 'grid' | 'dense',
): Promise<void> {
const dashboard = new DashboardPage(page);
const { dashboardId, chartId } = await buildSingleChartDashboard(
page,
testAssets,
testInfo,
spec,
);
await loadDashboardWithChart(dashboard, dashboardId, chartId);
const canvas = dashboard.getChart(chartId).locator('canvas').first();
await expect(canvas).toBeVisible();
await rightClickCanvasDatum(page, dashboard, canvas, pattern);
await drillByFirstOfferedValueAndAssert(page, dashboard);
}
/**
* Right-click a big-number chart's rendered value to open its context menu,
* drill the whole chart (no row/point filter), and assert the modal opened
* with no filter tags and a real row count. Shared by Big Number and Big
* Number with Trendline, which differ only in their chart params.
*/
async function expectWholeChartDrillFromContextMenu(
page: Page,
dashboard: DashboardPage,
chartId: number,
): Promise<void> {
const samples = expectSamplesPost(page);
await dashboard
.getChart(chartId)
.locator('.header-line')
.click({ button: 'right' });
await dashboard.contextMenuDrillToDetail();
await samples;
await expect(dashboard.drillModal().element).toBeVisible();
// Whole-chart drill: no per-value filter tag.
await expect(dashboard.drillModal().filterValues).toHaveCount(0);
await expect(dashboard.drillModal().rowCountLabel).toContainText('rows');
}
// Shared form-data fragment for the echarts time-series family (line/scatter/
// generic/smooth/step): one temporal axis, one metric, split by gender series.
const TIMESERIES_PARAMS = {
x_axis: 'ds',
time_grain_sqla: 'P1Y',
metrics: ['count'],
groupby: ['gender'],
row_limit: 1000,
};
testWithAssets(
'drill-to-detail modal: opens from the header menu, paginates, and reload resets to page 1',
async ({ page, testAssets }) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
const dashboard = new DashboardPage(page);
const { dashboardId, chartId } = await buildSingleChartDashboard(
page,
testAssets,
testWithAssets.info(),
{
vizType: 'big_number_total',
chartNamePrefix: 'drill_bignum',
params: { metric: 'count', adhoc_filters: [] },
},
);
await loadDashboardWithChart(dashboard, dashboardId, chartId);
// Open the modal from the chart's "More Options" header menu.
const samplesOnOpen = expectSamplesPost(page);
await dashboard.openDrillToDetailFromMenu(chartId);
await samplesOnOpen;
const modal = dashboard.drillModal();
await expect(modal.element).toBeVisible();
await expect(modal.element).toContainText('Drill to detail:');
// The metadata bar and a real row count prove the modal loaded backend data.
await expect(modal.metadataBar).toBeVisible();
await expect(modal.rowCountLabel).toContainText('rows');
// No drill filter was applied (whole-chart drill).
await expect(modal.filterValues).toHaveCount(0);
// The full dataset spans multiple pages, and the grid has rendered rows.
expect(await modal.pageItems.count()).toBeGreaterThan(1);
await expect(modal.gridCells.first()).toBeVisible();
await expect(modal.activePageItem).toContainText('1');
// Paginate forward: clicking page 2 fires a real samples fetch and moves the
// active page to 2.
const samplesOnPage2 = expectSamplesPost(page);
await modal.goToPage(2);
await samplesOnPage2;
await expect(modal.activePageItem).toContainText('2');
// Reload re-fetches and resets back to the first page.
const samplesOnReload = expectSamplesPost(page);
await modal.reload();
await samplesOnReload;
await expect(modal.activePageItem).toContainText('1');
},
);
testWithAssets(
'drill-to-detail modal: big number value right-click drills the whole chart (no filter)',
async ({ page, testAssets }) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
const dashboard = new DashboardPage(page);
const { dashboardId, chartId } = await buildSingleChartDashboard(
page,
testAssets,
testWithAssets.info(),
{
vizType: 'big_number_total',
chartNamePrefix: 'drill_bignum_rc',
params: { metric: 'count', adhoc_filters: [] },
},
);
await loadDashboardWithChart(dashboard, dashboardId, chartId);
await expectWholeChartDrillFromContextMenu(page, dashboard, chartId);
},
);
testWithAssets(
'drill-to-detail modal: table cell right-click drills by that value and clearing the filter restores the full set',
async ({ page, testAssets }) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
const dashboard = new DashboardPage(page);
const { dashboardId, chartId } = await buildSingleChartDashboard(
page,
testAssets,
testWithAssets.info(),
{
vizType: 'table',
chartNamePrefix: 'drill_table',
params: {
query_mode: 'aggregate',
groupby: ['gender'],
metrics: ['count'],
row_limit: 100,
server_pagination: false,
},
},
);
await loadDashboardWithChart(dashboard, dashboardId, chartId);
// Right-click the "boy" dimension cell and drill by it.
const samplesOnDrill = expectSamplesPost(page);
await dashboard
.getChart(chartId)
.getByText('boy', { exact: true })
.first()
.click({ button: 'right' });
await dashboard.contextMenuDrillToDetailBy('boy');
await samplesOnDrill;
const modal = dashboard.drillModal();
await expect(modal.element).toBeVisible();
await expect(modal.filterValues.first()).toContainText('boy');
const filteredCount = parseRowCount(await modal.rowCountLabel.innerText());
expect(filteredCount).toBeGreaterThan(0);
// Clearing the filter reloads the samples and restores the larger, unfiltered total.
const samplesOnClear = expectSamplesPost(page);
await modal.clearFirstFilter();
await samplesOnClear;
await expect(modal.filterValues).toHaveCount(0);
await expect
.poll(async () => parseRowCount(await modal.rowCountLabel.innerText()))
.toBeGreaterThan(filteredCount);
},
);
testWithAssets(
'drill-to-detail modal: pivot table cell right-click drills by the cell value',
async ({ page, testAssets }) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
const dashboard = new DashboardPage(page);
const { dashboardId, chartId } = await buildSingleChartDashboard(
page,
testAssets,
testWithAssets.info(),
{
vizType: 'pivot_table_v2',
chartNamePrefix: 'drill_pivot',
params: {
groupbyRows: ['gender'],
groupbyColumns: [],
metrics: ['count'],
aggregateFunction: 'Sum',
rowTotals: false,
colTotals: false,
},
},
);
await loadDashboardWithChart(dashboard, dashboardId, chartId);
await dashboard
.getChart(chartId)
.locator('[role="gridcell"]')
.first()
.click({ button: 'right' });
// The cell's row dimension determines the offered value; drill by it and
// assert the same value lands in the modal filter.
await drillByFirstOfferedValueAndAssert(page, dashboard);
},
);
testWithAssets(
'drill-to-detail modal: pie slice right-click (canvas) drills by the slice value',
async ({ page, testAssets }) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
// Pie is a donut by default (center is a hole), so scan the ring for a slice.
await expectCanvasDrillByValueRoundTrips(
page,
testAssets,
testWithAssets.info(),
{
vizType: 'pie',
chartNamePrefix: 'drill_pie',
params: { groupby: ['gender'], metric: 'count' },
},
'ring',
);
},
);
testWithAssets(
'drill-to-detail modal: line chart point right-click (canvas) drills by the point value',
async ({ page, testAssets }) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
// Scan the plot grid for a point on one of the series lines.
await expectCanvasDrillByValueRoundTrips(
page,
testAssets,
testWithAssets.info(),
{
vizType: 'echarts_timeseries_line',
chartNamePrefix: 'drill_line',
params: TIMESERIES_PARAMS,
},
'grid',
);
},
);
testWithAssets(
'drill-to-detail modal: big number with trendline right-click drills the whole chart (no filter)',
async ({ page, testAssets }) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
const dashboard = new DashboardPage(page);
const { dashboardId, chartId } = await buildSingleChartDashboard(
page,
testAssets,
testWithAssets.info(),
{
vizType: 'big_number',
chartNamePrefix: 'drill_bignum_trend',
params: {
metric: 'count',
x_axis: 'ds',
time_grain_sqla: 'P1Y',
adhoc_filters: [],
},
},
);
await loadDashboardWithChart(dashboard, dashboardId, chartId);
await expectWholeChartDrillFromContextMenu(page, dashboard, chartId);
},
);
interface CanvasDrillCase {
title: string;
spec: ChartSpec;
pattern: 'ring' | 'grid' | 'dense';
}
// Every remaining canvas (echarts) chart is a thin caller of
// expectCanvasDrillByValueRoundTrips, differing only in viz type, chart
// params, and which point-scan pattern finds a drillable mark.
const CANVAS_DRILL_CASES: CanvasDrillCase[] = [
{
title:
'drill-to-detail modal: scatter chart point right-click (canvas) drills by the point value',
spec: {
vizType: 'echarts_timeseries_scatter',
chartNamePrefix: 'drill_scatter',
// Enlarge the markers so a region scan reliably lands on a point;
// scatter's default dots are a few pixels wide and a sparse grid misses
// them.
params: { ...TIMESERIES_PARAMS, markerSize: 20 },
},
pattern: 'dense',
},
{
title:
'drill-to-detail modal: generic time-series point right-click (canvas) drills by the point value',
spec: {
vizType: 'echarts_timeseries',
chartNamePrefix: 'drill_generic',
params: TIMESERIES_PARAMS,
},
pattern: 'grid',
},
{
title:
'drill-to-detail modal: smooth line point right-click (canvas) drills by the point value',
spec: {
vizType: 'echarts_timeseries_smooth',
chartNamePrefix: 'drill_smooth',
params: TIMESERIES_PARAMS,
},
pattern: 'grid',
},
{
title:
'drill-to-detail modal: step line point right-click (canvas) drills by the point value',
spec: {
vizType: 'echarts_timeseries_step',
chartNamePrefix: 'drill_step',
params: TIMESERIES_PARAMS,
},
pattern: 'grid',
},
{
title:
'drill-to-detail modal: mixed time-series point right-click (canvas) drills by the point value',
spec: {
vizType: 'mixed_timeseries',
chartNamePrefix: 'drill_mixed',
params: {
x_axis: 'ds',
time_grain_sqla: 'P1Y',
metrics: ['count'],
groupby: ['gender'],
metrics_b: ['count'],
groupby_b: ['gender'],
row_limit: 1000,
},
},
pattern: 'grid',
},
{
title:
'drill-to-detail modal: box plot right-click (canvas) drills by the box value',
spec: {
vizType: 'box_plot',
chartNamePrefix: 'drill_boxplot',
params: {
groupby: ['gender'],
metrics: ['count'],
columns: ['ds'],
},
},
pattern: 'dense',
},
{
title:
'drill-to-detail modal: funnel segment right-click (canvas) drills by the segment value',
spec: {
vizType: 'funnel',
chartNamePrefix: 'drill_funnel',
params: { groupby: ['gender'], metric: 'count' },
},
pattern: 'dense',
},
{
title:
'drill-to-detail modal: gauge right-click (canvas) drills by the gauge value',
spec: {
vizType: 'gauge_chart',
chartNamePrefix: 'drill_gauge',
params: { groupby: ['gender'], metric: 'count' },
},
pattern: 'dense',
},
{
title:
'drill-to-detail modal: treemap tile right-click (canvas) drills by the tile value',
spec: {
vizType: 'treemap_v2',
chartNamePrefix: 'drill_treemap',
params: { metric: 'count', groupby: ['gender'] },
},
pattern: 'dense',
},
];
for (const { title, spec, pattern } of CANVAS_DRILL_CASES) {
testWithAssets(title, async ({ page, testAssets }) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
await expectCanvasDrillByValueRoundTrips(
page,
testAssets,
testWithAssets.info(),
spec,
pattern,
);
});
}
testWithAssets(
'drill-to-detail modal: drilling a time-series point "by all" applies every dimension of that point',
async ({ page, testAssets }) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
const dashboard = new DashboardPage(page);
const { dashboardId, chartId } = await buildSingleChartDashboard(
page,
testAssets,
testWithAssets.info(),
{
vizType: 'echarts_timeseries_line',
chartNamePrefix: 'drill_all',
// Two groupby dimensions so each point genuinely carries more than one
// drillable value — the whole point of "Drill to detail by all".
params: { ...TIMESERIES_PARAMS, groupby: ['gender', 'state'] },
},
);
await loadDashboardWithChart(dashboard, dashboardId, chartId);
const canvas = dashboard.getChart(chartId).locator('canvas').first();
await expect(canvas).toBeVisible();
await rightClickCanvasDatum(page, dashboard, canvas, 'grid');
// A line point carries two dimensions (the temporal value and the gender
// series), so "Drill to detail by all" must apply both as filters.
const offered = await dashboard.drillByOfferedValues();
expect(offered.length).toBeGreaterThanOrEqual(2);
const samples = expectSamplesPost(page);
await dashboard.contextMenuDrillToDetailBy('all');
await samples;
await expect(dashboard.drillModal().element).toBeVisible();
expect(
await dashboard.drillModal().filterValues.count(),
).toBeGreaterThanOrEqual(2);
},
);
testWithAssets(
'drill-to-detail modal: table drills correctly by each of multiple dimension values',
async ({ page, testAssets }) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
const dashboard = new DashboardPage(page);
const { dashboardId, chartId } = await buildSingleChartDashboard(
page,
testAssets,
testWithAssets.info(),
{
vizType: 'table',
chartNamePrefix: 'drill_table_multi',
params: {
query_mode: 'aggregate',
groupby: ['gender'],
metrics: ['count'],
row_limit: 100,
server_pagination: false,
},
},
);
await loadDashboardWithChart(dashboard, dashboardId, chartId);
for (const value of ['boy', 'girl']) {
const samples = expectSamplesPost(page);
await dashboard
.getChart(chartId)
.getByText(value, { exact: true })
.first()
.click({ button: 'right' });
await dashboard.contextMenuDrillToDetailBy(value);
await samples;
const modal = dashboard.drillModal();
await expect(modal.element).toBeVisible();
await expect(modal.filterValues.first()).toContainText(value);
await modal.close();
}
},
);
@@ -1,58 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* With SOFT_DELETE enabled the delete-confirmation modal becomes recoverable:
* it explains the object is moved to the archive (and for how long), and drops
* the "type DELETE to confirm" friction. Non-destructive the modal is opened
* and dismissed without deleting anything.
*/
import { test, expect } from '@playwright/test';
import { skipUnlessFeatureEnabled } from '../../helpers/featureFlags';
test.beforeEach(async ({ page }) => {
await skipUnlessFeatureEnabled(page, 'SOFT_DELETE');
});
test('chart delete confirmation reflects soft-delete (archive) semantics', async ({
page,
}) => {
await page.goto('chart/list/');
await page.locator('[data-test="chart-row-delete"]').first().waitFor();
await page.locator('[data-test="chart-row-delete"]').first().click();
// The action reads as "Archive", not "Delete". Scope to the dialog: with
// the flag on, every list row's delete action is also named "Archive", so
// an unscoped button query is a strict-mode violation (25 rows + modal).
const dialog = page.getByRole('dialog');
await expect(dialog.getByText(/^Archive .+\?$/)).toBeVisible();
await expect(dialog.getByRole('button', { name: 'Archive' })).toBeVisible();
// Recoverable copy instead of "Are you sure … permanently".
await expect(page.getByText(/moved to Recently Archived/i)).toBeVisible();
await expect(
page.getByText(/recover it there within \d+ days/i),
).toBeVisible();
// No "type DELETE to confirm" input in recoverable mode.
await expect(page.getByTestId('delete-modal-input')).toHaveCount(0);
// Dismiss without deleting.
await page.getByTestId('close-modal-btn').click();
});
@@ -29,7 +29,7 @@
* restore it and asserts via the API that it is live again.
*/
import { test, expect, Page } from '@playwright/test';
import { apiGet, apiPost } from '../../helpers/api/requests';
import { apiGet } from '../../helpers/api/requests';
import { extractIdFromResponse } from '../../helpers/api/assertions';
import {
apiPostChart,
@@ -188,58 +188,3 @@ test('permanently deletes an archived item from the view', async ({ page }) => {
await TYPES[0].softDelete(page, id).catch(() => {});
}
});
test('shows an empty message and no rows when the search matches nothing', async ({
page,
}) => {
await page.goto('archived/');
await expect(page.getByTestId('archived-list-view')).toBeVisible();
const search = page.getByPlaceholder(/type a value/i);
await search.click();
await search.fill(`e2e_nonexistent_${Date.now()}`);
await search.press('Enter');
await expect(
page.getByText('No results match your filter criteria'),
).toBeVisible();
await expect(page.getByTestId('archived-row-restore')).toHaveCount(0);
});
test('restoring an already-restored row surfaces an error without crashing', async ({
page,
}) => {
const name = `e2e_stale_${Date.now()}`;
const id = await TYPES[0].create(page, name);
// Capture the uuid before soft-delete (a soft-deleted GET returns 404).
const { uuid } = (await (await apiGetDashboard(page, id)).json()).result;
try {
expect((await apiDeleteDashboard(page, id)).ok()).toBeTruthy();
await openArchive(page, 'Dashboard', name);
await expect(page.getByText(name, { exact: false })).toBeVisible();
// Simulate another actor restoring the object out from under this view.
const restored = await apiPost(
page,
`api/v1/dashboard/${uuid}/restore`,
{},
);
expect(restored.ok()).toBeTruthy();
// Clicking the now-stale row's Restore yields a 404 → danger toast, no crash.
await page
.getByRole('row')
.filter({ hasText: name })
.getByTestId('archived-row-restore')
.click();
await expect(
page.getByText(`Failed to restore ${name}`, { exact: false }),
).toBeVisible({ timeout: 15000 });
// The page is still functional (the list view did not crash).
await expect(page.getByTestId('archived-list-view')).toBeVisible();
} finally {
// Re-archive the (possibly) restored dashboard, whatever happened above.
await apiDeleteDashboard(page, id).catch(() => {});
}
});
@@ -40,10 +40,18 @@ import { testWithAssets, expect } from '../../helpers/fixtures';
import { apiGet } from '../../helpers/api/requests';
import { apiPostChart, apiPutChart } from '../../helpers/api/chart';
import { getDatasetByName } from '../../helpers/api/dataset';
import { getAccessToken } from '../../helpers/api/embedded';
import { TIMEOUT } from '../../utils/constants';
const DATASET_NAME = 'birth_names';
async function authorizeApi(page: Page): Promise<void> {
const accessToken = await getAccessToken(page);
await page.context().setExtraHTTPHeaders({
Authorization: `Bearer ${accessToken}`,
});
}
// Visible row text must never expose synthetic identifiers (layout node
// ids like CHART-xyz / ROW-… or bare UUIDs) — the rendering layer maps
// these to human names or kind-only phrasing.
@@ -79,19 +87,35 @@ async function currentUserSubjectId(page: Page): Promise<number> {
* this reads like its sibling specs, but fall back to whatever the instance
* has rather than requiring a particular fixture to be loaded.
*/
async function anyDatasetId(page: Page): Promise<number> {
async function anyDataset(page: Page): Promise<{
id: number;
columnName: string;
}> {
const named = await getDatasetByName(page, DATASET_NAME);
if (named) {
return named.id;
let datasetId = named?.id;
if (!datasetId) {
const res = await apiGet(
page,
`api/v1/dataset/?q=${rison.encode({ columns: ['id'], page_size: 1 })}`,
);
expect(res.ok(), 'dataset list request').toBeTruthy();
const [first] = (await res.json()).result;
expect(first, 'the instance has at least one dataset').toBeTruthy();
datasetId = first.id;
}
const res = await apiGet(
page,
`api/v1/dataset/?q=${rison.encode({ columns: ['id'], page_size: 1 })}`,
);
expect(res.ok(), 'dataset list request').toBeTruthy();
const [first] = (await res.json()).result;
expect(first, 'the instance has at least one dataset').toBeTruthy();
return first.id;
if (datasetId === undefined) {
throw new Error('Unable to resolve a dataset id');
}
const detailRes = await apiGet(page, `api/v1/dataset/${datasetId}`);
expect(detailRes.ok(), 'dataset detail request').toBeTruthy();
const { columns } = (await detailRes.json()).result;
const [firstColumn] = columns;
expect(firstColumn, 'the dataset has at least one column').toBeTruthy();
return {
id: datasetId,
columnName: firstColumn.column_name,
};
}
/** Open the Explore "Additional actions → View version history" panel. */
@@ -109,7 +133,8 @@ testWithAssets(
async ({ page, testAssets }) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
const datasetId = await anyDatasetId(page);
await authorizeApi(page);
const { id: datasetId, columnName } = await anyDataset(page);
const baseName = `version_history_${Date.now()}`;
const chartResp = await apiPostChart(page, {
@@ -123,7 +148,7 @@ testWithAssets(
datasource: `${datasetId}__table`,
viz_type: 'table',
query_mode: 'raw',
all_columns: [],
all_columns: [columnName],
adhoc_filters: [],
row_limit: 10,
}),
@@ -171,3 +196,79 @@ testWithAssets(
).toBeFalsy();
},
);
testWithAssets(
'minor edit of a non-canonical chart omits hydration noise',
async ({ page, testAssets }) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
await authorizeApi(page);
const { id: datasetId, columnName } = await anyDataset(page);
const baseName = `version_history_normalization_${Date.now()}`;
const chartResp = await apiPostChart(page, {
slice_name: baseName,
viz_type: 'table',
datasource_id: datasetId,
datasource_type: 'table',
// Deliberately omit visualization defaults. Explore hydration supplies
// them, reproducing params imported before they were canonical.
params: JSON.stringify({
datasource: `${datasetId}__table`,
viz_type: 'table',
query_mode: 'raw',
all_columns: [columnName],
adhoc_filters: [],
extra_form_data: {},
dashboards: [],
row_limit: 10,
}),
});
expect(chartResp.ok(), 'chart creation').toBeTruthy();
const chartBody = await chartResp.json();
const chartId: number = chartBody.result?.id ?? chartBody.id;
expect(chartId, 'chart creation should return an id').toBeTruthy();
testAssets.trackChart(chartId);
const adminSubjectId = await currentUserSubjectId(page);
const editorResp = await apiPutChart(page, chartId, {
editors: [adminSubjectId],
});
expect(editorResp.ok(), 'claim chart editorship').toBeTruthy();
await page.goto(`explore/?slice_id=${chartId}`);
await page.getByRole('combobox', { name: 'Row limit' }).click();
await page.getByRole('option', { name: '100', exact: true }).click();
await page.locator('[data-test="query-save-button"]').click();
await page.locator('[data-test="save-overwrite-radio"]').click();
const saveResponsePromise = page.waitForResponse(
response =>
response.request().method() === 'PUT' &&
response.url().includes(`/api/v1/chart/${chartId}`),
);
await page.locator('[data-test="btn-modal-save"]').click();
const saveResponse = await saveResponsePromise;
expect(saveResponse.ok(), 'chart overwrite').toBeTruthy();
const requestPayload = saveResponse.request().postDataJSON();
const savedParams = JSON.parse(requestPayload.params);
expect(
savedParams.matrixify_enable,
'overwrite contains a default absent from the stored params',
).toBe(false);
await openVersionHistory(page);
const panel = page.locator('[aria-label="Version history"]');
const newestGroup = panel
.locator('[data-test="version-history-save-group"]')
.first();
await expect(newestGroup, 'shows the overwrite save group').toBeVisible();
await newestGroup.getByRole('button').first().click();
const rows = newestGroup.locator(
'[data-test="version-history-action-row"]',
);
await expect(rows, 'shows only the intentional edit').toHaveCount(1);
await expect(rows.first()).toContainText(/row limit/i);
},
);
@@ -38,7 +38,7 @@ import {
getTotalsMetrics,
isTimeComparison,
timeCompareOperator,
TotalsAggregate,
toTotalsAggregate,
} from '@superset-ui/chart-controls';
import { isEmpty } from 'lodash-es';
import { TableChartFormData } from './types';
@@ -696,13 +696,16 @@ export const buildQueryUncached: BuildQuery<TableChartFormData> = (
formData.show_totals &&
queryMode === QueryMode.Aggregate,
);
const totalsAggregate: TotalsAggregate =
formData.totals_aggregate === 'AVG' ? 'AVG' : 'SUM';
const totalsAggregate = toTotalsAggregate(formData.totals_aggregate);
// Raw-mode summary columns have no metric of their own to preserve, so
// ORIGINAL has nothing to fall back to; sum them as before.
const rawSummaryAggregate =
totalsAggregate === 'ORIGINAL' ? 'SUM' : totalsAggregate;
const totalsMetrics =
rawSummaryColumns.length > 0
? rawSummaryColumns.map(columnName => ({
expressionType: 'SIMPLE' as const,
aggregate: totalsAggregate,
aggregate: rawSummaryAggregate,
column: { column_name: columnName },
label: columnName,
}))
@@ -503,14 +503,18 @@ const config: ControlPanelConfig = {
label: t('Summary aggregation'),
renderTrigger: true,
description: t(
'Aggregation used for the summary row, independent of each ' +
"metric's own aggregation. Only applies to simple metrics " +
'(a metric built from custom SQL keeps its own aggregation ' +
'in the summary row).',
'Aggregation used for the summary row. By default each metric ' +
'keeps its own aggregation; Sum and Average override it for ' +
'the summary row only. The override applies to simple ' +
'metrics (a metric built from custom SQL always keeps its ' +
'own aggregation). Overriding a count or a distinct count ' +
'sums the counted column instead, which fails outright on a ' +
'non-numeric column.',
),
default: 'SUM',
default: 'ORIGINAL',
clearable: false,
choices: [
['ORIGINAL', t("Each metric's own")],
['SUM', t('Sum')],
['AVG', t('Average')],
],
@@ -27,11 +27,21 @@ const StyledTotalCell = styled.div`
`}
`;
// `align` originates from the chart's stored column_config
// (col.config.horizontalAlign), which can be set to an arbitrary string via
// a direct chart-params API write. Emotion compiles interpolated strings as
// CSS source, so the value must be clamped to a closed set of keywords
// before it reaches the stylesheet — never interpolated raw.
const ALLOWED_ALIGN_VALUES = new Set(['left', 'right', 'center']);
const safeAlign = (align?: string) =>
align && ALLOWED_ALIGN_VALUES.has(align) ? align : 'left';
const CellContainer = styled.div<{ backgroundColor?: string; align?: string }>`
display: flex;
background-color: ${({ backgroundColor }) =>
backgroundColor || 'transparent'};
justify-content: ${({ align }) => align || 'left'};
justify-content: ${({ align }) => safeAlign(align)};
`;
const ArrowContainer = styled.div<{ arrowColor?: string }>`
@@ -0,0 +1,59 @@
/**
* 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 '@testing-library/jest-dom';
import { render } from '@superset-ui/core/spec';
import { NumericCellRenderer } from '../src/renderers/NumericCellRenderer';
const renderCell = (horizontalAlign?: string) => {
const params = {
value: 42,
valueFormatted: '42',
node: { rowPinned: undefined, rowIndex: 0 },
hasBasicColorFormatters: false,
basicColorFormatters: [],
col: {
isNumeric: true,
config: horizontalAlign ? { horizontalAlign } : {},
},
valueRange: undefined,
alignPositiveNegative: false,
colorPositiveNegative: false,
} as unknown as Parameters<typeof NumericCellRenderer>[0];
return render(<NumericCellRenderer {...params} />);
};
const collectInjectedCss = () =>
Array.from(document.querySelectorAll('style'))
.map(style => style.textContent ?? '')
.join('\n');
test('applies an allowed horizontalAlign value from column config', () => {
const { container } = renderCell('center');
expect(container.firstChild).toHaveStyle({ justifyContent: 'center' });
});
test('does not compile a malicious horizontalAlign into the stylesheet', () => {
const payload =
'right;} & { position:fixed; top:0; left:0; width:100vw; height:100vh; z-index:99999; background:#fff url(https://attacker.example/beacon) }';
const { container } = renderCell(payload);
const css = collectInjectedCss();
expect(css).not.toContain('position:fixed');
expect(css).not.toContain('attacker.example');
expect(container.firstChild).toHaveStyle({ justifyContent: 'left' });
});
@@ -1561,7 +1561,7 @@ describe('plugin-chart-ag-grid-table', () => {
expect(queries[1].metrics).toEqual(['count']);
});
test('defaults aggregate-mode totals to SUM for a simple metric', () => {
test("defaults aggregate-mode totals to the metric's own aggregate", () => {
const simpleMetric = {
expressionType: 'SIMPLE' as const,
column: { column_name: 'sales' },
@@ -1580,9 +1580,29 @@ describe('plugin-chart-ag-grid-table', () => {
{ ownState: {} },
);
expect(queries[1].metrics).toEqual([
{ ...simpleMetric, aggregate: 'SUM' },
]);
expect(queries[1].metrics).toEqual([simpleMetric]);
});
test('keeps COUNT_DISTINCT in aggregate-mode totals by default', () => {
const countDistinctMetric = {
expressionType: 'SIMPLE' as const,
column: { column_name: 'contract_id' },
aggregate: 'COUNT_DISTINCT' as const,
label: 'contracts',
};
const { queries } = buildQuery(
{
viz_type: VizType.Table,
datasource: '11__table',
query_mode: QueryMode.Aggregate,
groupby: ['state'],
metrics: [countDistinctMetric],
show_totals: true,
},
{ ownState: {} },
);
expect(queries[1].metrics).toEqual([countDistinctMetric]);
});
test('overrides aggregate-mode totals to AVG for a simple metric when totals_aggregate is set', () => {
@@ -38,3 +38,11 @@ export const convertUTCTimestampToLocal = (utcTimestamp: number): number => {
const offsetMs = date.getTimezoneOffset() * 60 * 1000;
return utcTimestamp + offsetMs;
};
// Escapes HTML special characters before formatter output reaches an
// innerHTML sink. Mirrors plugin-chart-country-map's escapeHtml.
export const escapeHtml = (text: unknown): string => {
const div = document.createElement('div');
div.textContent = String(text);
return div.innerHTML;
};
@@ -13,6 +13,7 @@ import d3tip from 'd3-tip';
import { t } from '@apache-superset/core/translation';
import { getContrastingColor } from '@superset-ui/core';
import { CALENDAR_TOOLTIP_CLASS } from '../tooltip';
import { escapeHtml } from '../utils';
var d3 = typeof require === 'function' ? require('d3') : window.d3;
@@ -22,14 +23,16 @@ var CalHeatMap = function () {
'use strict';
var self = this;
// d3-tip assigns the .html() return value to the tip node via
// innerHTML, so formatter output is HTML-escaped first.
self.tip = d3tip()
.attr('class', `d3-tip ${CALENDAR_TOOLTIP_CLASS}`)
.direction('n')
.offset([-5, 0])
.html(
d => `
${self.options.timeFormatter(d.t)}: <strong>${self.options.valueFormatter(
d.v,
${escapeHtml(self.options.timeFormatter(d.t))}: <strong>${escapeHtml(
self.options.valueFormatter(d.v),
)}</strong>
`,
);
@@ -37,7 +40,7 @@ var CalHeatMap = function () {
.attr('class', `d3-tip ${CALENDAR_TOOLTIP_CLASS}`)
.direction('n')
.offset([-5, 0])
.html(d => self.options.valueFormatter(d));
.html(d => escapeHtml(self.options.valueFormatter(d)));
this.allowedDataType = ['json', 'csv', 'tsv', 'txt'];
@@ -25,8 +25,12 @@ type FunctionalDateFormat = (date: Date) => string;
interface CalHeatMapInstance {
options: {
dateFormatter: DateFormatter | null;
timeFormatter: (t: number) => string;
valueFormatter: (v: number) => string;
};
formatDate(date: Date, format: string | FunctionalDateFormat): string;
tip: { html(): (d: { t: number; v: number }) => string };
legendTip: { html(): (d: number) => string };
}
const CalHeatMap = CalHeatMapImport as unknown as new () => CalHeatMapInstance;
@@ -59,3 +63,29 @@ test('CalHeatMap keeps the D3 formatter fallback', () => {
expect(calendar.formatDate(date, '%B')).toBe('January');
});
test('cell tooltip HTML escapes creator-controlled formatter output', () => {
// Regression test: the tip's .html() callback is assigned to the
// tooltip node via innerHTML (d3-tip), so formatter output must be
// escaped before it's returned.
const calendar = new CalHeatMap();
calendar.options.timeFormatter = () => '<img src=x onerror=alert(1)>';
calendar.options.valueFormatter = () => '<svg onload=alert(2)>';
const html = calendar.tip.html()({ t: 0, v: 1 });
expect(html).not.toContain('<img');
expect(html).not.toContain('<svg');
expect(html).toContain('&lt;img');
expect(html).toContain('&lt;svg');
});
test('legend tooltip HTML escapes creator-controlled formatter output', () => {
const calendar = new CalHeatMap();
calendar.options.valueFormatter = () => '<img src=x onerror=alert(1)>';
const html = calendar.legendTip.html()(1);
expect(html).not.toContain('<img');
expect(html).toContain('&lt;img');
});
@@ -17,7 +17,11 @@
* under the License.
*/
import { getFormattedUTCTime, convertUTCTimestampToLocal } from '../src/utils';
import {
getFormattedUTCTime,
convertUTCTimestampToLocal,
escapeHtml,
} from '../src/utils';
test('getFormattedUTCTime formats local timestamp for display as UTC date', () => {
const utcTimestamp = 1420070400000; // 2015-01-01 00:00:00 UTC
@@ -87,3 +91,22 @@ test('convertUTCTimestampToLocal and getFormattedUTCTime work together to displa
const formattedTime = getFormattedUTCTime(localTimestamp, '%Y-%m-%d');
expect(formattedTime).toContain('2024-01-01');
});
test('escapeHtml neutralizes markup smuggled through a time format string', () => {
// Regression test: d3-time-format passes non-% characters through
// verbatim, so escaping must happen before the innerHTML sink.
const formatted = getFormattedUTCTime(
1704067200000,
'%Y <img src=x onerror=alert(1)>',
);
const escaped = escapeHtml(formatted);
expect(formatted).toContain('<img');
expect(escaped).not.toContain('<img');
expect(escaped).toContain('&lt;img');
});
test('escapeHtml stringifies non-string formatter output safely', () => {
expect(escapeHtml(1234)).toEqual('1234');
expect(escapeHtml('a & b < c')).toEqual('a &amp; b &lt; c');
});
@@ -33,6 +33,27 @@ import { WmsLayerConf, WfsLayerConf, LayerConf, XyzLayerConf } from '../types';
import { isWfsLayerConf, isWmsLayerConf, isXyzLayerConf } from '../typeguards';
import { isVersionBelow } from './serviceUtil';
/**
* Escape HTML special characters in a layer attribution string.
*
* OpenLayers' Attribution control renders attribution strings via innerHTML,
* and the attribution here comes from creator-supplied chart form data, so it
* must be treated as untrusted text rather than markup to prevent stored XSS.
*
* @param attribution The attribution string from the layer configuration
*
* @returns The attribution with HTML special characters escaped
*/
export const escapeAttribution = (attribution?: string): string | undefined =>
attribution === undefined
? undefined
: attribution
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
/**
* Create a WMS layer.
*
@@ -49,7 +70,7 @@ export const createWmsLayer = (wmsLayerConf: WmsLayerConf) => {
LAYERS: layersParam,
VERSION: version,
},
attributions: attribution,
attributions: escapeAttribution(attribution),
}),
});
};
@@ -66,7 +87,7 @@ export const createXyzLayer = (xyzLayerConf: XyzLayerConf) => {
return new TileLayer({
source: new XyzSource({
url,
attributions: attribution,
attributions: escapeAttribution(attribution),
}),
});
};
@@ -90,7 +111,7 @@ export const createWfsLayer = async (wfsLayerConf: WfsLayerConf) => {
const wfsSource = new VectorSource({
format: new GeoJSON(),
attributions: attribution,
attributions: escapeAttribution(attribution),
url: extent => {
const requestUrl = new URL(url);
const params = requestUrl.searchParams;
@@ -17,20 +17,65 @@
* under the License.
*/
import { WfsLayerConf } from '../../src/types';
import { WfsLayerConf, WmsLayerConf, XyzLayerConf } from '../../src/types';
import {
createLayer,
createWfsLayer,
createWmsLayer,
createXyzLayer,
escapeAttribution,
} from '../../src/util/layerUtil';
describe('layerUtil', () => {
describe('escapeAttribution', () => {
test('escapes HTML markup in attribution strings', () => {
expect(escapeAttribution('(c) OSM <img src=x onerror=alert(1)>')).toBe(
'(c) OSM &lt;img src=x onerror=alert(1)&gt;',
);
expect(escapeAttribution('a & "b" \'c\'')).toBe(
'a &amp; &quot;b&quot; &#039;c&#039;',
);
expect(escapeAttribution(undefined)).toBeUndefined();
});
});
describe('createWmsLayer', () => {
test('exists', () => {
// function is trivial
expect(createWmsLayer).toBeDefined();
});
test('escapes HTML in the layer attribution', () => {
const wmsLayerConf: WmsLayerConf = {
title: 'wms',
type: 'WMS',
url: 'https://ows-demo.terrestris.de/geoserver/osm/wms',
version: '1.3.0',
layersParam: 'osm:osm-fuel',
attribution: '(c) OSM <img src=x onerror=alert(1)>',
};
const layer = createWmsLayer(wmsLayerConf);
const attributions = layer.getSource()?.getAttributions();
expect(attributions?.(undefined as never)).toEqual([
'(c) OSM &lt;img src=x onerror=alert(1)&gt;',
]);
});
});
describe('createXyzLayer', () => {
test('escapes HTML in the layer attribution', () => {
const xyzLayerConf: XyzLayerConf = {
title: 'osm',
type: 'XYZ',
url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '(c) OSM <img src=x onerror=alert(1)>',
};
const layer = createXyzLayer(xyzLayerConf);
const attributions = layer.getSource()?.getAttributions();
expect(attributions?.(undefined as never)).toEqual([
'(c) OSM &lt;img src=x onerror=alert(1)&gt;',
]);
});
});
describe('createWfsLayer', () => {
@@ -105,7 +105,7 @@
"source": [
"## Download Data\n",
"\n",
"Download datasets (_Admin 0 - Countries_ in [1:10](https://www.naturalearthdata.com/downloads/10m-cultural-vectors/), and _Admin 1 States, Provinces_ in 1:10 and [1:50](https://www.naturalearthdata.com/downloads/50m-cultural-vectors/)) from Natural Earch Data:"
"Download datasets (_Admin 0 - Countries_ in [1:10](https://www.naturalearthdata.com/downloads/10m-cultural-vectors/), and _Admin 1 \u2013 States, Provinces_ in 1:10 and [1:50](https://www.naturalearthdata.com/downloads/50m-cultural-vectors/)) from Natural Earch Data:"
]
},
{
@@ -584,7 +584,7 @@
" </tr>\n",
" </tbody>\n",
"</table>\n",
"<p>9 rows × 121 columns</p>\n",
"<p>9 rows \u00d7 121 columns</p>\n",
"</div>"
],
"text/plain": [
@@ -926,33 +926,33 @@
" <td>11.0</td>\n",
" <td>11.0</td>\n",
" <td>Q34617</td>\n",
" <td>سان بيير وميكلون</td>\n",
" <td>সাঁ পিয়ের ও মিকলোঁ</td>\n",
" <td>\u0633\u0627\u0646 \u0628\u064a\u064a\u0631 \u0648\u0645\u064a\u0643\u0644\u0648\u0646</td>\n",
" <td>\u09b8\u09be\u0981 \u09aa\u09bf\u09af\u09bc\u09c7\u09b0 \u0993 \u09ae\u09bf\u0995\u09b2\u09cb\u0981</td>\n",
" <td>Saint-Pierre und Miquelon</td>\n",
" <td>Saint Pierre and Miquelon</td>\n",
" <td>San Pedro y Miquelón</td>\n",
" <td>San Pedro y Miquel\u00f3n</td>\n",
" <td>Saint-Pierre-et-Miquelon</td>\n",
" <td>Σαιν-Πιερ και Μικελόν</td>\n",
" <td>सन्त पियर और मिकलान</td>\n",
" <td>Saint-Pierre és Miquelon</td>\n",
" <td>\u03a3\u03b1\u03b9\u03bd-\u03a0\u03b9\u03b5\u03c1 \u03ba\u03b1\u03b9 \u039c\u03b9\u03ba\u03b5\u03bb\u03cc\u03bd</td>\n",
" <td>\u0938\u0928\u094d\u0924 \u092a\u093f\u092f\u0930 \u0914\u0930 \u092e\u093f\u0915\u0932\u093e\u0928</td>\n",
" <td>Saint-Pierre \u00e9s Miquelon</td>\n",
" <td>Saint Pierre dan Miquelon</td>\n",
" <td>Saint-Pierre e Miquelon</td>\n",
" <td>サンピエール島・ミクロン島</td>\n",
" <td>생피에르 미클롱</td>\n",
" <td>\u30b5\u30f3\u30d4\u30a8\u30fc\u30eb\u5cf6\u30fb\u30df\u30af\u30ed\u30f3\u5cf6</td>\n",
" <td>\uc0dd\ud53c\uc5d0\ub974 \ubbf8\ud074\ub871</td>\n",
" <td>Saint-Pierre en Miquelon</td>\n",
" <td>Saint-Pierre i Miquelon</td>\n",
" <td>Saint-Pierre e Miquelon</td>\n",
" <td>Сен-Пьер и Микелон</td>\n",
" <td>\u0421\u0435\u043d-\u041f\u044c\u0435\u0440 \u0438 \u041c\u0438\u043a\u0435\u043b\u043e\u043d</td>\n",
" <td>Saint-Pierre och Miquelon</td>\n",
" <td>Saint Pierre ve Miquelon</td>\n",
" <td>Saint-Pierre và Miquelon</td>\n",
" <td>圣皮埃尔和密克隆</td>\n",
" <td>Saint-Pierre v\u00e0 Miquelon</td>\n",
" <td>\u5723\u76ae\u57c3\u5c14\u548c\u5bc6\u514b\u9686</td>\n",
" <td>1159315673</td>\n",
" <td>סן-פייר ומיקלון</td>\n",
" <td>Сен-П'єр і Мікелон</td>\n",
" <td>سینٹ پیئر و میکیلون</td>\n",
" <td>سن پیر و میکلن</td>\n",
" <td>聖皮埃與密克隆群島</td>\n",
" <td>\u05e1\u05df-\u05e4\u05d9\u05d9\u05e8 \u05d5\u05de\u05d9\u05e7\u05dc\u05d5\u05df</td>\n",
" <td>\u0421\u0435\u043d-\u041f'\u0454\u0440 \u0456 \u041c\u0456\u043a\u0435\u043b\u043e\u043d</td>\n",
" <td>\u0633\u06cc\u0646\u0679 \u067e\u06cc\u0626\u0631 \u0648 \u0645\u06cc\u06a9\u06cc\u0644\u0648\u0646</td>\n",
" <td>\u0633\u0646 \u067e\u06cc\u0631 \u0648 \u0645\u06cc\u06a9\u0644\u0646</td>\n",
" <td>\u8056\u76ae\u57c3\u8207\u5bc6\u514b\u9686\u7fa4\u5cf6</td>\n",
" <td>None</td>\n",
" <td>None</td>\n",
" <td>None</td>\n",
@@ -1051,33 +1051,33 @@
" <td>11.0</td>\n",
" <td>11.0</td>\n",
" <td>None</td>\n",
" <td>ميكلون ولانغليد</td>\n",
" <td>মিকুইলন-ল্যাংলেড</td>\n",
" <td>\u0645\u064a\u0643\u0644\u0648\u0646 \u0648\u0644\u0627\u0646\u063a\u0644\u064a\u062f</td>\n",
" <td>\u09ae\u09bf\u0995\u09c1\u0987\u09b2\u09a8-\u09b2\u09cd\u09af\u09be\u0982\u09b2\u09c7\u09a1</td>\n",
" <td>Miquelon-Langlade</td>\n",
" <td>Miquelon-Langlade</td>\n",
" <td>Miquelón-Langlade</td>\n",
" <td>Miquel\u00f3n-Langlade</td>\n",
" <td>Miquelon-Langlade</td>\n",
" <td>Μικελόν-Λαγκλέιντ</td>\n",
" <td>मिकेलॉन-लैंगलेड</td>\n",
" <td>\u039c\u03b9\u03ba\u03b5\u03bb\u03cc\u03bd-\u039b\u03b1\u03b3\u03ba\u03bb\u03ad\u03b9\u03bd\u03c4</td>\n",
" <td>\u092e\u093f\u0915\u0947\u0932\u0949\u0928-\u0932\u0948\u0902\u0917\u0932\u0947\u0921</td>\n",
" <td>Miquelon-Langlade</td>\n",
" <td>Miquelon-Langlade</td>\n",
" <td>Miquelon-Langlade</td>\n",
" <td>ミクロン=ラングラード</td>\n",
" <td>미클롱-랭글레이드</td>\n",
" <td>\u30df\u30af\u30ed\u30f3\uff1d\u30e9\u30f3\u30b0\u30e9\u30fc\u30c9</td>\n",
" <td>\ubbf8\ud074\ub871-\ub7ad\uae00\ub808\uc774\ub4dc</td>\n",
" <td>Miquelon-Langlade</td>\n",
" <td>Miquelon-Langlade</td>\n",
" <td>Miquelão-Langlade</td>\n",
" <td>Микелон-Ланглад</td>\n",
" <td>Miquel\u00e3o-Langlade</td>\n",
" <td>\u041c\u0438\u043a\u0435\u043b\u043e\u043d-\u041b\u0430\u043d\u0433\u043b\u0430\u0434</td>\n",
" <td>Miquelon-Langlade</td>\n",
" <td>Miquelon-Langlade</td>\n",
" <td>Miquelon-Langlade</td>\n",
" <td>密克隆-朗格拉德</td>\n",
" <td>\u5bc6\u514b\u9686-\u6717\u683c\u62c9\u5fb7</td>\n",
" <td>1159315961</td>\n",
" <td>מירה</td>\n",
" <td>Міквелон-Лангладе</td>\n",
" <td>میکیولون لینگلاڈے</td>\n",
" <td>میکوئلون-لانگلید</td>\n",
" <td>密克隆-朗格拉德</td>\n",
" <td>\u05de\u05d9\u05e8\u05d4</td>\n",
" <td>\u041c\u0456\u043a\u0432\u0435\u043b\u043e\u043d-\u041b\u0430\u043d\u0433\u043b\u0430\u0434\u0435</td>\n",
" <td>\u0645\u06cc\u06a9\u06cc\u0648\u0644\u0648\u0646 \u0644\u06cc\u0646\u06af\u0644\u0627\u0688\u06d2</td>\n",
" <td>\u0645\u06cc\u06a9\u0648\u0626\u0644\u0648\u0646-\u0644\u0627\u0646\u06af\u0644\u06cc\u062f</td>\n",
" <td>\u5bc6\u514b\u9686-\u6717\u683c\u62c9\u5fb7</td>\n",
" <td>None</td>\n",
" <td>None</td>\n",
" <td>None</td>\n",
@@ -1167,48 +1167,48 @@
"2177 PM.97501 None None 1.0 fra SB00 None \n",
"\n",
" min_label max_label min_zoom wikidataid name_ar \\\n",
"2176 11.0 11.0 11.0 Q34617 سان بيير وميكلون \n",
"2177 11.0 11.0 11.0 None ميكلون ولانغليد \n",
"2176 11.0 11.0 11.0 Q34617 \u0633\u0627\u0646 \u0628\u064a\u064a\u0631 \u0648\u0645\u064a\u0643\u0644\u0648\u0646 \n",
"2177 11.0 11.0 11.0 None \u0645\u064a\u0643\u0644\u0648\u0646 \u0648\u0644\u0627\u0646\u063a\u0644\u064a\u062f \n",
"\n",
" name_bn name_de \\\n",
"2176 সাঁ পিয়ের ও মিকলোঁ Saint-Pierre und Miquelon \n",
"2177 মিকুইলন-ল্যাংলেড Miquelon-Langlade \n",
"2176 \u09b8\u09be\u0981 \u09aa\u09bf\u09af\u09bc\u09c7\u09b0 \u0993 \u09ae\u09bf\u0995\u09b2\u09cb\u0981 Saint-Pierre und Miquelon \n",
"2177 \u09ae\u09bf\u0995\u09c1\u0987\u09b2\u09a8-\u09b2\u09cd\u09af\u09be\u0982\u09b2\u09c7\u09a1 Miquelon-Langlade \n",
"\n",
" name_en name_es \\\n",
"2176 Saint Pierre and Miquelon San Pedro y Miquelón \n",
"2177 Miquelon-Langlade Miquelón-Langlade \n",
"2176 Saint Pierre and Miquelon San Pedro y Miquel\u00f3n \n",
"2177 Miquelon-Langlade Miquel\u00f3n-Langlade \n",
"\n",
" name_fr name_el name_hi \\\n",
"2176 Saint-Pierre-et-Miquelon Σαιν-Πιερ και Μικελόν सन्त पियर और मिकलान \n",
"2177 Miquelon-Langlade Μικελόν-Λαγκλέιντ मिकेलॉन-लैंगलेड \n",
"2176 Saint-Pierre-et-Miquelon \u03a3\u03b1\u03b9\u03bd-\u03a0\u03b9\u03b5\u03c1 \u03ba\u03b1\u03b9 \u039c\u03b9\u03ba\u03b5\u03bb\u03cc\u03bd \u0938\u0928\u094d\u0924 \u092a\u093f\u092f\u0930 \u0914\u0930 \u092e\u093f\u0915\u0932\u093e\u0928 \n",
"2177 Miquelon-Langlade \u039c\u03b9\u03ba\u03b5\u03bb\u03cc\u03bd-\u039b\u03b1\u03b3\u03ba\u03bb\u03ad\u03b9\u03bd\u03c4 \u092e\u093f\u0915\u0947\u0932\u0949\u0928-\u0932\u0948\u0902\u0917\u0932\u0947\u0921 \n",
"\n",
" name_hu name_id \\\n",
"2176 Saint-Pierre és Miquelon Saint Pierre dan Miquelon \n",
"2176 Saint-Pierre \u00e9s Miquelon Saint Pierre dan Miquelon \n",
"2177 Miquelon-Langlade Miquelon-Langlade \n",
"\n",
" name_it name_ja name_ko \\\n",
"2176 Saint-Pierre e Miquelon サンピエール島・ミクロン島 생피에르 미클롱 \n",
"2177 Miquelon-Langlade ミクロン=ラングラード 미클롱-랭글레이드 \n",
"2176 Saint-Pierre e Miquelon \u30b5\u30f3\u30d4\u30a8\u30fc\u30eb\u5cf6\u30fb\u30df\u30af\u30ed\u30f3\u5cf6 \uc0dd\ud53c\uc5d0\ub974 \ubbf8\ud074\ub871 \n",
"2177 Miquelon-Langlade \u30df\u30af\u30ed\u30f3\uff1d\u30e9\u30f3\u30b0\u30e9\u30fc\u30c9 \ubbf8\ud074\ub871-\ub7ad\uae00\ub808\uc774\ub4dc \n",
"\n",
" name_nl name_pl \\\n",
"2176 Saint-Pierre en Miquelon Saint-Pierre i Miquelon \n",
"2177 Miquelon-Langlade Miquelon-Langlade \n",
"\n",
" name_pt name_ru name_sv \\\n",
"2176 Saint-Pierre e Miquelon Сен-Пьер и Микелон Saint-Pierre och Miquelon \n",
"2177 Miquelão-Langlade Микелон-Ланглад Miquelon-Langlade \n",
"2176 Saint-Pierre e Miquelon \u0421\u0435\u043d-\u041f\u044c\u0435\u0440 \u0438 \u041c\u0438\u043a\u0435\u043b\u043e\u043d Saint-Pierre och Miquelon \n",
"2177 Miquel\u00e3o-Langlade \u041c\u0438\u043a\u0435\u043b\u043e\u043d-\u041b\u0430\u043d\u0433\u043b\u0430\u0434 Miquelon-Langlade \n",
"\n",
" name_tr name_vi name_zh \\\n",
"2176 Saint Pierre ve Miquelon Saint-Pierre và Miquelon 圣皮埃尔和密克隆 \n",
"2177 Miquelon-Langlade Miquelon-Langlade 密克隆-朗格拉德 \n",
"2176 Saint Pierre ve Miquelon Saint-Pierre v\u00e0 Miquelon \u5723\u76ae\u57c3\u5c14\u548c\u5bc6\u514b\u9686 \n",
"2177 Miquelon-Langlade Miquelon-Langlade \u5bc6\u514b\u9686-\u6717\u683c\u62c9\u5fb7 \n",
"\n",
" ne_id name_he name_uk name_ur \\\n",
"2176 1159315673 סן-פייר ומיקלון Сен-П'єр і Мікелон سینٹ پیئر و میکیلون \n",
"2177 1159315961 מירה Міквелон-Лангладе میکیولون لینگلاڈے \n",
"2176 1159315673 \u05e1\u05df-\u05e4\u05d9\u05d9\u05e8 \u05d5\u05de\u05d9\u05e7\u05dc\u05d5\u05df \u0421\u0435\u043d-\u041f'\u0454\u0440 \u0456 \u041c\u0456\u043a\u0435\u043b\u043e\u043d \u0633\u06cc\u0646\u0679 \u067e\u06cc\u0626\u0631 \u0648 \u0645\u06cc\u06a9\u06cc\u0644\u0648\u0646 \n",
"2177 1159315961 \u05de\u05d9\u05e8\u05d4 \u041c\u0456\u043a\u0432\u0435\u043b\u043e\u043d-\u041b\u0430\u043d\u0433\u043b\u0430\u0434\u0435 \u0645\u06cc\u06a9\u06cc\u0648\u0644\u0648\u0646 \u0644\u06cc\u0646\u06af\u0644\u0627\u0688\u06d2 \n",
"\n",
" name_fa name_zht FCLASS_ISO FCLASS_US FCLASS_FR FCLASS_RU \\\n",
"2176 سن پیر و میکلن 聖皮埃與密克隆群島 None None None None \n",
"2177 میکوئلون-لانگلید 密克隆-朗格拉德 None None None None \n",
"2176 \u0633\u0646 \u067e\u06cc\u0631 \u0648 \u0645\u06cc\u06a9\u0644\u0646 \u8056\u76ae\u57c3\u8207\u5bc6\u514b\u9686\u7fa4\u5cf6 None None None None \n",
"2177 \u0645\u06cc\u06a9\u0648\u0626\u0644\u0648\u0646-\u0644\u0627\u0646\u06af\u0644\u06cc\u062f \u5bc6\u514b\u9686-\u6717\u683c\u62c9\u5fb7 None None None None \n",
"\n",
" FCLASS_ES FCLASS_CN FCLASS_TW FCLASS_IN FCLASS_NP FCLASS_PK FCLASS_DE \\\n",
"2176 None None None None None None None \n",
@@ -1330,7 +1330,7 @@
" 'costa rica',\n",
" 'croatia',\n",
" 'cuba',\n",
" 'curaçao',\n",
" 'cura\u00e7ao',\n",
" 'cyprus',\n",
" 'czech republic',\n",
" 'denmark',\n",
@@ -1343,7 +1343,7 @@
" 'equatorial guinea',\n",
" 'eritrea',\n",
" 'estonia',\n",
" # 'eswatini', # not sure why this doesn't work Swaziland isn't available to alias, either.\n",
" # 'eswatini', # not sure why this doesn't work \u2014 Swaziland isn't available to alias, either.\n",
" 'ethiopia',\n",
" 'falkland islands',\n",
" 'faroe islands',\n",
@@ -1443,7 +1443,7 @@
" 'portugal',\n",
" 'puerto rico',\n",
" 'qatar',\n",
" # 'réunion', # part of France, in Natural Earth data\n",
" # 'r\u00e9union', # part of France, in Natural Earth data\n",
" 'republic of serbia',\n",
" 'romania',\n",
" 'russia',\n",
@@ -1911,34 +1911,34 @@
" <td>9.0</td>\n",
" <td>1159320473</td>\n",
" <td>Q8646</td>\n",
" <td>هونغ كونغ</td>\n",
" <td>হংকং</td>\n",
" <td>\u0647\u0648\u0646\u063a \u0643\u0648\u0646\u063a</td>\n",
" <td>\u09b9\u0982\u0995\u0982</td>\n",
" <td>Hongkong</td>\n",
" <td>Hong Kong</td>\n",
" <td>Hong Kong</td>\n",
" <td>هنگ کنگ</td>\n",
" <td>\u0647\u0646\u06af \u06a9\u0646\u06af</td>\n",
" <td>Hong Kong</td>\n",
" <td>Χονγκ Κονγκ</td>\n",
" <td>הונג קונג</td>\n",
" <td>हांगकांग</td>\n",
" <td>\u03a7\u03bf\u03bd\u03b3\u03ba \u039a\u03bf\u03bd\u03b3\u03ba</td>\n",
" <td>\u05d4\u05d5\u05e0\u05d2 \u05e7\u05d5\u05e0\u05d2</td>\n",
" <td>\u0939\u093e\u0902\u0917\u0915\u093e\u0902\u0917</td>\n",
" <td>Hongkong</td>\n",
" <td>Hong Kong</td>\n",
" <td>Hong Kong</td>\n",
" <td>香港</td>\n",
" <td>홍콩</td>\n",
" <td>\u9999\u6e2f</td>\n",
" <td>\ud64d\ucf69</td>\n",
" <td>Hongkong</td>\n",
" <td>Hongkong</td>\n",
" <td>Hong Kong</td>\n",
" <td>Гонконг</td>\n",
" <td>\u0413\u043e\u043d\u043a\u043e\u043d\u0433</td>\n",
" <td>Hongkong</td>\n",
" <td>Hong Kong</td>\n",
" <td>Гонконг</td>\n",
" <td>ہانگ کانگ</td>\n",
" <td>Hồng Kông</td>\n",
" <td>香港</td>\n",
" <td>香港</td>\n",
" <td>\u0413\u043e\u043d\u043a\u043e\u043d\u0433</td>\n",
" <td>\u06c1\u0627\u0646\u06af \u06a9\u0627\u0646\u06af</td>\n",
" <td>H\u1ed3ng K\u00f4ng</td>\n",
" <td>\u9999\u6e2f</td>\n",
" <td>\u9999\u6e2f</td>\n",
" <td>MULTIPOLYGON (((114.22983 22.55581, 114.23471 ...</td>\n",
" <td>香港特别行政区</td>\n",
" <td>\u9999\u6e2f\u7279\u522b\u884c\u653f\u533a</td>\n",
" <td>CN-91</td>\n",
" </tr>\n",
" <tr>\n",
@@ -1965,34 +1965,34 @@
" <td>8.0</td>\n",
" <td>1159321335</td>\n",
" <td>Q865</td>\n",
" <td>تايوان</td>\n",
" <td>তাইওয়ান</td>\n",
" <td>\u062a\u0627\u064a\u0648\u0627\u0646</td>\n",
" <td>\u09a4\u09be\u0987\u0993\u09af\u09bc\u09be\u09a8</td>\n",
" <td>Republik China</td>\n",
" <td>Taiwan</td>\n",
" <td>República de China</td>\n",
" <td>تایوان</td>\n",
" <td>Taïwan</td>\n",
" <td>Δημοκρατία της Κίνας</td>\n",
" <td>טאיוואן</td>\n",
" <td>चीनी गणराज्य</td>\n",
" <td>Kínai Köztársaság</td>\n",
" <td>Rep\u00fablica de China</td>\n",
" <td>\u062a\u0627\u06cc\u0648\u0627\u0646</td>\n",
" <td>Ta\u00efwan</td>\n",
" <td>\u0394\u03b7\u03bc\u03bf\u03ba\u03c1\u03b1\u03c4\u03af\u03b1 \u03c4\u03b7\u03c2 \u039a\u03af\u03bd\u03b1\u03c2</td>\n",
" <td>\u05d8\u05d0\u05d9\u05d5\u05d5\u05d0\u05df</td>\n",
" <td>\u091a\u0940\u0928\u0940 \u0917\u0923\u0930\u093e\u091c\u094d\u092f</td>\n",
" <td>K\u00ednai K\u00f6zt\u00e1rsas\u00e1g</td>\n",
" <td>Taiwan</td>\n",
" <td>Taiwan</td>\n",
" <td>中華民国</td>\n",
" <td>중화민국</td>\n",
" <td>\u4e2d\u83ef\u6c11\u56fd</td>\n",
" <td>\uc911\ud654\ubbfc\uad6d</td>\n",
" <td>Taiwan</td>\n",
" <td>Republika Chińska</td>\n",
" <td>Republika Chi\u0144ska</td>\n",
" <td>Taiwan</td>\n",
" <td>Тайвань</td>\n",
" <td>\u0422\u0430\u0439\u0432\u0430\u043d\u044c</td>\n",
" <td>Taiwan</td>\n",
" <td>Çin Cumhuriyeti</td>\n",
" <td>Республіка Китай</td>\n",
" <td>تائیوان</td>\n",
" <td>Đài Loan</td>\n",
" <td>中华民国</td>\n",
" <td>中華民國</td>\n",
" <td>\u00c7in Cumhuriyeti</td>\n",
" <td>\u0420\u0435\u0441\u043f\u0443\u0431\u043b\u0456\u043a\u0430 \u041a\u0438\u0442\u0430\u0439</td>\n",
" <td>\u062a\u0627\u0626\u06cc\u0648\u0627\u0646</td>\n",
" <td>\u0110\u00e0i Loan</td>\n",
" <td>\u4e2d\u534e\u6c11\u56fd</td>\n",
" <td>\u4e2d\u83ef\u6c11\u570b</td>\n",
" <td>MULTIPOLYGON (((121.90577 24.9501, 121.83473 2...</td>\n",
" <td>中国台湾</td>\n",
" <td>\u4e2d\u56fd\u53f0\u6e7e</td>\n",
" <td>CN-71</td>\n",
" </tr>\n",
" <tr>\n",
@@ -2019,34 +2019,34 @@
" <td>9.0</td>\n",
" <td>1159320475</td>\n",
" <td>Q14773</td>\n",
" <td>ماكاو</td>\n",
" <td>মাকাও</td>\n",
" <td>\u0645\u0627\u0643\u0627\u0648</td>\n",
" <td>\u09ae\u09be\u0995\u09be\u0993</td>\n",
" <td>Macau</td>\n",
" <td>Macau</td>\n",
" <td>Macao</td>\n",
" <td>ماکائو</td>\n",
" <td>\u0645\u0627\u06a9\u0627\u0626\u0648</td>\n",
" <td>Macao</td>\n",
" <td>Μακάου</td>\n",
" <td>מקאו</td>\n",
" <td>मकाउ</td>\n",
" <td>Makaó</td>\n",
" <td>\u039c\u03b1\u03ba\u03ac\u03bf\u03c5</td>\n",
" <td>\u05de\u05e7\u05d0\u05d5</td>\n",
" <td>\u092e\u0915\u093e\u0909</td>\n",
" <td>Maka\u00f3</td>\n",
" <td>Makau</td>\n",
" <td>Macao</td>\n",
" <td>マカオ</td>\n",
" <td>마카오</td>\n",
" <td>\u30de\u30ab\u30aa</td>\n",
" <td>\ub9c8\uce74\uc624</td>\n",
" <td>Macau</td>\n",
" <td>Makau</td>\n",
" <td>Macau</td>\n",
" <td>Макао</td>\n",
" <td>\u041c\u0430\u043a\u0430\u043e</td>\n",
" <td>Macao</td>\n",
" <td>Makao</td>\n",
" <td>Аоминь</td>\n",
" <td>مکاؤ</td>\n",
" <td>\u0410\u043e\u043c\u0438\u043d\u044c</td>\n",
" <td>\u0645\u06a9\u0627\u0624</td>\n",
" <td>Ma Cao</td>\n",
" <td>澳门</td>\n",
" <td>澳門</td>\n",
" <td>\u6fb3\u95e8</td>\n",
" <td>\u6fb3\u9580</td>\n",
" <td>MULTIPOLYGON (((113.5586 22.16303, 113.56943 2...</td>\n",
" <td>澳门特别行政区</td>\n",
" <td>\u6fb3\u95e8\u7279\u522b\u884c\u653f\u533a</td>\n",
" <td>CN-92</td>\n",
" </tr>\n",
" </tbody>\n",
@@ -2070,34 +2070,34 @@
"2 4 3 MO 20070017 5 0.0 4.0 \n",
"\n",
" max_label ne_id wikidataid name_ar name_bn name_de \\\n",
"0 9.0 1159320473 Q8646 هونغ كونغ হংকং Hongkong \n",
"1 8.0 1159321335 Q865 تايوان তাইওয়ান Republik China \n",
"2 9.0 1159320475 Q14773 ماكاو মাকাও Macau \n",
"0 9.0 1159320473 Q8646 \u0647\u0648\u0646\u063a \u0643\u0648\u0646\u063a \u09b9\u0982\u0995\u0982 Hongkong \n",
"1 8.0 1159321335 Q865 \u062a\u0627\u064a\u0648\u0627\u0646 \u09a4\u09be\u0987\u0993\u09af\u09bc\u09be\u09a8 Republik China \n",
"2 9.0 1159320475 Q14773 \u0645\u0627\u0643\u0627\u0648 \u09ae\u09be\u0995\u09be\u0993 Macau \n",
"\n",
" name_en name_es name_fa name_fr name_el \\\n",
"0 Hong Kong Hong Kong هنگ کنگ Hong Kong Χονγκ Κονγκ \n",
"1 Taiwan República de China تایوان Taïwan Δημοκρατία της Κίνας \n",
"2 Macau Macao ماکائو Macao Μακάου \n",
"0 Hong Kong Hong Kong \u0647\u0646\u06af \u06a9\u0646\u06af Hong Kong \u03a7\u03bf\u03bd\u03b3\u03ba \u039a\u03bf\u03bd\u03b3\u03ba \n",
"1 Taiwan Rep\u00fablica de China \u062a\u0627\u06cc\u0648\u0627\u0646 Ta\u00efwan \u0394\u03b7\u03bc\u03bf\u03ba\u03c1\u03b1\u03c4\u03af\u03b1 \u03c4\u03b7\u03c2 \u039a\u03af\u03bd\u03b1\u03c2 \n",
"2 Macau Macao \u0645\u0627\u06a9\u0627\u0626\u0648 Macao \u039c\u03b1\u03ba\u03ac\u03bf\u03c5 \n",
"\n",
" name_he name_hi name_hu name_id name_it name_ja \\\n",
"0 הונג קונג हांगकांग Hongkong Hong Kong Hong Kong 香港 \n",
"1 טאיוואן चीनी गणराज्य Kínai Köztársaság Taiwan Taiwan 中華民国 \n",
"2 מקאו मकाउ Makaó Makau Macao マカオ \n",
"0 \u05d4\u05d5\u05e0\u05d2 \u05e7\u05d5\u05e0\u05d2 \u0939\u093e\u0902\u0917\u0915\u093e\u0902\u0917 Hongkong Hong Kong Hong Kong \u9999\u6e2f \n",
"1 \u05d8\u05d0\u05d9\u05d5\u05d5\u05d0\u05df \u091a\u0940\u0928\u0940 \u0917\u0923\u0930\u093e\u091c\u094d\u092f K\u00ednai K\u00f6zt\u00e1rsas\u00e1g Taiwan Taiwan \u4e2d\u83ef\u6c11\u56fd \n",
"2 \u05de\u05e7\u05d0\u05d5 \u092e\u0915\u093e\u0909 Maka\u00f3 Makau Macao \u30de\u30ab\u30aa \n",
"\n",
" name_ko name_nl name_pl name_pt name_ru name_sv \\\n",
"0 홍콩 Hongkong Hongkong Hong Kong Гонконг Hongkong \n",
"1 중화민국 Taiwan Republika Chińska Taiwan Тайвань Taiwan \n",
"2 마카오 Macau Makau Macau Макао Macao \n",
"0 \ud64d\ucf69 Hongkong Hongkong Hong Kong \u0413\u043e\u043d\u043a\u043e\u043d\u0433 Hongkong \n",
"1 \uc911\ud654\ubbfc\uad6d Taiwan Republika Chi\u0144ska Taiwan \u0422\u0430\u0439\u0432\u0430\u043d\u044c Taiwan \n",
"2 \ub9c8\uce74\uc624 Macau Makau Macau \u041c\u0430\u043a\u0430\u043e Macao \n",
"\n",
" name_tr name_uk name_ur name_vi name_zh_x name_zht \\\n",
"0 Hong Kong Гонконг ہانگ کانگ Hồng Kông 香港 香港 \n",
"1 Çin Cumhuriyeti Республіка Китай تائیوان Đài Loan 中华民国 中華民國 \n",
"2 Makao Аоминь مکاؤ Ma Cao 澳门 澳門 \n",
"0 Hong Kong \u0413\u043e\u043d\u043a\u043e\u043d\u0433 \u06c1\u0627\u0646\u06af \u06a9\u0627\u0646\u06af H\u1ed3ng K\u00f4ng \u9999\u6e2f \u9999\u6e2f \n",
"1 \u00c7in Cumhuriyeti \u0420\u0435\u0441\u043f\u0443\u0431\u043b\u0456\u043a\u0430 \u041a\u0438\u0442\u0430\u0439 \u062a\u0627\u0626\u06cc\u0648\u0627\u0646 \u0110\u00e0i Loan \u4e2d\u534e\u6c11\u56fd \u4e2d\u83ef\u6c11\u570b \n",
"2 Makao \u0410\u043e\u043c\u0438\u043d\u044c \u0645\u06a9\u0627\u0624 Ma Cao \u6fb3\u95e8 \u6fb3\u9580 \n",
"\n",
" geometry name_zh_y iso_3166_2 \n",
"0 MULTIPOLYGON (((114.22983 22.55581, 114.23471 ... 香港特别行政区 CN-91 \n",
"1 MULTIPOLYGON (((121.90577 24.9501, 121.83473 2... 中国台湾 CN-71 \n",
"2 MULTIPOLYGON (((113.5586 22.16303, 113.56943 2... 澳门特别行政区 CN-92 "
"0 MULTIPOLYGON (((114.22983 22.55581, 114.23471 ... \u9999\u6e2f\u7279\u522b\u884c\u653f\u533a CN-91 \n",
"1 MULTIPOLYGON (((121.90577 24.9501, 121.83473 2... \u4e2d\u56fd\u53f0\u6e7e CN-71 \n",
"2 MULTIPOLYGON (((113.5586 22.16303, 113.56943 2... \u6fb3\u95e8\u7279\u522b\u884c\u653f\u533a CN-92 "
]
},
"execution_count": 14,
@@ -2114,7 +2114,7 @@
"china_sars = china_sars.merge(pd.DataFrame(\n",
" data={\n",
" \"name_en\": [\"Taiwan\", \"Hong Kong\", \"Macau\"],\n",
" \"name_zh\": [\"中国台湾\", \"香港特别行政区\", \"澳门特别行政区\"],\n",
" \"name_zh\": [\"\u4e2d\u56fd\u53f0\u6e7e\", \"\u9999\u6e2f\u7279\u522b\u884c\u653f\u533a\", \"\u6fb3\u95e8\u7279\u522b\u884c\u653f\u533a\"],\n",
" \"iso_3166_2\": [\"CN-71\", \"CN-91\", \"CN-92\"],\n",
" },\n",
"), on=\"name_en\", how=\"left\")\n",
@@ -2252,7 +2252,7 @@
" }\n",
")[[\"geometry\", \"iso_3166_2\", \"name\"]].copy()\n",
"\n",
"# Convert MA01 MA-01\n",
"# Convert MA01 \u2192 MA-01\n",
"morocco_copy[\"iso_3166_2\"] = morocco_copy[\n",
" \"iso_3166_2\"\n",
"].str.replace(\n",
@@ -2290,7 +2290,7 @@
"source": [
"#### Finland\n",
"\n",
"- The Åland Islands (ISO country code AX) is an autonomous region of Finland, and carries the ISO-3166 code FI-01."
"- The \u00c5land Islands (ISO country code AX) is an autonomous region of Finland, and carries the ISO-3166 code FI-01."
]
},
{
@@ -2312,12 +2312,12 @@
"outputs": [],
"source": [
"finland_aland = df_admin0_10m.loc[\n",
" df_admin0_10m.name_en.isin(['Åland']),\n",
" df_admin0_10m.name_en.isin(['\u00c5land']),\n",
" [x for x in df_admin0_10m.columns if x in df.columns]\n",
"]\n",
"finland_aland = finland_aland.merge(pd.DataFrame(\n",
" data={\n",
" \"name_en\": [\"Åland\"],\n",
" \"name_en\": [\"\u00c5land\"],\n",
" \"name_fi\": [\"Ahvenanmaan maakunta\"],\n",
" \"iso_3166_2\": [\"FI-01\"],\n",
" },\n",
@@ -3197,34 +3197,34 @@
"\n",
"# Turkey city name corrections\n",
"# Fix completely wrong spellings\n",
"replace_column('name', turkey, 'Kinkkale', 'Kırıkkale')\n",
"replace_column('name', turkey, 'Kinkkale', 'K\u0131r\u0131kkale')\n",
"replace_column('name', turkey, 'Zinguldak', 'Zonguldak')\n",
"replace_column('name', turkey, 'K. Maras', 'Kahramanmaraş')\n",
"replace_column('name', turkey, 'K. Maras', 'Kahramanmara\u015f')\n",
"\n",
"# Fix missing Turkish characters\n",
"replace_column('name', turkey, 'Adiyaman', 'Adıyaman')\n",
"replace_column('name', turkey, 'Agri', 'Ağrı')\n",
"replace_column('name', turkey, 'Aydin', 'Aydın')\n",
"replace_column('name', turkey, 'Balikesir', 'Balıkesir')\n",
"replace_column('name', turkey, 'Çankiri', 'Çankırı')\n",
"replace_column('name', turkey, 'Diyarbakir', 'Diyarbakır')\n",
"replace_column('name', turkey, 'Elazig', 'Elâzığ')\n",
"replace_column('name', turkey, 'Eskisehir', 'Eskişehir')\n",
"replace_column('name', turkey, 'Gümüshane', 'Gümüşhane')\n",
"replace_column('name', turkey, 'Hakkari', 'Hakkâri')\n",
"replace_column('name', turkey, 'Istanbul', 'İstanbul')\n",
"replace_column('name', turkey, 'Izmir', 'İzmir')\n",
"replace_column('name', turkey, 'Iğdir', 'Iğdır')\n",
"replace_column('name', turkey, 'Kirklareli', 'Kırklareli')\n",
"replace_column('name', turkey, 'Kirsehir', 'Kıehir')\n",
"replace_column('name', turkey, 'Mugla', 'Muğla')\n",
"replace_column('name', turkey, 'Mus', 'Muş')\n",
"replace_column('name', turkey, 'Nevsehir', 'Nevşehir')\n",
"replace_column('name', turkey, 'Nigde', 'Niğde')\n",
"replace_column('name', turkey, 'Sanliurfa', 'Şanlıurfa')\n",
"replace_column('name', turkey, 'Sirnak', 'Şırnak')\n",
"replace_column('name', turkey, 'Tekirdag', 'Tekirdağ')\n",
"replace_column('name', turkey, 'Usak', 'Uşak')\n",
"replace_column('name', turkey, 'Adiyaman', 'Ad\u0131yaman')\n",
"replace_column('name', turkey, 'Agri', 'A\u011fr\u0131')\n",
"replace_column('name', turkey, 'Aydin', 'Ayd\u0131n')\n",
"replace_column('name', turkey, 'Balikesir', 'Bal\u0131kesir')\n",
"replace_column('name', turkey, '\u00c7ankiri', '\u00c7ank\u0131r\u0131')\n",
"replace_column('name', turkey, 'Diyarbakir', 'Diyarbak\u0131r')\n",
"replace_column('name', turkey, 'Elazig', 'El\u00e2z\u0131\u011f')\n",
"replace_column('name', turkey, 'Eskisehir', 'Eski\u015fehir')\n",
"replace_column('name', turkey, 'G\u00fcm\u00fcshane', 'G\u00fcm\u00fc\u015fhane')\n",
"replace_column('name', turkey, 'Hakkari', 'Hakk\u00e2ri')\n",
"replace_column('name', turkey, 'Istanbul', '\u0130stanbul')\n",
"replace_column('name', turkey, 'Izmir', '\u0130zmir')\n",
"replace_column('name', turkey, 'I\u011fdir', 'I\u011fd\u0131r')\n",
"replace_column('name', turkey, 'Kirklareli', 'K\u0131rklareli')\n",
"replace_column('name', turkey, 'Kirsehir', 'K\u0131r\u015fehir')\n",
"replace_column('name', turkey, 'Mugla', 'Mu\u011fla')\n",
"replace_column('name', turkey, 'Mus', 'Mu\u015f')\n",
"replace_column('name', turkey, 'Nevsehir', 'Nev\u015fehir')\n",
"replace_column('name', turkey, 'Nigde', 'Ni\u011fde')\n",
"replace_column('name', turkey, 'Sanliurfa', '\u015eanl\u0131urfa')\n",
"replace_column('name', turkey, 'Sirnak', '\u015e\u0131rnak')\n",
"replace_column('name', turkey, 'Tekirdag', 'Tekirda\u011f')\n",
"replace_column('name', turkey, 'Usak', 'U\u015fak')\n",
"turkey_copy = turkey.copy()"
]
},
@@ -3263,18 +3263,18 @@
"\n",
"# Region names corresponding to NUTS-1\n",
"\n",
"region_name_dict = {'TR1':'İstanbul',\n",
" 'TR2':'Batı Marmara',\n",
"region_name_dict = {'TR1':'\u0130stanbul',\n",
" 'TR2':'Bat\u0131 Marmara',\n",
" 'TR3':'Ege',\n",
" 'TR4':'Doğu Marmara',\n",
" 'TR5':'Batı Anadolu',\n",
" 'TR4':'Do\u011fu Marmara',\n",
" 'TR5':'Bat\u0131 Anadolu',\n",
" 'TR6':'Akdeniz',\n",
" 'TR7':'Orta Anadolu',\n",
" 'TR8':'Batı Karadeniz',\n",
" 'TR9':'Doğu Karadeniz',\n",
" 'TRA':'Kuzeydoğu Anadolu',\n",
" 'TRC':'Güneydoğu Anadolu',\n",
" 'TRB':'Ortadoğu Anadolu'\n",
" 'TR8':'Bat\u0131 Karadeniz',\n",
" 'TR9':'Do\u011fu Karadeniz',\n",
" 'TRA':'Kuzeydo\u011fu Anadolu',\n",
" 'TRC':'G\u00fcneydo\u011fu Anadolu',\n",
" 'TRB':'Ortado\u011fu Anadolu'\n",
" }\n",
"\n",
"\n",
@@ -3517,8 +3517,8 @@
"france_copy = france.copy()\n",
"reposition(france_copy, france.name=='Guadeloupe', 57.4, 25.4, 1.5, 1.5)\n",
"reposition(france_copy, france.name=='Martinique', 58.4, 27.1, 1.5, 1.5)\n",
"reposition(france_copy, france.name=='Guyane française', 52, 37.7, 0.35, 0.35)\n",
"reposition(france_copy, france.name=='La Réunion', -55, 62.8, 1.5, 1.5)\n",
"reposition(france_copy, france.name=='Guyane fran\u00e7aise', 52, 37.7, 0.35, 0.35)\n",
"reposition(france_copy, france.name=='La R\u00e9union', -55, 62.8, 1.5, 1.5)\n",
"reposition(france_copy, france.name=='Mayotte', -43, 54.3, 1.5, 1.5)\n",
"\n",
"not speed_run and france_copy.plot(figsize=(8, 8), **plot_styles)"
@@ -3669,8 +3669,8 @@
"france_overseas = france.copy()\n",
"reposition(france_overseas, france.name=='Guadeloupe', 53.2, 29, 1.5, 1.5)\n",
"reposition(france_overseas, france.name=='Martinique', 52.8, 27.5, 1.5, 1.5)\n",
"reposition(france_overseas, france.name=='Guyane française', 45, 35.5, 0.3, 0.3)\n",
"reposition(france_overseas, france.name=='La Réunion', -58.2, 60.5, 1.5, 1.5)\n",
"reposition(france_overseas, france.name=='Guyane fran\u00e7aise', 45, 35.5, 0.3, 0.3)\n",
"reposition(france_overseas, france.name=='La R\u00e9union', -58.2, 60.5, 1.5, 1.5)\n",
"reposition(france_overseas, france.name=='Mayotte', -50.5, 52.2, 2, 2)\n",
"\n",
"# Tahiti\n",
@@ -3713,7 +3713,7 @@
"france_overseas = pd.concat([france_overseas, saint_martin_data], ignore_index=True)\n",
"reposition(france_overseas, france_overseas.admin=='Saint Martin', 54.8, 30.3, 5, 5)\n",
"\n",
"# Saint Barthélémy\n",
"# Saint Barth\u00e9l\u00e9my\n",
"saint_barthelemy_data = df[(df['admin'] == 'Saint Barthelemy')]\n",
"france_overseas = pd.concat([france_overseas, saint_barthelemy_data], ignore_index=True)\n",
"reposition(france_overseas, france_overseas.admin=='Saint Barthelemy', 54.5, 30, 8, 8)\n",
@@ -3729,13 +3729,13 @@
"france_overseas = pd.concat([france_overseas, paris_and_littlecrowndpts_copy], ignore_index=True)\n",
"\n",
"# Update metadata properly\n",
"france_overseas.loc[france_overseas['name'] == 'Windward Islands', ['name', 'iso_3166_2']] = ['Polynésie française', 'FR-PF']\n",
"france_overseas.loc[france_overseas['name'] == 'Archipel des Kerguelen', ['name', 'iso_3166_2']] = ['Terres australes et antarctiques françaises', 'FR-TF']\n",
"france_overseas.loc[france_overseas['name'] == 'Windward Islands', ['name', 'iso_3166_2']] = ['Polyn\u00e9sie fran\u00e7aise', 'FR-PF']\n",
"france_overseas.loc[france_overseas['name'] == 'Archipel des Kerguelen', ['name', 'iso_3166_2']] = ['Terres australes et antarctiques fran\u00e7aises', 'FR-TF']\n",
"france_overseas.loc[france_overseas['admin'] == 'Wallis and Futuna', ['name', 'iso_3166_2']] = ['Wallis et Futuna', 'FR-WF']\n",
"france_overseas.loc[france_overseas['admin'] == 'New Caledonia', ['name', 'iso_3166_2']] = ['Nouvelle-Calédonie', 'FR-NC']\n",
"france_overseas.loc[france_overseas['admin'] == 'New Caledonia', ['name', 'iso_3166_2']] = ['Nouvelle-Cal\u00e9donie', 'FR-NC']\n",
"france_overseas.loc[france_overseas['admin'] == 'Saint Pierre and Miquelon', ['name', 'iso_3166_2']] = ['Saint-Pierre-et-Miquelon', 'FR-PM']\n",
"france_overseas.loc[france_overseas['admin'] == 'Saint Martin', ['name', 'iso_3166_2']] = ['Saint-Martin', 'FR-MF']\n",
"france_overseas.loc[france_overseas['admin'] == 'Saint Barthelemy', ['name', 'iso_3166_2']] = ['Saint-Barthélémy', 'FR-BL']\n",
"france_overseas.loc[france_overseas['admin'] == 'Saint Barthelemy', ['name', 'iso_3166_2']] = ['Saint-Barth\u00e9l\u00e9my', 'FR-BL']\n",
"\n",
"# Plot data\n",
"france_overseas = france_overseas.rename(columns={'NAME_1': 'name','ISO': 'iso_3166_2'})\n",
@@ -3821,6 +3821,51 @@
"not speed_run and italy_regions.plot(figsize=(10, 7), **plot_styles)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "65aIalqEt1LR"
},
"source": [
"#### Italy Regions and Autonomous Provinces"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-27T19:54:28.892892Z",
"iopub.status.busy": "2026-07-27T19:54:28.892454Z",
"iopub.status.idle": "2026-07-27T19:54:31.123499Z",
"shell.execute_reply": "2026-07-27T19:54:31.122932Z"
}
},
"outputs": [],
"source": [
"trento_and_bozen = df[(df.admin == 'Italy') & (df.iso_3166_2.isin(['IT-TN', 'IT-BZ']))][['geometry','iso_3166_2','name']]\n",
"\n",
"italy_regions_and_autonomous_provinces = pd.concat([italy_regions, trento_and_bozen])\n",
"\n",
"italy_regions_and_autonomous_provinces = italy_regions_and_autonomous_provinces[italy_regions_and_autonomous_provinces['iso_3166_2'] != 'IT-32']"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-27T19:54:28.892892Z",
"iopub.status.busy": "2026-07-27T19:54:28.892454Z",
"iopub.status.idle": "2026-07-27T19:54:31.123499Z",
"shell.execute_reply": "2026-07-27T19:54:31.122932Z"
}
},
"outputs": [],
"source": [
"not speed_run and italy_regions_and_autonomous_provinces.plot(figsize=(10, 7), **plot_styles)"
]
},
{
"cell_type": "markdown",
"metadata": {
@@ -4331,86 +4376,86 @@
"output_type": "stream",
"text": [
"Kon Tum\n",
"Đắk Nông\n",
"Đắk Lắk\n",
"\u0110\u1eafk N\u00f4ng\n",
"\u0110\u1eafk L\u1eafk\n",
"Gia Lai\n",
"Bình Phước\n",
"Tây Ninh\n",
"B\u00ecnh Ph\u01b0\u1edbc\n",
"T\u00e2y Ninh\n",
"Long An\n",
"Đồng Tháp\n",
"\u0110\u1ed3ng Th\u00e1p\n",
"An Giang\n",
"Kiên Giang\n",
"Điện Biên\n",
"Sơn La\n",
"Thanh Hóa\n",
"Ngh An\n",
"Hà Tĩnh\n",
"Quảng Bình\n",
"Quảng Trị\n",
"Thừa Thiên - Huế\n",
"Qung Nam\n",
"Hà Giang\n",
"Cao Bng\n",
"Lào Cai\n",
"Lai Châu\n",
"Lạng Sơn\n",
"Qung Ninh\n",
"Sóc Trăng\n",
"Tin Giang\n",
"Bà Rịa - Vũng Tàu\n",
"Thành phố Hồ Chí Minh\n",
"Khánh Hòa\n",
"Cà Mau\n",
"Bạc Liêu\n",
"Hu Giang\n",
"Vĩnh Long\n",
"Trà Vinh\n",
"Bến Tre\n",
"Đồng Nai\n",
"Bình Thuận\n",
"Ninh Thun\n",
"Phú Yên\n",
"Bình Định\n",
"Quảng Ngãi\n",
"Đà Nẵng\n",
"Ninh Bình\n",
"Nam Định\n",
"Thái Bình\n",
"Hải Phòng\n",
"Hòa Bình\n",
"Tuyên Quang\n",
"Yên Bái\n",
"Vĩnh Phúc\n",
"Phú Thọ\n",
"Hà Nội\n",
"Bắc Kạn\n",
"Hưng Yên\n",
"Bc Ninh\n",
"Bc Giang\n",
"Thái Nguyên\n",
"Hải Dương\n",
"Hà Nam\n",
"Bình Dương\n",
"Lâm Đồng\n",
"Cần Thơ\n"
"Ki\u00ean Giang\n",
"\u0110i\u1ec7n Bi\u00ean\n",
"S\u01a1n La\n",
"Thanh H\u00f3a\n",
"Ngh\u1ec7 An\n",
"H\u00e0 T\u0129nh\n",
"Qu\u1ea3ng B\u00ecnh\n",
"Qu\u1ea3ng Tr\u1ecb\n",
"Th\u1eeba Thi\u00ean - Hu\u1ebf\n",
"Qu\u1ea3ng Nam\n",
"H\u00e0 Giang\n",
"Cao B\u1eb1ng\n",
"L\u00e0o Cai\n",
"Lai Ch\u00e2u\n",
"L\u1ea1ng S\u01a1n\n",
"Qu\u1ea3ng Ninh\n",
"S\u00f3c Tr\u0103ng\n",
"Ti\u1ec1n Giang\n",
"B\u00e0 R\u1ecba - V\u0169ng T\u00e0u\n",
"Th\u00e0nh ph\u1ed1 H\u1ed3 Ch\u00ed Minh\n",
"Kh\u00e1nh H\u00f2a\n",
"C\u00e0 Mau\n",
"B\u1ea1c Li\u00eau\n",
"H\u1eadu Giang\n",
"V\u0129nh Long\n",
"Tr\u00e0 Vinh\n",
"B\u1ebfn Tre\n",
"\u0110\u1ed3ng Nai\n",
"B\u00ecnh Thu\u1eadn\n",
"Ninh Thu\u1eadn\n",
"Ph\u00fa Y\u00ean\n",
"B\u00ecnh \u0110\u1ecbnh\n",
"Qu\u1ea3ng Ng\u00e3i\n",
"\u0110\u00e0 N\u1eb5ng\n",
"Ninh B\u00ecnh\n",
"Nam \u0110\u1ecbnh\n",
"Th\u00e1i B\u00ecnh\n",
"H\u1ea3i Ph\u00f2ng\n",
"H\u00f2a B\u00ecnh\n",
"Tuy\u00ean Quang\n",
"Y\u00ean B\u00e1i\n",
"V\u0129nh Ph\u00fac\n",
"Ph\u00fa Th\u1ecd\n",
"H\u00e0 N\u1ed9i\n",
"B\u1eafc K\u1ea1n\n",
"H\u01b0ng Y\u00ean\n",
"B\u1eafc Ninh\n",
"B\u1eafc Giang\n",
"Th\u00e1i Nguy\u00ean\n",
"H\u1ea3i D\u01b0\u01a1ng\n",
"H\u00e0 Nam\n",
"B\u00ecnh D\u01b0\u01a1ng\n",
"L\u00e2m \u0110\u1ed3ng\n",
"C\u1ea7n Th\u01a1\n"
]
}
],
"source": [
"vietnam = df[df.admin == 'Vietnam']\n",
"vietnam_copy = vietnam.copy()\n",
"replace_column('name', vietnam_copy, 'Ðong Tháp', 'Đồng Tháp')\n",
"replace_column('name', vietnam_copy, 'Son La', 'Sơn La')\n",
"replace_column('name', vietnam_copy, 'Ha Tinh', 'Hà Tĩnh')\n",
"replace_column('name', vietnam_copy, 'Quàng Nam', 'Qung Nam')\n",
"replace_column('name', vietnam_copy, 'Lai Chau', 'Lai Châu')\n",
"replace_column('name', vietnam_copy, 'Hồ Chí Minh city', 'Thành phố Hồ Chí Minh')\n",
"replace_column('name', vietnam_copy, 'Hau Giang', 'Hu Giang')\n",
"replace_column('name', vietnam_copy, 'Ha Noi', 'Hà Nội')\n",
"replace_column('name', vietnam_copy, 'Can Tho', 'Cần Thơ')\n",
"replace_column('name', vietnam_copy, 'Đông Nam Bộ', 'Đồng Nai')\n",
"replace_column('name', vietnam_copy, 'Đông Bắc', 'Bắc Kạn')\n",
"replace_column('name', vietnam_copy, 'Đồng Bằng Sông Hồng', 'Hưng Yên')\n",
"replace_column('name', vietnam_copy, '\u00d0ong Th\u00e1p', '\u0110\u1ed3ng Th\u00e1p')\n",
"replace_column('name', vietnam_copy, 'Son La', 'S\u01a1n La')\n",
"replace_column('name', vietnam_copy, 'Ha Tinh', 'H\u00e0 T\u0129nh')\n",
"replace_column('name', vietnam_copy, 'Qu\u00e0ng Nam', 'Qu\u1ea3ng Nam')\n",
"replace_column('name', vietnam_copy, 'Lai Chau', 'Lai Ch\u00e2u')\n",
"replace_column('name', vietnam_copy, 'H\u1ed3 Ch\u00ed Minh city', 'Th\u00e0nh ph\u1ed1 H\u1ed3 Ch\u00ed Minh')\n",
"replace_column('name', vietnam_copy, 'Hau Giang', 'H\u1eadu Giang')\n",
"replace_column('name', vietnam_copy, 'Ha Noi', 'H\u00e0 N\u1ed9i')\n",
"replace_column('name', vietnam_copy, 'Can Tho', 'C\u1ea7n Th\u01a1')\n",
"replace_column('name', vietnam_copy, '\u0110\u00f4ng Nam B\u1ed9', '\u0110\u1ed3ng Nai')\n",
"replace_column('name', vietnam_copy, '\u0110\u00f4ng B\u1eafc', 'B\u1eafc K\u1ea1n')\n",
"replace_column('name', vietnam_copy, '\u0110\u1ed3ng B\u1eb1ng S\u00f4ng H\u1ed3ng', 'H\u01b0ng Y\u00ean')\n",
"for i in vietnam_copy['name']:\n",
" print(i)"
]
@@ -4454,6 +4499,7 @@
" \"turkey\": turkey_copy,\n",
" \"turkey_regions\": turkey_regions,\n",
" \"italy_regions\": italy_regions,\n",
" \"italy_regions_and_autonomous_provinces\": italy_regions_and_autonomous_provinces,\n",
" \"philippines_regions\": philippines_regions,\n",
" \"latvia\": latvia_copy,\n",
" \"netherlands\": netherlands_copy,\n",
@@ -4492,7 +4538,7 @@
"aruba has only one subdivision - removing from countries array\n",
"british indian ocean territory has only one subdivision - removing from countries array\n",
"cayman islands has only one subdivision - removing from countries array\n",
"curaçao has only one subdivision - removing from countries array\n",
"cura\u00e7ao has only one subdivision - removing from countries array\n",
"falkland islands has only one subdivision - removing from countries array\n",
"faroe islands has only one subdivision - removing from countries array\n",
"gibraltar has only one subdivision - removing from countries array\n",
@@ -4528,7 +4574,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"curaçao has only one subdivision - removing from countries array\n",
"cura\u00e7ao has only one subdivision - removing from countries array\n",
"falkland islands has only one subdivision - removing from countries array\n",
"faroe islands has only one subdivision - removing from countries array\n"
]

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