Compare commits

...
Author SHA1 Message Date
hainenber ffdaf9e6b8 chore(build): replace external file-related deps with native Node modules
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-08-09 23:24:12 +07:00
3b164e4270 fix(ci): remove extraneous plugins build job (#42677)
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-08 17:27:51 -07:00
bf294cfa43 perf(dashboard): avoid query context N+1 queries (#42474)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-08 17:27:04 -07:00
42e4030104 feat(security): add guest user attributes and get_guest_user_attribute() macro (#33924)
Co-authored-by: Yash Janoria <yash.janoria@314ecorp.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-08-08 16:56:07 -07:00
8181917f79 fix: Apply timezone offset to convert local time boundaries to UTC (#37014)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-08 16:40:31 -07:00
d14ab29dcb feat(a11y): enable ECharts aria-label (#28171)
Co-authored-by: rusackas <evan@preset.io>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-08 15:36:24 -07:00
a75665b014 fix(i18n): improve Simplified Chinese translations (#42794)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 12:51:59 -07:00
db5cd1aa38 chore(i18n): update Italian messages.po translations (#38948)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 12:40:08 -07:00
abb6dd6e39 fix(i18n): add Tamil (ta) locale with corrected catalog path (#38962)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 12:31:29 -07:00
4a587b8539 feat(security): actionable, request-access-aware data permission errors (#41843)
Co-authored-by: Greg Neighbors <gregneighbors@Gregs-Air-2.lan>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Greg Neighbors <gregneighbors@Gregs-MacBook-Air-2.local>
2026-08-08 11:51:15 -07:00
127a6f92a6 chore(i18n): update french po file to match latest pot file version (#40635)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 10:43:30 -07:00
25481799f6 test(deckgl): pin GeoJSON layer against multi-row payloads (#34748) (#42734)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-08 08:06:05 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1da7a7fdb1 chore(deps-dev): update starrocks requirement from <2,>=1.3.3 to >=1.3.4,<2 (#42903)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-08 01:54:54 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
185b5938ba chore(deps-dev): bump pyinstrument from 5.1.2 to 5.1.3 (#42905)
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-08 01:54:51 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
503309531f chore(deps-dev): update playwright requirement from <2,>=1.61.0 to >=1.62.0,<2 (#42908)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-08 01:54:48 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
44a86b725f chore(deps-dev): update teradatasql requirement from >=20.0.0.63 to >=20.0.0.64 (#42909)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-08 01:54:46 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Amin GhadersohiClaude Sonnet 5
38727906a2 chore(deps): bump react-window and @types/react-window in /superset-frontend (#42528)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 23:02:27 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Joe LirusackasClaude Opus 4.8
2a9c5acb01 chore(deps): bump numpy from 1.26.4 to 2.4.6 (#42778)
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>
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 22:55:20 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d8b97e31fa chore(deps-dev): bump @playwright/test from 1.61.1 to 1.62.1 in /superset-frontend (#42699)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 21:05:56 -07:00
bd03440ac8 feat(alerts-reports): add per-schedule toggle to include/exclude the Explore in Superset link (#42494)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 21:05:16 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a6c478c59d chore(deps-dev): bump lerna from 9.0.4 to 10.0.0 in /superset-frontend (#42829)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-08 10:19:24 +07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fd36419ca2 chore(deps): bump nanoid from 3.3.16 to 3.3.18 in /docs (#42902)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-08 10:08:38 +07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Joe Li
d80f4c841d chore(deps-dev): update sqlalchemy-risingwave requirement from <2.0.0,>=1.4.1 to <3.0.0,>=1.4.1 (#42856)
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-08 09:59:53 +07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Joe Li
6baa467f59 chore(deps): bump google-auth-library from 10.9.1 to 11.0.0 in /superset-frontend (#42870)
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-08 09:58:51 +07:00
b18015f63b feat: Color Picker replaces the color selection drop-down list in conditional formatting (#42053)
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-08 02:00:56 +03:00
eb8592d4bf fix(sqla): don't mislabel DB errors as ColumnNotFoundException in adhoc_column_to_sqla (#42889)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 15:03:49 -07:00
55af4685a2 fix(i18n-es): correct fuzzy-matcher strandings in Spanish catalog (#42808)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 11:29:14 -07:00
c901f01693 fix(datasets): preserve metric/column uuids on dataset export (#42393)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 11:15:24 -07:00
4c894affb2 fix(models): purge_oauth2_tokens filters on wrong column (#42211)
Signed-off-by: Martin Brodeur <addressedemartin@gmail.com>
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 11:04:11 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Joe Li
0f9049608a chore(deps): bump dompurify from 3.4.12 to 3.4.13 in /superset-frontend (#42893)
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-07 11:00:19 -07:00
Gabriel Torres RuizandGitHub 91b1a6cac5 feat(theme): allow deployments to register extra theme tokens 2026-08-07 14:37:45 -03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
86a328ca9e chore(deps): bump dompurify from 3.4.12 to 3.4.13 in /docs (#42894)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 10:23:30 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
38ba4a69bf chore(deps): bump react-intersection-observer from 10.1.0 to 11.0.0 in /superset-frontend (#42869)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 10:13:31 -07:00
280253b1fb fix(deletion-retention): dedupe repeated blocked audits (#42863)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
2026-08-07 10:11:36 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Joe Li
79c2cd367d chore(deps): update flask-sqlalchemy requirement from <3.0,>=2.5.1 to <4.0,>=2.5.1 (#42771)
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-07 09:42:42 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
fd6a0a60a6 chore(deps): bump holidays from 0.100 to 0.102 (#42860)
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-07 09:41:57 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
c2b5311f47 chore(deps-dev): bump google-cloud-bigquery from 3.42.2 to 3.43.0 (#42861)
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-07 09:41:26 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
70a82a28bf chore(deps): bump redis from 8.0.1 to 8.1.0 (#42857)
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-07 09:41:03 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
f348cf425d chore(deps): bump shillelagh from 1.4.4 to 1.4.5 (#42858)
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-07 09:41:00 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
74196cf694 chore(deps): bump markdown from 3.10.2 to 3.10.3 (#42859)
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-07 09:40:57 -07:00
fe06ebe796 feat(versioning): enable version history and capture by default (#42801)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 10:25:48 -03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f1411ccd6c chore(deps-dev): update pyathena requirement from <4,>=3.35.2 to >=3.35.4,<4 (#42862)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 06:19:46 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1770823f0a chore(deps-dev): bump eslint-plugin-jest-dom from 5.6.0 to 5.7.0 in /superset-frontend (#42868)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 06:18:31 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b7b873321d chore(deps-dev): bump baseline-browser-mapping from 2.11.7 to 2.11.8 in /superset-frontend (#42872)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 06:17:22 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d363cabec9 chore(deps): bump markdown-to-jsx from 9.9.0 to 9.10.1 in /superset-frontend (#42885)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 06:17:18 -07:00
Joe LiandGitHub 8683d1608e fix(migrations): merge divergent Alembic heads into a single chain (#42878) 2026-08-07 09:27:03 -03:00
Mehmet Salih YavuzandGitHub 14583b6a5f fix(themes): serve system themes with the algorithm of the slot they fill (#42700) 2026-08-07 14:29:49 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
27498d26aa chore(deps): bump the docusaurus-openapi group in /docs with 2 updates (#42883)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 02:42:39 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
39e66476e1 chore(deps): bump actions/setup-java from 5.6.0 to 5.7.0 (#42884)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 02:42:33 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Đỗ Trọng Hải
8153a82489 chore(deps): bump baseline-browser-mapping from 2.11.7 to 2.11.8 in /docs (#42852)
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-06 23:24:24 -07:00
448eb25d76 feat(docs): add documentation for usage of minio or other s3 compatible service as a cache backend or celery results backend (#35825)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 18:36:34 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9cec7411e9 chore(deps): bump mermaid from 11.16.0 to 11.16.1 in /docs (#42873)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-06 18:33:23 -07:00
ae66b697e5 fix(plugin-chart-table): align sticky header column dividers with table body (#42573)
Co-authored-by: Juliann Forbes <juliann@preset.io>
2026-08-06 15:40:44 -07:00
22fb1b3a44 test(dashboard): migrate edit-mode component E2E tests to Playwright (#41438)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-06 15:22:19 -07:00
Elizabeth ThompsonandGitHub 7dd6ce835f fix(datasets): catch TemplateError instead of narrower TemplateSyntaxError in render_dataset_fields (#42802) 2026-08-06 15:05:08 -07:00
4f2147009f feat(dashboard): expand all chart descriptions (#32958)
Co-authored-by: Urban Pettersson <urban.pettersson@alteryx.com>
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 14:47:30 -07:00
2ecce20e48 chore(viz): remove legacy explore_json + viz.py pipeline (#41714)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-06 13:22:15 -07:00
01a9fdc621 chore(reports): thread cache-key/execution-id log context through screenshot capture logs (#42657)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-06 09:43:12 -07:00
ba09f399ac feat(soft-delete): enable soft delete by default and purge for real (#42800)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 09:34:55 -07:00
e4c306f5eb fix(deps): revert simple-zstd from 2.1.0 back to 1.4.2, again (#42804)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 13:18:07 -03:00
ea8fa58158 docs: scope AI pre-commit checks to changed files, to align with ci's pre-commit checks (#42835)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
2026-08-06 10:59:33 -03:00
a00e2952a1 style(frontend): apply oxfmt to existing files (#42813)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
2026-08-06 10:58:27 -03:00
de93a19b3c fix(engine): update databend engine spec for dialect version >=0.4.6 (#28627)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 06:28:16 -07:00
Alexandru SoareandGitHub 57d6e5c637 feat(hooks): add configurable post-creation hook for dashboards (#42837) 2026-08-06 16:11:55 +03:00
dependabot[bot]andGitHub 0fc0d21dd3 chore(deps): bump github/codeql-action/init from 4.37.3 to 4.37.4 (#42827)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-06 14:12:22 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
76e6909cfd chore(deps): bump @swc/core from 1.15.46 to 1.15.47 in /docs (#42825)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-06 03:14:34 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
450e43b8f2 chore(deps): bump baseline-browser-mapping from 2.11.6 to 2.11.7 in /docs (#42826)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-06 03:14:31 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a7dd86adc6 chore(deps): bump github/codeql-action/analyze from 4.37.3 to 4.37.4 (#42828)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-06 03:14:25 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
bbd7ac7278 chore(deps-dev): bump baseline-browser-mapping from 2.11.6 to 2.11.7 in /superset-frontend (#42830)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-06 03:14:22 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
05af8eeff2 chore(deps): bump react-map-gl from 8.1.1 to 8.1.2 in /superset-frontend (#42831)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-06 03:14:18 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3f413e6e10 chore(deps-dev): bump @swc/core from 1.15.46 to 1.15.47 in /superset-frontend (#42832)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-06 03:14:14 -07:00
4278f4b3d9 fix(explore): honor explicit List legend selection instead of flipping to scroll (#41749)
Co-authored-by: Joe Li <joe@preset.io>
2026-08-05 23:08:05 -07:00
Joe LiandGitHub b8c44a1ad5 fix(ci): restore master validation checks (#42807) 2026-08-05 22:58:05 -07:00
d594a4d157 fix(mcp): make streamable-http session mode configurable via MCP_STATELESS_HTTP (#42814)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 21:31:22 -07:00
442995481c test(deckgl): Polygon chart with a column literally named "polygon" (#33669) (#42591)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-05 20:27:13 -07:00
1de35d1361 fix(mcp): reject unknown fields in nested chart-config models too (#42626) (#42732)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-05 16:24:55 -07:00
3f011edabb fix(mcp): supply required DBEventLogger args so mcp_tool_error events are logged (#42579) (#42730)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-05 16:22:10 -07:00
f5bca3bf3f fix(sqllab): stop OAuth2 banner from ballooning the database selector popover (#42652)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 15:40:42 -07:00
32e4e3c6a8 fix(reports): enforce dashboard readiness and execution budget (#42624)
Co-authored-by: Matt Fitzgerald <matt.fitzgerald@preset.io>
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-05 14:30:35 -07:00
ad0538935d fix(pivot-table): blank ambiguous cross-metric totals and null values in fraction mode (#42810)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 14:17:31 -07:00
633f393880 feat(pivot-table): reintroduce show-values-as-fraction display option (#42761)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 14:17:30 -07:00
0e4e368768 feat(mcp): add observability to MCP service (#41921)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-05 11:52:34 -07:00
126c93b495 fix(versioning): pin shadow-row reads and restore to (id, uuid) (#42797)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 11:12:20 -07:00
e880711bbb fix(migrations): actually drop _customer_location_uc (list == set no-op) (#42642)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 11:12:14 -07:00
47a4dabd14 feat(soft-delete): warn at startup when a retention task's Celery config is incomplete (#42641)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 11:12:09 -07:00
JUST.in DO ITandGitHub 657e2d2267 perf(TimeTable): skip rendering TableView while the window is resizing (#42097) 2026-08-05 10:41:39 -07:00
05193ed7e1 fix(models): guard three more backref cascades for SQLAlchemy 2.0 (#42787)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-05 09:58:45 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>rusackasClaude Opus 4.8
2c7adcaef8 chore(deps): bump sqlglot from 30.12.0 to 30.14.0 (#42772)
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: rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 09:44:05 -07:00
f65b42408e fix(tasks): gate exception_type in task properties behind SHOW_STACKTRACE (#40587)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-05 09:31:30 -07:00
482a59ec72 fix(docker): replace stale superset_app hostname in dev WEBDRIVER_BASEURL (#42495)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-05 09:31:10 -07:00
e7338a2add fix(mcp): route FastMCP ValidationError through the validation error handler (#42578) (#42738)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-05 09:27:28 -07:00
9c97d4859c fix(mcp): stop false-flagging successful responses with error_type:null as failures (#42580) (#42736)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-05 09:27:17 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
19399a5068 chore(deps-dev): bump jsdom from 30.0.0 to 30.0.1 in /superset-frontend (#42791)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-05 22:25:56 +07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
efab63d42c chore(deps): bump docker/login-action from 4.5.2 to 4.6.0 (#42769)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-05 22:24:57 +07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
816f37f5b9 chore(deps-dev): bump gevent from 26.4.0 to 26.7.0 (#42770)
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-04 23:33:55 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
872311b3c2 chore(deps-dev): bump duckdb from 1.5.4 to 1.5.5 (#42773)
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-04 23:33:52 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6713f1be45 chore(deps-dev): update teradatasql requirement from >=20.0.0.62 to >=20.0.0.63 (#42774)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 23:33:48 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0692607b4a chore(deps-dev): update taospy requirement from >=2.8.9 to >=2.8.10 (#42775)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 23:33:44 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
fcfee7efbc chore(deps-dev): bump fastmcp from 3.4.4 to 3.4.5 (#42776)
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-04 23:33:41 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
0cc5c51ba3 chore(deps): bump selenium from 4.45.0 to 4.46.0 (#42777)
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-04 23:33:37 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a57c9b34c9 chore(deps-dev): bump webpack from 5.109.1 to 5.109.2 in /docs (#42779)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 23:33:33 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ae8c4bd451 chore(deps): bump baseline-browser-mapping from 2.11.5 to 2.11.6 in /docs (#42780)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 23:33:30 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
247f610ee1 chore(deps-dev): bump eslint-plugin-jest-dom from 5.5.0 to 5.6.0 in /superset-frontend (#42781)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 23:33:27 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4c33258b9d chore(deps): bump react-draggable from 4.7.0 to 4.7.1 in /superset-frontend (#42782)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 23:33:23 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3cf83f0a5a chore(deps-dev): bump baseline-browser-mapping from 2.11.5 to 2.11.6 in /superset-frontend (#42783)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 23:33:20 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
425c70ae37 chore(deps-dev): bump webpack from 5.109.1 to 5.109.2 in /superset-frontend (#42784)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 23:33:16 -07:00
ab124a333c fix(SafeMarkdown): let htmlSchemaOverrides replace matching default attribute rules (#42202)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 23:10:14 -07:00
7324225c77 fix(filters): sort numeric filter values numerically, not lexicographically (#36775) (#42596)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-04 23:06:39 -07:00
endimonanandGitHub 1605676aa7 fix(reports): use zip extension for bundled CSV attachments (#42640) 2026-08-04 21:09:19 -07:00
SkinnyPigeonandGitHub d6c8fdd10d fix(explore): recompute currency_formats when dataset is saved from c… (#42480) 2026-08-04 20:41:06 -07:00
Abdul RehmanandGitHub 956231b9ee fix(dataset): make post-save column refresh best-effort for Jinja SQL (#42463) 2026-08-04 20:40:09 -07:00
2f9bde5579 fix(sql): preserve quoted-identifier casing for the HANA dialect (#39328) (#42731)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-04 20:39:35 -07:00
Joe LiandGitHub 5720e9c7fd chore(deps): require manual deck.gl upgrades (#42653) 2026-08-05 10:06:07 +07:00
e871ee27cf fix(i18n-es): correct format-placeholder defects in Spanish catalog (#42728)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 09:21:27 +07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1a74c8ef67 chore(deps-dev): bump fast-uri from 3.1.4 to 3.1.5 in /superset-embedded-sdk (#42764)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-05 09:07:27 +07:00
22909bb039 fix(sqllab): retry table metadata preview after OAuth2 redirect (#42390)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 21:32:13 -04:00
da27b4ed80 chore(sqla2): bump SQLAlchemy-dependent drivers to dual-compat versions (#42542)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-04 17:08:41 -07:00
Amin GhadersohiandGitHub 86b2076169 fix(mcp): handle missing metrics/columns and null data in chart preview (#42576) 2026-08-04 19:59:47 -04:00
a0895a1fbd fix(native-filters): allow X-clear on defaultToFirstItem filters with required warning (#42762)
Co-authored-by: madhushree agarwal <madhushree_agarwal@apple.com>
2026-08-04 16:57:34 -07:00
043163b54c fix(mcp): reject unparseable time_range instead of silently matching full table (#42283)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 19:45:30 -04:00
Amin GhadersohiandGitHub 33c386a2d6 fix(core): reduce metric expression log noise (#42619) 2026-08-04 16:35:02 -07:00
Elizabeth ThompsonandGitHub 7c03736623 fix(alerts): wrap Jinja rendering errors in AlertCommand._execute_query (#42714) 2026-08-04 15:07:10 -07:00
Elizabeth ThompsonandGitHub 4e9e884dd8 fix(sqllab): wrap process_template() in QueryEstimationCommand to prevent raw UndefinedError leak (#42757) 2026-08-04 15:02:47 -07:00
1478e32bc2 fix(mcp): persist Handlebars template under camelCase key so it renders (#42725)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 16:21:57 -04:00
e103d92b48 feat(versioning): version-history UI (#41551)
Co-authored-by: Kamil Gabryjelski <kamil.gabryjelski@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
2026-08-04 12:37:46 -07:00
Dang DaoandGitHub 9377659ffa fix(dashboard): preserve duplicate chart holders in filter scopes (#42502) 2026-08-04 21:24:25 +02:00
Lalith KothuruandGitHub 50c84f220b docs: fix parameter name in Explorable.get_query_result docstring (#42646) 2026-08-04 10:15:05 -07:00
Alexandru SoareandGitHub 3dde95d809 feat(reports): add retry on failure support for reports (#42481) 2026-08-04 18:04:32 +03:00
dependabot[bot]andGitHub 0615c73ff3 chore(deps): bump docker/setup-buildx-action from 3.12.0 to 4.2.0 (#42692) 2026-08-04 21:57:40 +07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e66c7fcb63 chore(deps-dev): bump oxfmt from 0.60.0 to 0.61.0 in /superset-frontend (#42746)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 06:19:36 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
29098afff7 chore(deps): bump react-arborist from 3.15.1 to 3.16.0 in /superset-frontend (#42689)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 03:13:38 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>hainenber
46b583668b chore(deps-dev): bump ip-address from 10.2.0 to 10.4.0 in /superset-frontend (#42727)
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-04 03:11:11 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ebda22bdfc chore(deps-dev): bump @types/node from 26.1.1 to 26.1.2 in /superset-websocket (#42741)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 03:11:06 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5f2bc09b9f chore(deps): bump the storybook group in /docs with 2 updates (#42742)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 03:11:03 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f67d12edf8 chore(deps-dev): bump webpack from 5.109.0 to 5.109.1 in /docs (#42743)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 03:10:59 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
64590b5138 chore(deps): bump baseline-browser-mapping from 2.11.4 to 2.11.5 in /docs (#42744)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 03:10:56 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
327f526a63 chore(deps-dev): bump the storybook group in /superset-frontend with 5 updates (#42745)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 03:10:53 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0048c4d30b chore(deps): bump acorn from 8.17.0 to 8.18.0 in /superset-frontend (#42747)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 03:10:48 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
720f36e763 chore(deps-dev): bump webpack from 5.109.0 to 5.109.1 in /superset-frontend (#42748)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 03:10:44 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e52f22226f chore(deps): bump ol from 10.9.0 to 10.10.0 in /superset-frontend (#42749)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 03:10:40 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
89d544c2ed chore(deps-dev): bump @types/node from 26.1.1 to 26.1.2 in /superset-frontend (#42750)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 03:10:36 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
cf7527f2e6 chore(deps-dev): bump oxlint from 1.75.0 to 1.76.0 in /superset-frontend (#42751)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 03:10:33 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9a5cf76336 chore(deps): bump fast-uri from 3.1.4 to 3.1.5 in /superset-frontend/cypress-base (#42721)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 02:49:02 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1666ccaa2b chore(deps-dev): bump jsdom from 29.1.1 to 30.0.0 in /superset-frontend (#42691)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 09:18:35 +07:00
Yuriy KrasilnikovandGitHub 457cd3487d fix(api): include query lifecycle timing in /api/v1/chart/data response (#37516) 2026-08-03 17:27:57 -07:00
03b35186e5 fix(jinja): handle UndefinedError from virtual dataset templates (#42366)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-03 17:09:31 -07:00
25ab96188f fix(sqllab): roll back session before retrying get_query after a broken transaction (#42675)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-03 15:04:28 -07:00
4b11fa09c7 fix(errors): map uncaught SupersetException status/log-level correctly (#42643)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-03 15:03:00 -07:00
Joe LiandGitHub 5075253b91 fix(ci): validate frontend dependency tree (#42720) 2026-08-03 14:38:00 -07:00
1049 changed files with 72946 additions and 29633 deletions
+1 -1
View File
@@ -34,7 +34,7 @@
**/*.geojson @villebro @rusackas
**/*.ipynb @villebro @rusackas
/superset-frontend/plugins/legacy-plugin-chart-country-map/ @villebro @rusackas
/superset-frontend/plugins/plugin-chart-country-map/ @villebro @rusackas
# Notify translation maintainers of changes to translations
+12 -6
View File
@@ -28,6 +28,11 @@ updates:
# See https://github.com/apache/superset/pull/37384#issuecomment-3793991389
# TODO: remove the plugin once Lodash usage has been migrated to a more readily tree-shakeable alternative
- dependency-name: "@swc/plugin-transform-imports"
# deck.gl and luma.gl share strict peer constraints across the root and
# plugin workspaces, and root overrides pin their transitive versions.
# Upgrade both families together in a manually validated change.
- dependency-name: "@deck.gl/*"
- dependency-name: "@luma.gl/*"
# `just-handlerbars-helpers` library in plugin-chart-handlebars requires `currencyformatter`` to be < 2
- dependency-name: "currencyformatter.js"
update-types: ["version-update:semver-major"]
@@ -42,6 +47,13 @@ updates:
# as a single manual upgrade anyway. TODO: remove when Babel 8 support is viable.
- dependency-name: "@babel/*"
update-types: ["version-update:semver-major"]
# v2.0.0 renamed ZSTDDecompress to decompress and made it async, breaking
# the webpack dev proxy (see #38662, #39138, #39139). Dependabot reopened
# the same bump in #39369 after the first revert, so pin it here instead
# of relying on a package.json comment (package.json is JSON and can't
# hold comments). Remove this once the proxy code is updated to await
# the async decompress() API.
- dependency-name: "simple-zstd"
directory: "/superset-frontend/"
schedule:
interval: "daily"
@@ -59,12 +71,6 @@ updates:
babel:
patterns:
- "@babel/*"
deckgl:
patterns:
- "@deck.gl/*"
lumagl:
patterns:
- "@luma.gl/*"
storybook:
patterns:
- "@storybook/*"
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
- name: Login to Docker Hub
if: steps.check.outputs.python
continue-on-error: true
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
+3 -3
View File
@@ -80,20 +80,20 @@ jobs:
- starburstdata/presto:350-e.6
steps:
- name: Log in to Docker Hub (authenticated source pulls)
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GHCR (push target)
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Copy image to GHCR
env:
+2 -2
View File
@@ -177,10 +177,10 @@ jobs:
run: |
zstd -d < docker-image.tar.zst | docker load
- name: Build Plugins Packages
- name: Validate dependency tree
run: |
docker run --rm $TAG bash -c \
"npm run plugins:build"
"npm ls --all --package-lock-only --depth=0 --json > /dev/null"
test-storybook:
needs: frontend-build
@@ -38,7 +38,7 @@ jobs:
- name: Login to Docker Hub
if: ${{ steps.dependabot-metadata.outputs.package-ecosystem == 'pip' }}
continue-on-error: true
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
+1
View File
@@ -138,6 +138,7 @@ PROJECT.md
.aider*
.claude_rc*
.claude/settings.local.json
.claude/worktrees/
.env.local
oxc-custom-build/
*.code-workspace
+1 -1
View File
@@ -64,7 +64,7 @@ repos:
hooks:
- id: oxfmt-frontend
name: oxfmt (frontend)
entry: bash -c 'cd superset-frontend && files=(); for f in "$@"; do files+=("${f#superset-frontend/}"); done; npx oxfmt --write -- "${files[@]}"' --
entry: bash -c 'cd superset-frontend && files=(); for f in "$@"; do files+=("${f#superset-frontend/}"); done; npx oxfmt --write --no-error-on-unmatched-pattern -- "${files[@]}"' --
language: system
pass_filenames: true
files: ^superset-frontend/.*\.(js|jsx|ts|tsx|css|scss|sass|json)$
+11 -4
View File
@@ -2,22 +2,29 @@
Apache Superset is a data visualization platform with Flask/Python backend and React/TypeScript frontend.
## ⚠️ CRITICAL: Always Run Pre-commit Before Pushing
## Run Pre-commit Before Pushing
**ALWAYS run `pre-commit run --all-files` before pushing commits.** CI will fail if pre-commit checks don't pass. This is non-negotiable.
Always run pre-commit against the files changed by the current branch before
pushing. This matches CI and keeps unrelated failures already present on
`master` from blocking otherwise independent work.
```bash
# Stage your changes first
git add .
# Run pre-commit on all files
pre-commit run --all-files
# Run pre-commit on staged files
pre-commit run
# If there are auto-fixes, stage them and commit
git add .
git commit --amend # or new commit
```
Use `pre-commit run --all-files` when auditing or repairing the repository-wide
baseline. If that check finds failures in files untouched by the current branch,
fix them in a separate branch rather than adding unrelated changes to the
current pull request.
Common pre-commit failures:
- **Formatting** - black, oxfmt, eslint will auto-fix
- **Type errors** - mypy failures need manual fixes
+162 -37
View File
@@ -24,6 +24,91 @@ assists people when migrating to a new version.
## Next
- [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.
### Soft delete is on by default, and purging is live
`SOFT_DELETE` now ships **on** (`DEFAULT_FEATURE_FLAGS`), so deleting a
dashboard, chart, or dataset archives it rather than removing it. Archived
objects are hidden from normal listings, recoverable from **Recently Archived**,
and permanently removed once the retention window elapses.
`SOFT_DELETE_PURGE_DRY_RUN` also flips to `False`, so the nightly
`deletion_retention.purge_soft_deleted` task deletes for real instead of only
logging `would_purge` counts.
**What operators should do before upgrading:**
- **Size the first live purge.** The first real run removes every entity that
aged past `SOFT_DELETE_RETENTION_DAYS` (default 30) since soft delete began
capturing, which on a busy deployment can be a large batch in one window. To
see the size first, set `SOFT_DELETE_PURGE_DRY_RUN = True`, read the
`would_purge` counts from one nightly run, then set it back.
- **Check a replaced `CELERY_CONFIG`.** A deployment that redefines it rather
than inheriting must carry both `superset.tasks.deletion_retention` in
`imports` and the `deletion_retention.purge_soft_deleted` beat entry;
a startup warning now names whichever is absent.
**Both switches are retained.** `SOFT_DELETE = False` restores hard-delete
behaviour and `SOFT_DELETE_PURGE_DRY_RUN = True` suspends purging, at any time.
One caveat on turning soft delete back off: objects archived while it was on are
**resurrected** into normal listings, since the rows were never removed — an
emergency stop rather than a clean rollback.
### Version history is on by default
`VERSION_HISTORY` and `ENABLE_VERSIONING_CAPTURE` now both ship **on**. Every
save of a chart, dashboard, or dataset writes version rows, and the version
history panel appears on Explore and Dashboard pages. The two flip together
deliberately: a panel with capture off renders an empty "No history yet" that
misrepresents the entity as unchanged.
**What operators should expect:**
- **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.
- **`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.
`ENABLE_VERSIONING_CAPTURE` is **retained permanently** as an operational
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.
### Scheduled report execution now enforces one application deadline
Scheduled report (not alert) executions are now governed by a single
end-to-end deadline shared by browser readiness, capture/PDF generation,
notification delivery, and terminal-state persistence, configured via
`ALERT_REPORTS_EXECUTION_BUDGET_SECONDS` (with per-phase reserve settings).
Behavior changes to be aware of:
- The effective budget for a schedule is
`min(ALERT_REPORTS_EXECUTION_BUDGET_SECONDS, working_timeout)`. The default
budget (one hour) matches the historical `working_timeout` model default,
so default installations see no change in how long a report may run —
but reports now fail cleanly (with an error notification) at the deadline
instead of being killed silently by Celery.
- For REPORT schedules, the Celery `soft_time_limit`/`time_limit` are now
derived from that same effective budget plus
`ALERT_REPORTS_EXECUTION_HARD_TIMEOUT_GRACE_SECONDS`, replacing the
previous `working_timeout + ALERT_REPORTS_WORKING_TIME_OUT_LAG` /
`+ ALERT_REPORTS_WORKING_SOFT_TIME_OUT_LAG` derivation. Alert schedules
keep the previous behavior.
- A `working_timeout` smaller than the summed phase reserves is floored at
the minimum viable budget (reserves + 30s) with a warning; such reports
fail fast at the first phase check rather than erroring at setup.
- Dashboard reports whose charts have not mounted are no longer captured
blank: readiness is polled until the deadline, and the report fails loudly
if charts never mount. Thumbnails and non-report screenshots keep their
previous behavior.
### Principal listing APIs now honour related-field filters
Two authorization-related listing behaviors changed for API clients. Neither
@@ -233,6 +318,10 @@ are the intended model going forward; deprecating and removing implicit viewersh
in a later major version.
- [41044](https://github.com/apache/superset/issues/41044): Removes the deprecated `AVOID_COLORS_COLLISION` feature flag (it defaulted to `True`). Color-collision avoidance is now permanently enabled; any config override setting it to `False` is ignored.
- [41714](https://github.com/apache/superset/pull/41714): **Breaking — the legacy `explore_json` chart-data pipeline is removed** at its long-declared `5.0.0` EOL. The `/superset/explore_json/` and `/superset/explore_json/data/<cache_key>` endpoints, `superset/viz.py`, the `Slice.viz` property, the `get_viz` factory, the `load_explore_json_into_cache` celery task and the `viz=` overload of `security_manager.raise_for_access` are gone. Anything importing `superset.viz` must migrate to the QueryContext / `pandas_postprocessing` pipeline behind `/api/v1/chart/data`. All 15 remaining legacy charts were migrated first: most keep their `viz_type` and renderer (no action needed for saved charts), while saved nvd3 Bubble charts are auto-migrated to the ECharts Bubble Chart (`bubble_v2`) and saved "Time-series Percent Change" (`compare`) charts to the ECharts Line Chart, which restores the nvd3 renderer's interactive percent re-basing via a draggable baseline. The deck.gl Multiple Layers chart now fetches its layers entirely client-side, refitting the viewport as each layer's data arrives, and caps the number of sub-slices fanned out per chart at `DECK_MULTI_MAX_SLICES` (default 50, configurable); dashboard filter badges no longer aggregate child-layer filter metadata.
- [41714](https://github.com/apache/superset/pull/41714): Charts migrated in place keep a `NULL` saved query context until they are next opened in Explore (which regenerates it automatically) or re-saved. Until then, cache warm-up and annotation layers referencing such a chart report an actionable error rather than warming/rendering; opening the chart once resolves it.
- [41714](https://github.com/apache/superset/pull/41714): **Breaking for third-party viz plugins** — the `useLegacyApi` field of `ChartMetadata` in `@superset-ui/core` is removed. Plugins that set it must provide a `buildQuery` and consume `/api/v1/chart/data`. The migrated first-party packages also drop their `legacy-` prefix: `@superset-ui/legacy-plugin-chart-{calendar,chord,country-map,horizon,paired-t-test,parallel-coordinates,partition,rose,world-map}``@superset-ui/plugin-chart-*`, and `@superset-ui/legacy-preset-chart-nvd3``@superset-ui/preset-chart-nvd3`. The `can_explore_json` permission is no longer created or granted; custom roles referencing it should switch to the `can_read` permissions on `Chart`.
- [41813](https://github.com/apache/superset/pull/41813): `redis` (the Python client, `redis-py`) is bumped from 5.3.1 to 8.0.1. redis-py 8 changes several connection defaults; Superset's own Redis-backed features (`GLOBAL_ASYNC_QUERIES_CACHE_BACKEND`, `DISTRIBUTED_COORDINATION_CONFIG`, and the MCP Redis store) explicitly pin the pre-upgrade behavior so this bump is a no-op for them: the wire protocol stays RESP2 (not the new RESP3 default, which requires Redis/Sentinel 6+ to speak `HELLO`) and there is still no socket timeout by default (redis-py 8 defaults to 5s, which could otherwise newly time out large cached payloads or slow networks). The no-timeout default can now be overridden via two new config keys, `CACHE_REDIS_SOCKET_TIMEOUT` / `CACHE_REDIS_SOCKET_CONNECT_TIMEOUT`, on any `CacheConfig` dict using `CACHE_TYPE: RedisCache` or `RedisSentinelCache`. Separately, redis-py 6+ changed the default for `ssl_check_hostname` from `False` to `True` for SSL connections using `ssl_cert_reqs="required"` (the default) — this is a security improvement, so it has **not** been reverted; deployments with `CACHE_REDIS_SSL=True` whose certificates lack a hostname matching the connection address should set `CACHE_REDIS_SSL_CERT_REQS="none"` (disables cert verification entirely, matching hostname-check bypass) or replace the certificate. General-purpose cache/results backends configured via `CACHE_CONFIG` / `DATA_CACHE_CONFIG` / `RESULTS_BACKEND` with `CACHE_TYPE: RedisCache` go through `flask-caching`'s own Redis backend (outside Superset's code) and are subject to the same new defaults; pass `socket_timeout` / `protocol` via `CACHE_OPTIONS` there if needed. Celery broker and result-backend connections (built by `kombu`, also outside Superset's code) keep their no-socket-timeout behavior (`kombu` passes `socket_timeout=None` explicitly) but do **not** pin the wire protocol, so they follow redis-py's RESP3 default — which requires a Redis server new enough to speak `HELLO` (Redis 6+). Deployments using a pre-6.0 Redis server (EOL) as a Celery broker should upgrade the server before taking this bump.
@@ -252,7 +341,7 @@ in a later major version.
- **`SqlaTable.sql_url` query-string format.** `SqlaTable.sql_url` now URL-encodes `table_name` and joins it as a query parameter rather than concatenating a second `?`. Previously, with `Database.sql_url` returning `/sqllab/?dbid=<id>`, the concatenation produced `/sqllab/?dbid=<id>?table_name=<raw>` — a malformed second `?` that broke the query parser. External code that parsed the legacy `<base>?table_name=<raw>` shape now sees properly percent-encoded values (e.g. `/``%2F`, ` ``+` or `%20`); decode with `urllib.parse.parse_qsl`.
- **New config flag `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE` (default `False`).** Share/permalink URLs now substitute `window.location.origin` for the backend-supplied origin so a proxied or subdirectory-deployed Superset never hands the user an unreachable internal hostname. Operators whose reverse proxy correctly forwards `X-Forwarded-Host` *and* who want permalinks to carry the backend's literal origin can opt out by setting `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE = True` in `superset_config.py`. Default `False` (rewrite is on); flipping the default would regress the dominant proxied/subdir deployment to an unreachable host.
- **New config flag `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE` (default `False`).** Share/permalink URLs now substitute `window.location.origin` for the backend-supplied origin so a proxied or subdirectory-deployed Superset never hands the user an unreachable internal hostname. Operators whose reverse proxy correctly forwards `X-Forwarded-Host` _and_ who want permalinks to carry the backend's literal origin can opt out by setting `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE = True` in `superset_config.py`. Default `False` (rewrite is on); flipping the default would regress the dominant proxied/subdir deployment to an unreachable host.
- [41651](https://github.com/apache/superset/pull/41651): **New do-not-translate standard for translation catalogs.** Strings that must stay identical to the source — icon names (e.g. `bolt`), enum/option values (`step-after`), SQL keywords, API field names (`error_message`), code constants, and example placeholders — are now marked with a `#. do-not-translate` extracted comment. The list lives in the `superset/translations/do-not-translate.txt` registry; `scripts/translations/apply_do_not_translate.py` stamps the marker onto `messages.pot` during `babel_update.sh`, and `pybabel update` propagates it to every `.po`, so the status is consistent across all languages. The AI backfill (`backfill_po.py`) and translators leave these entries untranslated (source fallback). The legacy per-catalog convention (a `# Не переводить` translator comment in the `ru` catalog) is still honored for back-compat but is superseded by this standard; contributors adding new machine-read strings should add the msgid to the registry rather than annotating individual catalogs.
@@ -289,7 +378,7 @@ Theme tokens are unaffected — antd 6 removed none of the tokens Superset expos
### Guest-token RLS rules reject unknown fields
The `rls` rules passed to `POST /api/v1/security/guest_token/` are now validated strictly: a rule may only contain `dataset` and `clause`. Previously unknown fields were silently dropped, so a mistyped or legacy scope key (most commonly `datasource` instead of `dataset`) produced a rule with no `dataset`, which is treated as a *global* rule applied to every dataset the embedded resource can reach. Such a request now returns HTTP 400 identifying the offending field instead of issuing a token with an unintended global rule. Integrators that were sending extra fields in RLS rules must remove them; valid dataset-scoped (`{"dataset": 41, "clause": "..."}`) and global (`{"clause": "..."}`) rules are unaffected.
The `rls` rules passed to `POST /api/v1/security/guest_token/` are now validated strictly: a rule may only contain `dataset` and `clause`. Previously unknown fields were silently dropped, so a mistyped or legacy scope key (most commonly `datasource` instead of `dataset`) produced a rule with no `dataset`, which is treated as a _global_ rule applied to every dataset the embedded resource can reach. Such a request now returns HTTP 400 identifying the offending field instead of issuing a token with an unintended global rule. Integrators that were sending extra fields in RLS rules must remove them; valid dataset-scoped (`{"dataset": 41, "clause": "..."}`) and global (`{"clause": "..."}`) rules are unaffected.
### MCP service requires `MCP_JWT_AUDIENCE` when JWT auth is enabled
@@ -340,9 +429,23 @@ As a result the per-table **"Aggregation function"** control (which let you pick
how totals were aggregated client-side, e.g. Sum/Average/Count) has been
removed: totals now always reflect the metric's own definition evaluated at the
total's granularity. For additive metrics (`SUM`/`COUNT`/`MIN`/`MAX`) the result
is unchanged. Saved charts that set `aggregateFunction` will ignore it; no
migration is required. If you previously relied on a plain sum-of-cells total
for a non-additive metric, that specific behavior is no longer available.
is unchanged. If you previously relied on a plain sum-of-cells total for a
non-additive metric, that specific behavior is no longer available.
The "Sum as Fraction of Total/Rows/Columns" display options are back as a
new, standalone **"Show values as"** control (below "Combine metrics" in the
Options panel), since those were mathematically correct even before this
change and are unrelated to the totals-correctness fix. A DB migration
derives the new field from any still-present `aggregateFunction` Sum-fraction
value, so a chart that had one of those options configured picks the
equivalent "Show values as" setting back up automatically. The "Count as
Fraction of ..." variants are **not** migrated: they divided a record count,
while the new control divides the metric's own value, so translating them
automatically would silently change what the chart displays rather than
restore it; those charts need to be manually reconfigured if the value-based
percentage is what's wanted. Charts that used any other non-fraction
`aggregateFunction` value (Sum, Average, Count, ...) are unaffected, since
that specific behavior remains unavailable per the above.
### `thumbnail_url` removed from dashboard list API response
@@ -373,6 +476,7 @@ ALTER TABLE tagged_object DROP CONSTRAINT <constraint_name>;
-- MySQL: find names via `SHOW CREATE TABLE tagged_object;`
ALTER TABLE tagged_object DROP FOREIGN KEY <constraint_name>;
```
### Entity version-history infrastructure (gated off by default)
Introduces the schema and SQLAlchemy-Continuum wiring that captures version history for charts, dashboards, and datasets, plus read-only `GET /api/v1/{chart,dashboard,dataset}/<uuid>/versions/` endpoints. This ships **inert**: a new config flag `ENABLE_VERSIONING_CAPTURE` defaults to `False`, so no save writes any version rows and the endpoints return empty. It is an operational kill-switch (a release toggle that becomes a permanent ops switch), not a feature flag — set it to `True` to enable capture once validated. The migration is additive; existing entity `PUT` responses gain `old_version_uuid` / `new_version_uuid` body fields and an `ETag` header (both null/absent when capture is off).
@@ -389,12 +493,12 @@ These are behavior changes that take effect on upgrade regardless of `ENABLE_VER
A read-only companion to the version-history endpoints: each entity type gains a `GET /api/v1/{chart,dashboard,dataset}/<uuid>/activity/` endpoint returning a chronological, access-filtered stream of edits — the entity's own edits plus, for charts and dashboards, transitive edits to related entities during their association windows. Datasets have no related layer in V2, so `include=related` returns an empty stream for a dataset and `include=all` reduces to the dataset's own edits.
| Param | Type | Default | Purpose |
|---|---|---|---|
| `since` / `until` | ISO 8601 | — | Bound `issued_at` |
| `include` | `self` \| `related` \| `all` | `all` | Own edits, related edits, or both |
| `q` | string | — | Case-insensitive search over the full history, applied before pagination (so `count` reflects matches) |
| `page` / `page_size` | integer | `0` / `25` | Pagination (`page_size` clamped to 200) |
| Param | Type | Default | Purpose |
| -------------------- | ---------------------------- | ---------- | ------------------------------------------------------------------------------------------------------ |
| `since` / `until` | ISO 8601 | — | Bound `issued_at` |
| `include` | `self` \| `related` \| `all` | `all` | Own edits, related edits, or both |
| `q` | string | — | Case-insensitive search over the full history, applied before pagination (so `count` reflects matches) |
| `page` / `page_size` | integer | `0` / `25` | Pagination (`page_size` clamped to 200) |
Authorization reuses the resource's `can_read` permission and per-object `raise_for_access`; related-entity rows are visibility-filtered to what the caller may see. The stream is empty unless version capture is on (`ENABLE_VERSIONING_CAPTURE`).
@@ -406,17 +510,17 @@ Entity version history (the `version_transaction` / `*_version` shadow tables th
|---|---|---|
| `SUPERSET_VERSION_HISTORY_RETENTION_DAYS` | `30` | Version rows whose owning `version_transaction.issued_at` is older than this many days are pruned. Each entity's live row (`end_transaction_id IS NULL`) is always preserved, as are the live rows of its children and associations; closed historical rows (including the baseline) age out. Set to `0` or a negative value to disable pruning. |
The task ships in the default `CeleryConfig.beat_schedule`; a deployment that overrides `CELERY_CONFIG` without inheriting the default will log a startup warning that the prune task is absent (so it never silently stops running). Retention only prunes whatever history exists — capture itself is gated separately by `ENABLE_VERSIONING_CAPTURE` (ships off).
The task ships in the default `CeleryConfig` (both the `superset.tasks.version_history_retention` import and the beat entry). A deployment that overrides `CELERY_CONFIG` without the beat entry logs a startup warning. When the override explicitly defines `imports`, a missing retention module is also reported; an absent `imports` setting is not diagnosed because Celery may register tasks through `include`, autodiscovery, or worker startup imports. Retention only prunes whatever history exists — capture itself is gated separately by `ENABLE_VERSIONING_CAPTURE`, which now ships on.
### Deletion retention (soft-deleted entities are eventually purged)
Soft-deleted dashboards, charts, and datasets are now permanently removed after a retention window (default 30 days; `SOFT_DELETE_RETENTION_DAYS`, `0` disables; settable per workspace at runtime via the `deletion-retention set-window` CLI, which takes precedence). The `deletion_retention.purge_soft_deleted` Celery beat task runs daily and removes each aged-out entity together with its M:N join rows, owned children, datasource permission, and version-history shadow rows. After purge an entity is **unrecoverable** — its detail and `/restore` endpoints return 404 and its version history is gone.
The introducing release **defaults to dry-run** (`SOFT_DELETE_PURGE_DRY_RUN=True`): the task logs `would_purge` counts but deletes nothing, so operators can validate against production before activating real purging by setting it to `False`. Note `would_purge` is an **upper bound** — it counts every entity past the retention window without evaluating deletion blockers, so a real run may purge fewer (entities referenced by report schedules or set as a user's welcome dashboard are blocked and reported separately). The task only acts while the temporary `SOFT_DELETE` rollout flag is on.
Purging is **live by default** (`SOFT_DELETE_PURGE_DRY_RUN=False`), so the retention promise above is real on a stock deployment. Set it to `True` to have the task log `would_purge` counts and delete nothing — the lever is retained, so an operator can return to dry-run at any time. Note `would_purge` is an **upper bound** — it counts every entity past the retention window without evaluating deletion blockers, so a real run may purge fewer (entities referenced by report schedules or set as a user's welcome dashboard are blocked and reported separately). The task only acts while the `SOFT_DELETE` rollout flag is on; it now ships on by default.
Deployments that replace the default `CELERY_CONFIG` must add `superset.tasks.deletion_retention` to the Celery `imports` and schedule the `deletion_retention.purge_soft_deleted` task themselves. The shipped Docker development config includes both entries.
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 purge writes an immutable, content-free audit record to the new `purge_audit_log` table that survives the entity it names: the **scheduled** purge fails closed (an entity whose audit row cannot be written is skipped and retried next run), while **force-purge** proceeds even if the audit write fails — the operator is present and deletion outranks audit for a compliance erasure.
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.
### Recently Archived view and permanent delete (purge) endpoints
@@ -478,6 +582,7 @@ Operators can tune or disable the policy via config:
### Data uploads bounded by UPLOAD_MAX_FILE_SIZE_BYTES
Single data-file uploads (CSV, Excel, columnar) are now bounded by the `UPLOAD_MAX_FILE_SIZE_BYTES` config option, which defaults to `100 * 1024 * 1024` (100 MB). Files larger than this are rejected with a `413` before their contents are buffered into memory. Set `UPLOAD_MAX_FILE_SIZE_BYTES = None` to disable the check and restore unbounded uploads.
### Currency symbol position follows the locale when unset
When a chart's currency control leaves the **Prefix or suffix** field empty, the currency symbol position is now derived from the deployment locale's own convention via `Intl.NumberFormat` instead of always defaulting to a suffix. For example, under the default `en-US` locale `USD`, `GBP`, and `EUR` render as a prefix (`$ 1,000`), while eurozone locales such as `fr-FR` render `EUR` as a suffix (`1 000 €`). An explicit Prefix/Suffix selection is always honored and is unaffected.
@@ -603,7 +708,7 @@ SQLALCHEMY_ENCRYPTED_FIELD_ENGINE = "aes"
```bash
superset re-encrypt-secrets --engine aes-gcm
```
A live instance keeps writing *new* secrets as AES-CBC during the window between step 2 and the restart in step 4; this second pass sweeps those up (it is idempotent, so already-migrated values are skipped).
A live instance keeps writing _new_ secrets as AES-CBC during the window between step 2 and the restart in step 4; this second pass sweeps those up (it is idempotent, so already-migrated values are skipped).
Schedule the cutover in a quiet window. Runtime reads use only the single configured engine, so in a multi-worker deployment there is an unavoidable brief decrypt-outage between the migration commit and the last worker restarting with the new config — each migrator run is transactional, but the fleet-wide cutover is not zero-downtime.
@@ -631,11 +736,11 @@ With the flag enabled: `DELETE /api/v1/dataset/<id>` no longer hard-deletes the
**Schema migration:** the migration adds a nullable `deleted_at` column and an index on it (`ix_tables_deleted_at`) to the `tables` table. The column add is instant; the index build runs inline (no `CONCURRENTLY`) and may briefly block writes on the `tables` table (INSERT/UPDATE/DELETE are queued while the index builds; reads are unaffected) on large Postgres deployments. MySQL InnoDB builds the index online (no blocking). Production deployments with many thousands of datasets should run this migration during a maintenance window.
**Rollback note:** if the application code is rolled back after datasets have been soft-deleted, the older code path's visibility filter no longer applies and previously hidden rows become visible to the older code. Pair the rollback with a data decision (restore the rows, hard-delete them, or also downgrade the migration) rather than assuming the old hard-delete semantics still hold. **Downgrading the migration destroys the deletion markers**: `downgrade()` drops the `deleted_at` column, so any not-yet-restored soft-deleted datasets silently become live, active datasets with no record they were ever deleted. Reconcile the trash (restore or hard-delete each row) *before* downgrading, and disable the `SOFT_DELETE` flag first so no new soft deletes land mid-rollback.
**Rollback note:** if the application code is rolled back after datasets have been soft-deleted, the older code path's visibility filter no longer applies and previously hidden rows become visible to the older code. Pair the rollback with a data decision (restore the rows, hard-delete them, or also downgrade the migration) rather than assuming the old hard-delete semantics still hold. **Downgrading the migration destroys the deletion markers**: `downgrade()` drops the `deleted_at` column, so any not-yet-restored soft-deleted datasets silently become live, active datasets with no record they were ever deleted. Reconcile the trash (restore or hard-delete each row) _before_ downgrading, and disable the `SOFT_DELETE` flag first so no new soft deletes land mid-rollback.
**SQL Lab / dataset-creation flows:** creating a dataset over a table whose dataset sits in the trash is refused. The SQL Lab "save as dataset" flow (`get_or_create_dataset`) and file uploads return a **422 naming the hidden twin and the restore endpoint**; the plain create, update, and duplicate paths currently fail with the generic "already exists" 422. In all cases the remediation is the same: restore the hidden dataset (or use a different table name). Perm-string maintenance also covers hidden rows: renaming a database rewrites `perm`/`schema_perm`/`catalog_perm` on soft-deleted datasets and their charts, so a later restore does not resurrect stale permission strings.
**Importer behavior:** importing a dataset YAML whose UUID matches an existing **soft-deleted** dataset is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dataset imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dataset's exact UUID is an explicit request to bring it back. The restore preserves the original PK, the chart back-reference, `table_columns`, and `sql_metrics`. Non-editors get `ImportFailedError`. Callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
**Importer behavior:** importing a dataset YAML whose UUID matches an existing **soft-deleted** dataset is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dataset imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored _and_ has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dataset's exact UUID is an explicit request to bring it back. The restore preserves the original PK, the chart back-reference, `table_columns`, and `sql_metrics`. Non-editors get `ImportFailedError`. Callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
**Uniqueness-validation changes that apply regardless of the feature flag:** two dataset uniqueness checks were tightened alongside this work and are active even with `SOFT_DELETE` off. (1) Create/update uniqueness treats a dataset whose `catalog` is `NULL` as belonging to the database's default catalog, so a legacy twin pair (`catalog=NULL` vs. `catalog=<default>`, same database/schema/name) that older versions allowed now fails validation with "already exists" when either row is edited — resolve by renaming or removing one of the twins. (2) Duplicating a dataset now checks name collisions scoped to the target (database, catalog, schema) instead of globally by name alone: duplicates into other databases that were previously blocked are now allowed.
@@ -655,9 +760,9 @@ With the flag enabled: `DELETE /api/v1/chart/<id>` no longer hard-deletes the ch
**Schema migration:** the migration adds a nullable `deleted_at` column and an index on it (`ix_slices_deleted_at`) to the `slices` table. The column add is instant; the index build runs inline (no `CONCURRENTLY`) and may briefly block writes on the `slices` table (INSERT/UPDATE/DELETE are queued while the index builds; reads are unaffected) on large Postgres deployments. MySQL InnoDB builds the index online (no blocking).
**Rollback note:** if the application code is rolled back after charts have been soft-deleted, the older code path's visibility filter no longer applies and previously hidden rows become visible to the older code. Pair the rollback with a data decision (restore the rows, hard-delete them, or also downgrade the migration) rather than assuming the old hard-delete semantics still hold. **Downgrading the migration destroys the deletion markers**: `downgrade()` drops the `deleted_at` column, so any not-yet-restored soft-deleted charts silently become live, active charts with no record they were ever deleted. Reconcile the trash (restore or hard-delete each row) *before* downgrading, and disable the `SOFT_DELETE` flag first so no new soft deletes land mid-rollback.
**Rollback note:** if the application code is rolled back after charts have been soft-deleted, the older code path's visibility filter no longer applies and previously hidden rows become visible to the older code. Pair the rollback with a data decision (restore the rows, hard-delete them, or also downgrade the migration) rather than assuming the old hard-delete semantics still hold. **Downgrading the migration destroys the deletion markers**: `downgrade()` drops the `deleted_at` column, so any not-yet-restored soft-deleted charts silently become live, active charts with no record they were ever deleted. Reconcile the trash (restore or hard-delete each row) _before_ downgrading, and disable the `SOFT_DELETE` flag first so no new soft deletes land mid-rollback.
**Importer behavior:** importing a chart YAML whose UUID matches an existing **soft-deleted** chart is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active chart imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted chart's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all out-of-archive references (`dashboard_slices` junctions, `report.chart_id`, tag rows). The operation is permission-gated: non-editors get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
**Importer behavior:** importing a chart YAML whose UUID matches an existing **soft-deleted** chart is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active chart imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored _and_ has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted chart's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all out-of-archive references (`dashboard_slices` junctions, `report.chart_id`, tag rows). The operation is permission-gated: non-editors get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
- [39914](https://github.com/apache/superset/pull/39914) `ALERT_REPORT_SLACK_V2` now defaults to `True` and the legacy Slack v1 integration (`Slack` recipient type, `files.upload` API) is deprecated for removal in the next major. Slack blocked new apps from `files.upload` in May 2024 and fully retired the method for all apps on November 12, 2025; because the v1 path sends files through `files.upload`, v1 file-bearing sends now fail at the API level — only text-only `chat_postMessage` still works via the legacy path. Grant your Slack bot the `channels:read` and `groups:read` scopes so existing `Slack` recipients can be auto-upgraded to `SlackV2` on next send. Operators who explicitly override the flag to `False`, or whose Slack bot is missing those scopes, will see deprecation warnings while text-only sends continue through the legacy path.
@@ -681,7 +786,7 @@ The partial-index replacement is dialect-dependent: PostgreSQL uses a native `WH
**Slug semantics:** on PostgreSQL and MySQL 8.0.13+, the slug of a soft-deleted dashboard is **free for reuse**. A new active dashboard can claim it immediately. Restoring a soft-deleted dashboard whose slug has since been claimed returns **422 with a clean error** (`DashboardSlugConflictError`) — rename one of the dashboards and retry; the restore is not silently rejected by a database-level constraint violation.
**Importer behavior:** importing a dashboard YAML whose UUID matches an existing **soft-deleted** dashboard is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dashboard imported without `overwrite=true` is returned unchanged (the import never mutates it), but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dashboard's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all pre-deletion relationship rows (`dashboard_slices` junctions, editor/viewer subjects, tags). Callers whose imports must never mutate existing state should treat bundles that may contain previously deleted UUIDs accordingly. The operation is permission-gated: it requires `can_write` and editorship of the deleted row (or admin) — non-editors get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
**Importer behavior:** importing a dashboard YAML whose UUID matches an existing **soft-deleted** dashboard is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dashboard imported without `overwrite=true` is returned unchanged (the import never mutates it), but a soft-deleted UUID match is restored _and_ has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dashboard's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all pre-deletion relationship rows (`dashboard_slices` junctions, editor/viewer subjects, tags). Callers whose imports must never mutate existing state should treat bundles that may contain previously deleted UUIDs accordingly. The operation is permission-gated: it requires `can_write` and editorship of the deleted row (or admin) — non-editors get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
### Engine spec capability flag: `supports_offset`
@@ -693,10 +798,10 @@ A new `BaseEngineSpec.supports_offset` attribute (default `True`) indicates whet
A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission:
| Permission | Controls |
|---|---|
| `can_export_data` | CSV, Excel, JSON exports |
| `can_export_image` | Screenshot/PDF exports |
| Permission | Controls |
| -------------------- | ---------------------------- |
| `can_export_data` | CSV, Excel, JSON exports |
| `can_export_image` | Screenshot/PDF exports |
| `can_copy_clipboard` | Copy-to-clipboard operations |
When the feature flag is enabled, these permissions are enforced on both the frontend (disabled buttons with tooltips) and backend (403 responses from API endpoints). When disabled, legacy `can_csv` behavior is preserved.
@@ -740,14 +845,17 @@ The Kenya country map has been updated to reflect the 47 counties established un
MCP (Model Context Protocol) tools now include enhanced observability instrumentation for monitoring and debugging:
**Two-layer instrumentation:**
1. **Middleware layer** (`LoggingMiddleware`): Automatically logs all MCP tool calls with `duration_ms` and `success` status in the audit log (Action Log UI, logs table)
2. **Sub-operation tracking**: All 19 MCP tools include granular `event_logger.log_context()` blocks for tracking individual operations like validation, database writes, and query execution
**Action naming convention:**
- Tool-level logs: `mcp_tool_call` (via middleware)
- Sub-operation logs: `mcp.{tool_name}.{operation}` (e.g., `mcp.generate_chart.validation`, `mcp.execute_sql.query_execution`)
**Querying MCP logs:**
```sql
-- Top slowest MCP operations
SELECT action, COUNT(*) as calls, AVG(duration_ms) as avg_ms
@@ -782,6 +890,7 @@ A new `DISTRIBUTED_COORDINATION_CONFIG` configuration provides a unified Redis-b
The distributed coordination is used by the Global Task Framework (GTF) for abort notifications and task completion signaling, and will eventually replace `GLOBAL_ASYNC_QUERIES_CACHE_BACKEND` as the standard signaling backend. Configuring this is recommended for Redis enabled production deployments.
Example configuration in `superset_config.py`:
```python
DISTRIBUTED_COORDINATION_CONFIG = {
"CACHE_TYPE": "RedisCache",
@@ -796,9 +905,11 @@ See `superset/config.py` for complete configuration options.
### WebSocket config for GAQ with Docker
[35896](https://github.com/apache/superset/pull/35896) and [37624](https://github.com/apache/superset/pull/37624) updated documentation on how to run and configure Superset with Docker. Specifically for the WebSocket configuration, a new `docker/superset-websocket/config.example.json` was added to the repo, so that users could copy it to create a `docker/superset-websocket/config.json` file. The existing `docker/superset-websocket/config.json` was removed and git-ignored, so if you're using GAQ / WebSocket make sure to:
- Stash/backup your existing `config.json` file, to re-apply it after (will get git-ignored going forward)
- Update the `volumes` configuration for the `superset-websocket` service in your `docker-compose.override.yml` file, to include the `docker/superset-websocket/config.json` file. For example:
``` yaml
```yaml
services:
superset-websocket:
volumes:
@@ -811,7 +922,9 @@ services:
### Example Data Loading Improvements
#### New Directory Structure
Examples are now organized by name with data and configs co-located:
```
superset/examples/
├── _shared/ # Shared database & metadata configs
@@ -824,12 +937,14 @@ superset/examples/
```
#### Simplified Parquet-based Loading
- Auto-discovery: create `superset/examples/my_dataset/data.parquet` to add a new example
- Parquet is an Apache project format: compressed (~27% smaller), self-describing schema
- YAML configs define datasets, charts, and dashboards declaratively
- Removed Python-based data generation from individual example files
#### Test Data Reorganization
- Moved `big_data.py` to `superset/cli/test_loaders.py` - better reflects its purpose as a test utility
- Fixed inverted logic for `--load-test-data` flag (now correctly includes .test.yaml files when flag is set)
- Clarified CLI flags:
@@ -839,6 +954,7 @@ superset/examples/
- `--load-big-data` / `-b`: Generate synthetic stress-test data
#### Bug Fixes
- Fixed numpy array serialization for PostgreSQL (converts complex types to JSON strings)
- Fixed KeyError for `allow_csv_upload` field in database configs (now optional with default)
- Fixed test data loading logic that was incorrectly filtering files
@@ -848,6 +964,7 @@ superset/examples/
The MCP (Model Context Protocol) service enables AI assistants and automation tools to interact programmatically with Superset.
#### New Features
- MCP service infrastructure with FastMCP framework
- Tools for dashboards, charts, datasets, SQL Lab, and instance metadata
- Optional dependency: install with `pip install apache-superset[fastmcp]`
@@ -857,6 +974,7 @@ The MCP (Model Context Protocol) service enables AI assistants and automation to
#### New Configuration Options
**Development** (single-user, local testing):
```python
# superset_config.py
MCP_DEV_USERNAME = "admin" # User for MCP authentication
@@ -865,6 +983,7 @@ MCP_SERVICE_PORT = 5008
```
**Production** (JWT-based, multi-user):
```python
# superset_config.py
MCP_AUTH_ENABLED = True
@@ -910,12 +1029,14 @@ superset mcp run --port 5008 --use-factory-config
The MCP service runs as a **separate process** from the Superset web server.
**Important**:
- Requires same Python environment and configuration as Superset
- Shares database connections with main Superset app
- Can be scaled independently from web server
- Requires `fastmcp` package (optional dependency)
**Installation**:
```bash
# Install with MCP support
pip install apache-superset[fastmcp]
@@ -929,6 +1050,7 @@ Use systemd, supervisord, or Kubernetes to manage the MCP service process.
See `superset/mcp_service/PRODUCTION.md` for deployment guides.
**Security**:
- Development: Uses `MCP_DEV_USERNAME` for single-user access
- Production: **MUST** configure JWT authentication
- See `superset/mcp_service/SECURITY.md` for details
@@ -948,8 +1070,10 @@ See `superset/mcp_service/PRODUCTION.md` for deployment guides.
- [35062](https://github.com/apache/superset/pull/35062): Changed the function signature of `setupExtensions` to `setupCodeOverrides` with options as arguments.
### Breaking Changes
- [37370](https://github.com/apache/superset/pull/37370): The `APP_NAME` configuration variable no longer controls the browser window/tab title or other frontend branding. Application names should now be configured using the theme system with the `brandAppName` token. The `APP_NAME` config is still used for backend contexts (MCP service, logs, etc.) and serves as a fallback if `brandAppName` is not set.
- **Migration:**
```python
# Before (Superset 5.x)
APP_NAME = "My Custom App"
@@ -993,16 +1117,16 @@ See `superset/mcp_service/PRODUCTION.md` for deployment guides.
Eight M:N association tables move from a synthetic `id INTEGER PRIMARY KEY` to a composite `PRIMARY KEY (fk1, fk2)` on their two foreign-key columns. The surrogate `id` is dropped, and the redundant `UNIQUE (fk1, fk2)` on the two tables that carried one is removed (now subsumed by the PK).
| Table | Composite PK |
|---|---|
| `dashboard_roles` | `(dashboard_id, role_id)` |
| `dashboard_slices` | `(dashboard_id, slice_id)` |
| `dashboard_user` | `(user_id, dashboard_id)` |
| Table | Composite PK |
| ---------------------- | ------------------------------- |
| `dashboard_roles` | `(dashboard_id, role_id)` |
| `dashboard_slices` | `(dashboard_id, slice_id)` |
| `dashboard_user` | `(user_id, dashboard_id)` |
| `report_schedule_user` | `(user_id, report_schedule_id)` |
| `rls_filter_roles` | `(role_id, rls_filter_id)` |
| `rls_filter_tables` | `(table_id, rls_filter_id)` |
| `slice_user` | `(user_id, slice_id)` |
| `sqlatable_user` | `(user_id, table_id)` |
| `rls_filter_roles` | `(role_id, rls_filter_id)` |
| `rls_filter_tables` | `(table_id, rls_filter_id)` |
| `slice_user` | `(user_id, slice_id)` |
| `sqlatable_user` | `(user_id, table_id)` |
**Before upgrading:**
@@ -1013,6 +1137,7 @@ Eight M:N association tables move from a synthetic `id INTEGER PRIMARY KEY` to a
For large `dashboard_slices` / `report_schedule_user` tables, see the operator runbook in [#39859](https://github.com/apache/superset/pull/39859) — pre-flight inventory queries, per-dialect lock-window sizing, and the duplicate / NULL-FK roll-up — to plan the maintenance window.
## 6.0.0
- [33055](https://github.com/apache/superset/pull/33055): Upgrades Flask-AppBuilder to 5.0.0. The AUTH_OID authentication type has been deprecated and is no longer available as an option in Flask-AppBuilder. OpenID (OID) is considered a deprecated authentication protocol - if you are using AUTH_OID, you will need to migrate to an alternative authentication method such as OAuth, LDAP, or database authentication before upgrading.
- [34871](https://github.com/apache/superset/pull/34871): Fixed Jest test hanging issue from Ant Design v5 upgrade. MessageChannel is now mocked in test environment to prevent rc-overflow from causing Jest to hang. Test environment only - no production impact.
- [34782](https://github.com/apache/superset/pull/34782): Dataset exports now include the dataset ID in their file name (similar to charts and dashboards). If managing assets as code, make sure to rename existing dataset YAMLs to include the ID (and avoid duplicated files).
@@ -1021,8 +1146,8 @@ For large `dashboard_slices` / `report_schedule_user` tables, see the operator r
- Change any hex color values to one of: `"success"`, `"processing"`, `"error"`, `"warning"`, `"default"`
- Custom colors are no longer supported to maintain consistency with Ant Design components
- [34561](https://github.com/apache/superset/pull/34561) Added tiled screenshot functionality for Playwright-based reports to handle large dashboards more efficiently. When enabled (default: `SCREENSHOT_TILED_ENABLED = True`), dashboards with 20+ charts or height exceeding 5000px will be captured using multiple viewport-sized tiles and combined into a single image. This improves report generation performance and reliability for large dashboards.
Note: Pillow is now a required dependency (previously optional) to support image processing for tiled screenshots.
`thumbnails` optional dependency is now deprecated and will be removed in the next major release (7.0).
Note: Pillow is now a required dependency (previously optional) to support image processing for tiled screenshots.
`thumbnails` optional dependency is now deprecated and will be removed in the next major release (7.0).
- [33084](https://github.com/apache/superset/pull/33084) The DISALLOWED_SQL_FUNCTIONS configuration now includes additional potentially sensitive database functions across PostgreSQL, MySQL, SQLite, MS SQL Server, and ClickHouse. Existing queries using these functions may now be blocked. Review your SQL Lab queries and dashboards if you encounter "disallowed function" errors after upgrading
- [34235](https://github.com/apache/superset/pull/34235) CSV exports now use `utf-8-sig` encoding by default to include a UTF-8 BOM, improving compatibility with Excel.
- [34258](https://github.com/apache/superset/pull/34258) changing the default in Dockerfile to INCLUDE_CHROMIUM="false" (from "true") in the past. This ensures the `lean` layer is lean by default, and people can opt-in to the `chromium` layer by setting the build arg `INCLUDE_CHROMIUM=true`. This is a breaking change for anyone using the `lean` layer, as it will no longer include Chromium by default.
+7 -1
View File
@@ -122,7 +122,13 @@ FEATURE_FLAGS = {
}
EXTENSIONS_PATH = "/app/docker/extensions"
ALERT_REPORTS_NOTIFICATION_DRY_RUN = True
WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501
# The Docker Compose app service is named "superset" and listens on 8088. Report
# paths are root-relative, so urljoin drops the base path; only the scheme, host,
# and port must be correct here. SUPERSET_APP_ROOT is kept for consumers that
# concatenate paths directly (e.g. cache warm-up). For screenshots in the dev
# stack (unbuilt static assets) point this at the nginx service instead:
# http://nginx{SUPERSET_APP_ROOT}/
WEBDRIVER_BASEURL = f"http://superset:8088{os.environ.get('SUPERSET_APP_ROOT', '/')}/"
# The base URL for the email report hyperlinks.
WEBDRIVER_BASEURL_USER_FRIENDLY = (
f"http://localhost:8888/{os.environ.get('SUPERSET_APP_ROOT', '/')}/"
@@ -28,6 +28,7 @@ Alerts and reports are disabled by default. To turn them on, you'll need to chan
- Users can customize the email subject by including date code placeholders, which will automatically be replaced with the corresponding UTC date when the email is sent. To enable this functionality, activate the `"DATE_FORMAT_IN_EMAIL_SUBJECT"` [feature flag](/admin-docs/configuration/configuring-superset#feature-flags). This enables date formatting in email subjects, preventing all reporting emails from being grouped into the same thread (optional for the reporting feature).
- Use date codes from [strftime.org](https://strftime.org/) to create the email subject.
- If no date code is provided, the original string will be used as the email subject.
- Each alert/report has an "Include a link back to Superset" option (enabled by default) controlling whether the call-to-action link is included in email and Slack notifications. The link text in emails is configurable via `EMAIL_REPORTS_CTA`; the Slack message always uses "Explore in Superset". Uncheck the option when recipients should not receive a link to your Superset host, e.g. for external audiences.
##### Disable dry-run mode
@@ -132,7 +133,7 @@ Superset sends an HTTP POST with `Content-Type: application/json`:
},
"text": "Alert condition met: value exceeded threshold",
"description": "Monthly revenue dropped below target",
"url": "https://your-superset-host/superset/dashboard/1/"
"url": "https://your-superset-host/dashboard/1/"
}
```
@@ -245,6 +246,53 @@ class CeleryConfig:
}
CELERY_CONFIG = CeleryConfig
# Scheduled reports share one deadline across browser readiness, capture/PDF
# generation, delivery, and terminal-state persistence. The effective budget
# for a schedule is min(this value, the schedule's working_timeout), so the
# per-schedule field keeps its meaning as a user-facing cap. The default (one
# hour) matches the historical working_timeout default, so upgrading changes
# no default behavior; lower it to enforce a tighter report SLA.
ALERT_REPORTS_EXECUTION_BUDGET_SECONDS = 3600
# These reserves are part of (not additions to) the total budget and their sum
# must be less than it. Readiness polling stops in time to leave capacity for
# the later phases.
ALERT_REPORTS_EXECUTION_CAPTURE_RESERVE_SECONDS = 60
ALERT_REPORTS_EXECUTION_DELIVERY_RESERVE_SECONDS = 120
ALERT_REPORTS_EXECUTION_CLEANUP_RESERVE_SECONDS = 30
# Celery's hard limit leaves this additional window for terminal cleanup after
# the soft limit, which equals the resolved execution budget (the configured
# budget capped by each schedule's working_timeout).
# ALERT_REPORTS_WORKING_TIME_OUT_KILL controls these Celery limits; disabling
# it does not disable the application deadline above.
ALERT_REPORTS_EXECUTION_HARD_TIMEOUT_GRACE_SECONDS = 30
# Invalid budget/reserve combinations fail application startup instead of
# allowing every scheduled report to fail later. A report Celery soft timeout
# records ERROR and increments `reports.execute.celery_soft_timeout`; it does
# not attempt an in-band customer error notification during the hard-limit
# grace window. Alert schedules retain their existing timeout notifications.
#
# The application deadline is cooperative between synchronous phases. The
# Celery limits provide the final preemption boundary when the worker pool
# supports them; PDF construction is checked immediately before and after the
# synchronous builder but cannot be interrupted inside that call.
#
# Sizing the budget against infrastructure limits:
# - Kubernetes (or similar) pod termination grace must exceed
# budget + hard-timeout grace, or in-flight reports are killed mid-run on
# every deploy/node drain despite the application deadline.
# - The web server's per-request timeout (e.g. gunicorn ``timeout``) bounds
# each individual chart data request made by the headless browser -- not
# the report as a whole. Readiness allowance beyond that per-request
# ceiling buys nothing for a single slow chart (its request dies at the
# web layer and the chart reaches an error state), but multi-chart and
# tiled captures legitimately accumulate total time well past it.
# Screenshot-specific waits continue to apply to thumbnails and other
# standalone screenshot calls. Scheduled reports derive their waits from the
# shared execution deadline above.
SCREENSHOT_LOCATE_WAIT = 100
SCREENSHOT_LOAD_WAIT = 600
@@ -278,7 +326,7 @@ WEBDRIVER_OPTION_ARGS = [
]
# This is for internal use, you can keep http
WEBDRIVER_BASEURL = "http://superset:8088" # When running using docker compose use "http://superset_app:8088'
WEBDRIVER_BASEURL = "http://superset:8088" # This is also the default for Docker Compose, where the app service is named "superset"
# This is the link sent to the recipient. Change to your domain, e.g. https://superset.mydomain.com
WEBDRIVER_BASEURL_USER_FRIENDLY = "http://localhost:8088"
```
@@ -403,9 +451,9 @@ Possible fixes:
The worker may be unable to reach the report. It will use the value of `WEBDRIVER_BASEURL` to browse to the report. If that route is invalid, or presents an authentication challenge that the worker can't pass, the report screenshot will fail.
Check this by attempting to `curl` the URL of a report that you see in the error logs of your worker. For instance, from the worker environment, run `curl http://superset_app:8088/superset/dashboard/1/`. You may get different responses depending on whether the dashboard exists - for example, you may need to change the `1` in that URL. If there's a URL in your logs from a failed report screenshot, that's a good place to start. The goal is to determine a valid value for `WEBDRIVER_BASEURL` and determine if an issue like HTTPS or authentication is redirecting your worker.
Check this by attempting to `curl` the URL of a report that you see in the error logs of your worker. For instance, from the worker environment, run `curl http://superset:8088/dashboard/1/`. You may get different responses depending on whether the dashboard exists - for example, you may need to change the `1` in that URL. If there's a URL in your logs from a failed report screenshot, that's a good place to start. The goal is to determine a valid value for `WEBDRIVER_BASEURL` and determine if an issue like HTTPS or authentication is redirecting your worker.
In a deployment with authentication measures enabled like HTTPS and Single Sign-On, it may make sense to have the worker navigate directly to the Superset application running in the same location, avoiding the need to sign in. For instance, you could use `WEBDRIVER_BASEURL="http://superset_app:8088"` for a docker compose deployment, and set `"force_https": False,` in your `TALISMAN_CONFIG`.
In a deployment with authentication measures enabled like HTTPS and Single Sign-On, it may make sense to have the worker navigate directly to the Superset application running in the same location, avoiding the need to sign in. For instance, you could use `WEBDRIVER_BASEURL="http://superset:8088"` for a docker compose deployment, and set `"force_https": False,` in your `TALISMAN_CONFIG` — appropriate only for an isolated development stack or when HTTPS is enforced by a trusted proxy in front of Superset.
### Duplicate report deliveries
@@ -54,8 +54,8 @@ celery --app=superset.tasks.celery_app:app beat
To setup a result backend, you need to pass an instance of a derivative of `BaseCache` (`from
flask_caching.backends.base import BaseCache`) to the RESULTS_BACKEND configuration key in your
superset_config.py. You can use Memcached, Redis, S3 (https://pypi.python.org/pypi/s3werkzeugcache),
memory or the file system (in a single server-type setup or for testing), or to write your own
superset_config.py. You can use Memcached, Redis, S3, MinIO, memory
or the file system (in a single server-type setup or for testing), or to write your own
caching interface. Your `superset_config.py` may look something like:
```python
@@ -89,6 +89,12 @@ issues arise. Please clear your existing results cache store when upgrading an e
- SQL Lab will _only run your queries asynchronously if_ you enable **Asynchronous Query Execution**
in your database settings (Sources > Databases > Edit record).
- In order to use dedicated results backend, additional python libraries must be installed. These libraries can be installed using pip.
- [redis-py](https://pypi.org/project/redis/) for Redis.
- [pylibmc](https://pypi.org/project/pylibmc/) for memcached
- [s3werkzeugcache](https://pypi.python.org/pypi/s3werkzeugcache) for S3
- [minio-flask-cache](https://github.com/greggailly/minio-flask-cache) for MinIO or other S3 compatible service
## Celery Flower
Flower is a web based tool for monitoring the Celery cluster which you can install from pip:
+3 -3
View File
@@ -15,9 +15,8 @@ fail-fast behavior ensures operators are immediately aware of infrastructure iss
Superset uses [Flask-Caching](https://flask-caching.readthedocs.io/) for caching purposes.
Flask-Caching supports various caching backends, including Redis (recommended), Memcached,
SimpleCache (in-memory), or the local filesystem.
[Custom cache backends](https://flask-caching.readthedocs.io/en/latest/#custom-cache-backends)
are also supported.
SimpleCache (in-memory), MinIO/S3, or the local filesystem.
[Custom cache backends](https://flask-caching.readthedocs.io/en/latest/#custom-cache-backends) are also supported.
Caching can be configured by providing dictionaries in
`superset_config.py` that comply with [the Flask-Caching config specifications](https://flask-caching.readthedocs.io/en/latest/#configuring-flask-caching).
@@ -47,6 +46,7 @@ In order to use dedicated cache stores, additional python libraries must be inst
- For Redis: we recommend the [redis](https://pypi.python.org/pypi/redis) Python package
- Memcached: we recommend using [pylibmc](https://pypi.org/project/pylibmc/) client library as
`python-memcached` does not handle storing binary data correctly.
- MinIO (S3): we recommend using the [minio-flask-cache](https://github.com/greggailly/minio-flask-cache) package
These libraries can be installed using pip.
@@ -97,6 +97,50 @@ for more information on how to configure it.
At the very least, you'll want to change `SECRET_KEY` and `SQLALCHEMY_DATABASE_URI`. Continue reading for more about each of these.
## Chart-data query timing
Set `CHART_DATA_INCLUDE_TIMING = True` to add an optional versioned timing object
to every successful JSON query result returned by the chart-data API. The setting
is `False` by default, so enabling it is an explicit API-contract choice for an
operator. File exports, streaming responses, and HTTP error responses do not
include this object.
```json
{
"timing": {
"version": 1,
"query": {
"query_planning_ms": 1.23,
"cache_resolution_ms": 0.45,
"data_acquisition_ms": null,
"payload_assembly_ms": 0.67,
"total_ms": 2.98
}
}
}
```
Durations are milliseconds rounded to two decimal places. A numeric `0.0`
means that the corresponding stage ran but rounded below that precision;
`null` means it did not apply. For example, `data_acquisition_ms` is null for
a normal dataframe cache hit, while metadata-only results have null phase values
and a numeric total.
The phases have fixed ownership: `query_planning_ms` includes Jinja rendering,
row-level-security transformation, and cache identity; `cache_resolution_ms`
includes cache lookup, compatibility policy, deserialization, and rehydration;
`data_acquisition_ms` includes database work and annotation dependencies; and
`payload_assembly_ms` includes response shaping and AUTO-currency fallback.
`total_ms` is measured over one continuous per-query execution interval. It
contains the exposed stages and unattributed work such as query-result cache
persistence, so it is not the sum of the phase values. Query-context cache
persistence is excluded. The total also excludes request parsing,
authorization, contribution-total work before the per-query loop, client
post-processing, JSON serialization, network transfer, and background async
producer execution. Additive optional fields can preserve version 1; a field
rename, removal, or semantic change requires a new version.
## Specifying a SECRET_KEY
### Adding an initial SECRET_KEY
@@ -18,7 +18,7 @@ code is less ambiguous and is unique to all regions in the world.
## Included Maps
The current list of countries can be found in the src
[legacy-plugin-chart-country-map/src/countries.ts](https://github.com/apache/superset/blob/master/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries.ts)
[plugin-chart-country-map/src/countries.ts](https://github.com/apache/superset/blob/master/superset-frontend/plugins/plugin-chart-country-map/src/countries.ts)
The Country Maps visualization already ships with the maps for the following countries:
@@ -31,10 +31,10 @@ The Country Maps visualization already ships with the maps for the following cou
## Adding a New Country
To add a new country to the list, you'd have to edit files in
[@superset-ui/legacy-plugin-chart-country-map](https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-plugin-chart-country-map).
[@superset-ui/plugin-chart-country-map](https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-country-map).
1. Generate a new GeoJSON file for your country following the guide in [this Jupyter notebook](https://github.com/apache/superset/blob/master/superset-frontend/plugins/legacy-plugin-chart-country-map/scripts/Country%20Map%20GeoJSON%20Generator.ipynb).
2. Edit the countries list in [legacy-plugin-chart-country-map/src/countries.ts](https://github.com/apache/superset/blob/master/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries.ts).
1. Generate a new GeoJSON file for your country following the guide in [this Jupyter notebook](https://github.com/apache/superset/blob/master/superset-frontend/plugins/plugin-chart-country-map/scripts/Country%20Map%20GeoJSON%20Generator.ipynb).
2. Edit the countries list in [plugin-chart-country-map/src/countries.ts](https://github.com/apache/superset/blob/master/superset-frontend/plugins/plugin-chart-country-map/src/countries.ts).
3. Install superset-frontend dependencies: `cd superset-frontend && npm install`
4. Verify your countries in Superset plugins storybook: `npm run plugins:storybook`.
5. Build and install Superset from source code.
@@ -315,6 +315,76 @@ Here's a concrete example:
WHERE country_code = 'US'
```
**Guest User Attributes**
The `{{ get_guest_user_attribute('attribute_name') }}` macro returns a specific attribute value from the guest user context.
This is useful when working with embedded Superset where guest tokens can contain custom attributes that need to be
accessed in SQL queries.
This macro only works when the current user is a guest user (authenticated via guest token). If the current user is
not a guest user, or if the specified attribute doesn't exist, the macro will return `None` or the provided default value.
If you have caching enabled in your Superset configuration, then by default the resolved value (whether it
came from the guest token, a null attribute, or the provided default) will be used by Superset when
calculating the cache key. A cache key is a unique identifier that determines if there's a cache hit in the
future and Superset can retrieve cached data. Including the resolved value on every branch ensures two guests
whose tokens render different SQL never share a cache entry.
You can disable the inclusion of the attribute value in the calculation of the
cache key by adding the following parameter to your Jinja code, but only do so
when the value cannot affect the query results:
```
{{ get_guest_user_attribute('department', add_to_cache_keys=False) }}
```
You can also provide a default value if the attribute is not found:
```
{{ get_guest_user_attribute('region', default='US') }}
```
Here's a concrete example of using guest user attributes in a query:
```sql
SELECT *
FROM sales_data
WHERE region = '{{ get_guest_user_attribute("user_region", default="global") }}'
AND department = '{{ get_guest_user_attribute("department") }}'
```
:::warning[Security Warning]
Guest token attributes come from the embedding application. By default,
`get_guest_user_attribute()` escapes string values — including strings nested inside
arrays and object values, and caller-supplied defaults — through the database dialect's
literal rendering (the same mechanism as `url_param()`). This covers dialect-specific
escape characters such as the backslash on MySQL/MariaDB, so the example above is safe
to interpolate directly. If you pass `escape_result=False`, or interpolate non-string
values (numbers, booleans), you are responsible for validating or allowlisting the
values, since they originate outside Superset.
If a guest attribute is an array and you plan to pipe it through the `|where_in` filter
(for example `full_name IN {{ get_guest_user_attribute('names')|where_in }}`), call
`get_guest_user_attribute('names', escape_result=False)`. `where_in` already applies its
own dialect-safe quoting, so escaping the values twice can corrupt them (a value such as
`O'Brien` would come back doubly escaped and match nothing).
Only individual string values are escaped as SQL literals. Interpolating an entire array
or object directly (rather than through `|where_in`, or by accessing a specific element)
renders Python's string form of that structure, which is not valid SQL, and object keys
are not escaped at all. Use `|where_in` for arrays, `|tojson` where you need a
JSON-stringified value, or read individual keys/elements out of the structure yourself.
The same double-escaping problem described above for `|where_in` applies to `|tojson`:
pass `escape_result=False` before piping to `|tojson` (for example
`{{ get_guest_user_attribute('profile', escape_result=False)|tojson }}`), since JSON
already handles its own quoting and re-escaping a value first would corrupt it (a nested
string such as `O'Brien` would come back as the altered `O''Brien` in the serialized
JSON).
:::
### Explicitly Including Values in Cache Key
The `{{ cache_key_wrapper() }}` function explicitly instructs Superset to add a value to the
@@ -0,0 +1,120 @@
---
title: Version History
hide_title: true
sidebar_position: 8
version: 1
---
# Version History
Superset can record the history of a chart or dashboard as you edit it, and let
you look at, restore, or branch from any earlier version.
The panel lists each saved version with its author, timestamp, and a plain
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.
| Setting | Type | Effect |
| --- | --- | --- |
| `VERSION_HISTORY` | Feature flag | Shows the version history UI |
| `ENABLE_VERSIONING_CAPTURE` | Config value | Records versions as entities are saved |
```python
# superset_config.py
FEATURE_FLAGS = {"VERSION_HISTORY": True}
ENABLE_VERSIONING_CAPTURE = True
```
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.
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.
## Viewing history
On a chart, open **… (actions) → View version history** in Explore. A dashboard
uses the same entry in its own **… (actions)** menu.
The entry appears for users who may edit the entity — the same rule the server
applies to overwriting it: administrators, plus anyone whose subjects are among
the entity's editors. Users who cannot edit do not see the panel.
## Previewing a version
Selecting a version loads it into the page so you can see it before deciding
anything. While a preview is active:
- A banner across the top marks the page as historical and names the version.
- The dashboard or chart itself is inert — filters, controls, and chart
interactions are disabled, so nothing you click can be mistaken for editing
the live entity. You can still scroll, and still move between tabs.
- Saving is unavailable. Preview never writes.
Close the preview to return to the live version, along with whatever filter
selections you had before you opened it.
A dashboard preview reconstructs the layout as it was, including charts that
have since been removed from the dashboard. A chart that has been deleted
outright cannot be reconstructed; its slot shows a placeholder instead.
Preview is a close approximation of the version, not a pixel-faithful replay.
The dashboard's own saved properties — title, CSS, metadata, description,
slug, certification, draft or published state, and theme — are shown as they
were. Charts, though, render with their current definitions rather than the
definitions they had at that version, and a dashboard's chart membership is
reconstructed from the saved layout, so charts that belonged to the dashboard
without appearing in the layout are not represented. Restore applies the full
captured state, so a restored dashboard can differ from its preview in these
respects.
## Restoring a version
**Restore** rewrites the entity back to the selected version. Two things are
worth knowing:
- **History stays append-only.** A restore is itself recorded as a new version,
so restoring never destroys the states you moved away from — you can always
restore forward again.
- **A dashboard restore may be partial.** If the version referenced charts that
have since been deleted, the layout is restored without them and Superset
tells you how many were not reattached.
Restoring is offered to the same users who may edit the entity, and is withheld
from externally managed entities, whose source of truth lives outside Superset
and would overwrite the restore on the next sync.
## Opening a version as something new
**Open as new** creates a brand-new chart or dashboard from the selected
version and leaves the original untouched. This is the safer option when you
want an old state back but are not certain you want to give up the current one.
How much of the version comes across differs by entity, because the two copy
paths do. A new chart is built from the version itself: its name,
visualization type, dataset, controls, query context, description, and cache
timeout. A new dashboard is a copy of the *live* dashboard with the version's
title, CSS, metadata, and layout applied over it — the copy endpoint accepts
only those fields, so properties such as description, slug, certification, and
theme come from the dashboard as it stands today, not from the version.
## What gets recorded
Versions capture the entity's own saved state — a chart's name, visualization
type, and controls; a dashboard's title, layout, filters, and properties.
The timeline also shows **related** activity: changes to things the entity
depends on, such as the dataset behind a chart. These are shown for context and
cannot be restored from the entity's own history — restore acts on the entity
you are looking at.
Changes made outside a save, such as ownership or permission edits, are not part
of version history.
+7 -7
View File
@@ -58,14 +58,14 @@
"@fontsource/inter": "^5.3.0",
"@mdx-js/react": "^3.1.1",
"@saucelabs/theme-github-codeblock": "^0.3.0",
"@storybook/addon-docs": "^10.5.4",
"@storybook/addon-docs": "^10.5.5",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.46",
"@swc/core": "^1.15.47",
"antd": "^6.5.2",
"baseline-browser-mapping": "^2.11.4",
"baseline-browser-mapping": "^2.11.8",
"caniuse-lite": "^1.0.30001806",
"docusaurus-plugin-openapi-docs": "^5.1.2",
"docusaurus-theme-openapi-docs": "^5.1.2",
"docusaurus-plugin-openapi-docs": "^5.1.3",
"docusaurus-theme-openapi-docs": "^5.1.3",
"js-yaml": "^5.2.2",
"json-bigint": "^1.0.0",
"prism-react-renderer": "^2.4.1",
@@ -77,7 +77,7 @@
"react-table": "^7.8.0",
"remark-import-partial": "^0.0.2",
"reselect": "^5.2.0",
"storybook": "^10.5.4",
"storybook": "^10.5.5",
"swagger-ui-react": "^5.32.11",
"swc-loader": "^0.2.7",
"tinycolor2": "^1.4.2",
@@ -97,7 +97,7 @@
"oxfmt": "^0.61.0",
"typescript": "~6.0.3",
"typescript-eslint": "^8.65.0",
"webpack": "^5.109.0"
"webpack": "^5.109.2"
},
"browserslist": {
"production": [
+48 -7
View File
@@ -101,21 +101,29 @@ import json
import ast
import os
def eval_node(node):
"""Safely evaluate an AST node as a Python literal."""
def eval_node(node, constants=None):
"""Safely evaluate an AST node as a Python literal.
\`constants\` is an optional dict of module-level constant names -> already
-resolved Python values. It lets us resolve references like
\`AURORA_DATA_API_KNOWN_INCOMPATIBILITIES\` that point at a list/dict
defined (and potentially imported across files) elsewhere in
db_engine_specs, instead of falling through to the bare identifier
string.
"""
if node is None:
return None
if isinstance(node, ast.Constant):
return node.value
elif isinstance(node, ast.List):
return [eval_node(e) for e in node.elts]
return [eval_node(e, constants) for e in node.elts]
elif isinstance(node, ast.Dict):
result = {}
for k, v in zip(node.keys, node.values):
if k is not None:
key = eval_node(k)
key = eval_node(k, constants)
if key is not None:
result[key] = eval_node(v)
result[key] = eval_node(v, constants)
return result
elif isinstance(node, ast.Name):
# Handle True, False, None constants
@@ -125,12 +133,14 @@ def eval_node(node):
return False
elif node.id == 'None':
return None
if constants and node.id in constants:
return constants[node.id]
return node.id
elif isinstance(node, ast.Attribute):
# Handle DatabaseCategory.SOMETHING - return just the attribute name
return node.attr
elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
left, right = eval_node(node.left), eval_node(node.right)
left, right = eval_node(node.left, constants), eval_node(node.right, constants)
if isinstance(left, str) and isinstance(right, str):
return left + right
return None
@@ -274,6 +284,37 @@ CAP_METHODS = {
# Intermediate base classes (e.g. PrestoBaseEngineSpec) do count as overrides.
TRUE_BASE_CLASS = 'BaseEngineSpec'
# Pass 0: collect module-level literal constants across every engine spec
# file (e.g. AURORA_DATA_API_KNOWN_INCOMPATIBILITIES in base.py, imported
# into mysql.py's \`compatible_databases\` metadata) so \`metadata\` dicts
# that reference a shared constant by name resolve to its actual value
# instead of the bare identifier string. Only module-scope assignments
# (tree.body, not nested in classes/functions) are considered.
MODULE_CONSTANTS = {}
for filename in sorted(os.listdir(specs_dir)):
if not filename.endswith('.py') or filename in ('__init__.py', 'lib.py', 'lint_metadata.py'):
continue
filepath = os.path.join(specs_dir, filename)
try:
with open(filepath) as f:
source = f.read()
tree = ast.parse(source)
for item in tree.body:
targets = []
if isinstance(item, ast.Assign):
targets = item.targets
elif isinstance(item, ast.AnnAssign) and item.value is not None:
# Handle annotated module-level constants, e.g.
# \`AURORA_DATA_API_KNOWN_INCOMPATIBILITIES: list[KnownIncompatibility] = [...]\`
targets = [item.target]
for target in targets:
if isinstance(target, ast.Name) and target.id.isupper():
val = eval_node(item.value, MODULE_CONSTANTS)
if val is not None:
MODULE_CONSTANTS[target.id] = val
except Exception:
continue
# First pass: collect all class info (name, bases, metadata, cap_attrs, direct_methods)
class_info = {} # class_name -> {bases: [], metadata: {}, engine_name: str, filename: str, ...}
@@ -330,7 +371,7 @@ for filename in sorted(os.listdir(specs_dir)):
if isinstance(val, str):
engine_attr = val
elif target.id == 'metadata':
metadata = eval_node(item.value)
metadata = eval_node(item.value, MODULE_CONSTANTS)
elif target.id in CAP_ATTR_DEFAULTS:
val = eval_node(item.value)
if isinstance(val, bool):
+69 -1
View File
@@ -41,7 +41,7 @@ import {
GithubOutlined,
BugOutlined,
} from '@ant-design/icons';
import type { DatabaseInfo } from './types';
import type { DatabaseInfo, KnownIncompatibility } from './types';
// Simple code block component for connection strings
const CodeBlock: React.FC<{ children: React.ReactNode }> = ({ children }) => (
@@ -253,6 +253,53 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
);
};
// Render known incompatibilities with a Superset dependency (e.g. a driver
// that doesn't yet support SQLAlchemy 2.0). Shared between the top-level
// documentation and each compatible-database entry.
const renderKnownIncompatibilities = (
incompatibilities?: KnownIncompatibility[],
) => {
if (!incompatibilities?.length) return null;
return (
<Space direction="vertical" style={{ width: '100%' }}>
{incompatibilities.map((incompat, idx) => (
<Alert
key={idx}
type="warning"
showIcon
message={incompat.dependency}
description={
<>
{incompat.reason && (
<Paragraph style={{ marginBottom: 4 }}>
{incompat.reason}
</Paragraph>
)}
<Space size="middle">
{incompat.tracking_url && (
<a
href={incompat.tracking_url}
target="_blank"
rel="noreferrer"
>
<LinkOutlined /> Tracking issue
</a>
)}
{incompat.since && (
<Text type="secondary">
Last confirmed: {incompat.since}
</Text>
)}
</Space>
</>
}
/>
))}
</Space>
);
};
// Render compatible databases (for PostgreSQL, etc.)
const renderCompatibleDatabases = () => {
if (!docs?.compatible_databases?.length) return null;
@@ -320,6 +367,16 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
/>
</div>
)}
{compat.known_incompatibilities?.length > 0 && (
<div style={{ marginTop: 16 }}>
<Text strong>Known Incompatibilities:</Text>
<div style={{ marginTop: 8 }}>
{renderKnownIncompatibilities(
compat.known_incompatibilities,
)}
</div>
</div>
)}
{compat.notes && (
<Alert
message={compat.notes}
@@ -624,6 +681,17 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
</Card>
)}
{/* Known Incompatibilities */}
{docs?.known_incompatibilities?.length > 0 && (
<Card
title="Known Incompatibilities"
style={{ marginBottom: 16 }}
type="inner"
>
{renderKnownIncompatibilities(docs.known_incompatibilities)}
</Card>
)}
{/* Installation */}
{(docs?.pypi_packages?.length || docs?.install_instructions) && (
<Card title="Installation" style={{ marginBottom: 16 }}>
+9
View File
@@ -72,6 +72,13 @@ export interface SSLConfiguration {
};
}
export interface KnownIncompatibility {
dependency: string; // e.g. "SQLAlchemy 2.0"
reason?: string;
tracking_url?: string; // upstream issue/PR tracking a fix, if one exists
since?: string; // ISO date this was last confirmed still broken
}
export interface CompatibleDatabase {
name: string;
description?: string;
@@ -84,6 +91,7 @@ export interface CompatibleDatabase {
connection_examples?: ConnectionExample[];
notes?: string;
docs_url?: string;
known_incompatibilities?: KnownIncompatibility[];
}
export interface CustomError {
@@ -123,6 +131,7 @@ export interface DatabaseDocumentation {
advanced_features?: Record<string, string>;
compatible_databases?: CompatibleDatabase[];
custom_errors?: CustomError[]; // Database-specific error messages and troubleshooting info
known_incompatibilities?: KnownIncompatibility[]; // Unresolved incompatibilities with a Superset dependency
}
export interface TimeGrains {
+8 -2
View File
@@ -89,9 +89,9 @@
},
{
"name": "SOFT_DELETE",
"default": false,
"default": true,
"lifecycle": "development",
"description": "Temporary rollout / kill-switch gate for soft delete (default off = legacy hard delete). An emergency stop, not a clean rollback: flipping ON->OFF resurrects already-soft-deleted rows. Removed (along with its two gate points \u2014 BaseDAO.delete routing and the do_orm_execute visibility listener) once soft delete is stable."
"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",
@@ -104,6 +104,12 @@
"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": [
+7
View File
@@ -11383,6 +11383,13 @@
},
"User3": {
"properties": {
"attributes": {
"additionalProperties": {
"nullable": true
},
"nullable": true,
"type": "object"
},
"first_name": {
"type": "string"
},
@@ -423,7 +423,7 @@ echo "sqlalchemy-cratedb" >> ./docker/requirements-local.txt
#### Databend
The recommended connector library for Databend is [databend-sqlalchemy](https://pypi.org/project/databend-sqlalchemy/).
Superset has been tested on `databend-sqlalchemy>=0.2.3`.
Superset has been tested on `databend-sqlalchemy>=0.5.5`.
The recommended connection string is:
@@ -434,7 +434,7 @@ databend://{username}:{password}@{host}:{port}/{database_name}
Here's a connection string example of Superset connecting to a Databend database:
```
databend://user:password@localhost:8000/default?secure=false
databend://user:password@localhost:8000/default?sslmode=disable
```
#### Databricks
+121 -123
View File
@@ -4095,23 +4095,23 @@
resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b"
integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
"@storybook/addon-docs@^10.5.4":
version "10.5.4"
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.4.tgz#1906605d3dee2d86ff91bb79b444edb2961536fb"
integrity sha512-2Z/x2pKEmXOCQjmttYzPuQBu9aWeMly8uEs3msrCTBLiHs/F7IlBFnMu0Z+T2Qvk0LEy8O93AlcPSP76aCcKjw==
"@storybook/addon-docs@^10.5.5":
version "10.5.5"
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.5.tgz#c679e9e68b19559bc5dee74c5a4670e95677c26f"
integrity sha512-0YpKlimS4XE0kQ8Maa5coeefQxdyDrBHg1wOP3WTPuBe4FolFSCDveR0ge2+vuUBk+fZfn2+l+3Q2jmAWaRGDg==
dependencies:
"@mdx-js/react" "^3.0.0"
"@storybook/csf-plugin" "10.5.4"
"@storybook/csf-plugin" "10.5.5"
"@storybook/icons" "^2.0.2"
"@storybook/react-dom-shim" "10.5.4"
"@storybook/react-dom-shim" "10.5.5"
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.4":
version "10.5.4"
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.4.tgz#750cb3cb74693e482e48c945f1bd29252ab1dc52"
integrity sha512-DSp5Z/eZlRnKq0KrKLJE6uoYf/Ysc+FP0Z5DVTGnOrie+z3tC0lNi9I4RB++EXkJeUDS9/4dxvJZVSWjhLlXxw==
"@storybook/csf-plugin@10.5.5":
version "10.5.5"
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.5.tgz#bae774f81987a0114f525d7e90a2b1e6ddf535ba"
integrity sha512-/euibhRFqklYCZqUseokojmfYcQpXshVY2QmA1qCuxMz9SzVFD3iSTw+aFLTxpsJGGdcZJk8fnm/rEthLzZ9jA==
dependencies:
unplugin "^2.3.5"
@@ -4125,10 +4125,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.4":
version "10.5.4"
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.4.tgz#c92bb6d8b26aaa4523efa5a8b99178a0a76a3d52"
integrity sha512-YdlppEOReg8MvTECRNuf79gu2zL83JqKDHIR/65eS0M6y+ue9pkpfjYo7hZVIcyOcRd9npBDXMdt2kC92bCuaA==
"@storybook/react-dom-shim@10.5.5":
version "10.5.5"
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.5.tgz#d60aa45a1174b2b404305c0d932cb43854afadf7"
integrity sha512-PIk7N3LLrZIxfNxmkvmQN1d5UQ70XEedT8n0GhBiXnM6XL09xPGB8n8TZXeJBRYluKhDQcAyQeT0/OZmcDVQJg==
"@superset-ui/core@^0.20.4":
version "0.20.4"
@@ -4787,86 +4787,86 @@
dependencies:
apg-lite "^1.0.4"
"@swc/core-darwin-arm64@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.46.tgz#393903c7eda790dbd89abd8fa0afdd9041543e5f"
integrity sha512-IsISIT22EfktVJrlvIpnAxG2u/A9aob9l99HMlx80x72WlFmFPk1V3UhkEzx86eJP8hw049KTFv/RISho2cq2Q==
"@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-x64@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.46.tgz#ddf16787e320636621180df480a3490fd9a868ca"
integrity sha512-4Tj4ppVIPCmUMpmGFiGtyEriwLyJ+yi/US4WfBrP/ok8COGddDZXLEzQETnKyK46mjvr1v0jevrS23zjoff7vA==
"@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-linux-arm-gnueabihf@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.46.tgz#7bee01b7311c43b913771ef9c7012931871de73b"
integrity sha512-i8tUGnNjyOgMmfmgFSg4aeJLQoFyfpIHK5FjpQAwpRyQIqEUB2w1e8zIDQzY1WhOxx8NoS1S5iUL813Un4Sf5A==
"@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-arm64-gnu@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.46.tgz#964596d757d18f04a02873d85a3660416c09c187"
integrity sha512-c0OnhqzdhfOvv6qhNCcByepB+sNYOGZyhtr2Qa6ZCHvAWTYhSRw4j/u92Stue9PbZ/6q74b9nHzi76+kVzqQHQ==
"@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-musl@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.46.tgz#213d3ece772689a8166ed51064836346c6ce1c2a"
integrity sha512-imyRpNEcUzFQFV2LE4jL68ErvmKEuZCbvZru77iQREunJ+bR4i658cupTgtG1mLYM3F1Tzy3Sb9xYb02KghWTg==
"@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-ppc64-gnu@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.46.tgz#4d2ec554103c6bef60cc1e294f374ea5a5edaf78"
integrity sha512-ctEfcl/HcUeomK33cbySiHZm98GEDIxTm1EkpBsYCiHxElYBzvTXVeuQT2YwbUXn9XCrjiw4ipyUNk33k26qRg==
"@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-s390x-gnu@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.46.tgz#097a19792ec22e2f51f6bfac02da1e0b3f5e5bb1"
integrity sha512-DxlMdnt84TtRVTv7WL/thWyz9+QU8QZNNoAP9rrk0P68LziuhfePp8MjQ44zIprpTHTsEwyziIuGUUN5iSC1bQ==
"@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-x64-gnu@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.46.tgz#39c1ca215f9ca643a4aa3ca6250cc38ba5f5c673"
integrity sha512-SKxI7J6t90XPl8hRUqtJi9NfGdunN/E/vZMc7Bc0figeRdOPDBT+Tm8g7cx9xM0T0mewh2l+8dewa3Am27/P+A==
"@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-musl@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.46.tgz#323a720bc965fffeedacdc3167b46a291553b5e0"
integrity sha512-qj9T6B7bosI0VEsrWOVXZN1OXxS8Tp63ywyrLxNdOycnUtLdkgYcoBsN5y8ImnDDsnwrEWZOy1e+J4xSe7mA3Q==
"@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-win32-arm64-msvc@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.46.tgz#9c2cfd2a59be74671a018097b8914f8cfbcc698d"
integrity sha512-8p7l4c3LU+eA5g9Et1JPhNeMC1oQwXTGU+uah8DPIBX7YXzqswvaBtyKVmXefVGi/DJU1x3YJsc3mbAp9aWzSQ==
"@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-ia32-msvc@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.46.tgz#bd7bd009a47b0f9826212e7ed36385d32fe193d8"
integrity sha512-tUEnfr3Bn9u6FOjUb3PN9p+09qZC2j+wNDLKHzXXZn22rqGcUqR/ohCRSS+nG9B9+X+U+3FewNEHJkTmdIvMjQ==
"@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-x64-msvc@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.46.tgz#8371845a5bdb330cf05b009f602bb8c4636c6beb"
integrity sha512-Vux7UDzBJYQggSuPfcl2w9iu+IJpgpRCxHzgCaVkELnAXAE4XZMOTX9HNcaNiwfeIDqdu2rkr69RuDm6wY8neA==
"@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@^1.15.40", "@swc/core@^1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.46.tgz#8acc0f68ee55010fdc876adf2a8faf0b097c681b"
integrity sha512-Ri3em2mBpq3h2zSPliCYl63otDGqek8PPEfv2nWgRQEbZ/VBCNyypVTVQ6cEbTCXBhy+WE2T3fQb08moIyuYaw==
"@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==
dependencies:
"@swc/counter" "^0.1.3"
"@swc/types" "^0.1.27"
optionalDependencies:
"@swc/core-darwin-arm64" "1.15.46"
"@swc/core-darwin-x64" "1.15.46"
"@swc/core-linux-arm-gnueabihf" "1.15.46"
"@swc/core-linux-arm64-gnu" "1.15.46"
"@swc/core-linux-arm64-musl" "1.15.46"
"@swc/core-linux-ppc64-gnu" "1.15.46"
"@swc/core-linux-s390x-gnu" "1.15.46"
"@swc/core-linux-x64-gnu" "1.15.46"
"@swc/core-linux-x64-musl" "1.15.46"
"@swc/core-win32-arm64-msvc" "1.15.46"
"@swc/core-win32-ia32-msvc" "1.15.46"
"@swc/core-win32-x64-msvc" "1.15.46"
"@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/counter@^0.1.3":
version "0.1.3"
@@ -6453,10 +6453,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.4, baseline-browser-mapping@^2.9.19:
version "2.11.4"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.4.tgz#3da1a877a8be2ec06495123ce994b43af58cc55e"
integrity sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ==
baseline-browser-mapping@^2.10.38, baseline-browser-mapping@^2.11.8, baseline-browser-mapping@^2.9.19:
version "2.11.8"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.8.tgz#42eb5ffc99fd89ca8bf30e31557b487063eec86f"
integrity sha512-zAgkquC2WYF0PIc6XbNYkA2uuxxFavzgmX61R+dHDUa558V8Ejf8ozTZFR6QzM24RWu4kBcRkhJ5kpz77j9fnQ==
batch@0.6.1:
version "0.6.1"
@@ -7938,10 +7938,10 @@ doctrine@^2.1.0:
dependencies:
esutils "^2.0.2"
docusaurus-plugin-openapi-docs@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-5.1.2.tgz#1ef66a26d80e2a3baf4caae4e72a0dcf85c6d608"
integrity sha512-xlgl/8+2iqSQWdhTJay0lfXqWduaGVTYrn+9i+uT3Uk5gkeDjfQOIVnHGnIHfKg0pUjIm008fvEQVAxk3TvgaQ==
docusaurus-plugin-openapi-docs@^5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-5.1.3.tgz#b8cd5f8451aaf881deb1a744a8295685f1681865"
integrity sha512-HnpblSBdXoR39VNTIW9zWERUsMJxXOpvdQoBKyaTkUBPwCM48Z76+ndo2yO2vADq+EhWjJlfxL1DUzCrgNjThQ==
dependencies:
"@apidevtools/json-schema-ref-parser" "^15.3.3"
"@redocly/openapi-core" "^2.25.2"
@@ -7959,10 +7959,10 @@ docusaurus-plugin-openapi-docs@^5.1.2:
swagger2openapi "^7.0.8"
xml-formatter "^3.6.6"
docusaurus-theme-openapi-docs@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-5.1.2.tgz#8591e3be971920c5eb347b9aab4bac77db5c7c31"
integrity sha512-uJx+a4qHmpOnl+e7YzsPJSga6d6TLGEVZZPtI7KkzC/PgG7bS8E4P8hknvWqmAEahxhLWEb8izIA3up/0g9RwA==
docusaurus-theme-openapi-docs@^5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-5.1.3.tgz#e23644a63785352abbc76e42760c0dfdff3669e1"
integrity sha512-npbD1QahtjAEmrOet/86i5fTmcJX4/rPhVT+c0qKjm7StUNbyqjwchSVBQuU1rB69T51JOA9TpT/y6QcB9Xjvw==
dependencies:
"@hookform/error-message" "^2.0.1"
"@reduxjs/toolkit" "^2.8.2"
@@ -7973,7 +7973,7 @@ docusaurus-theme-openapi-docs@^5.1.2:
crypto-js "^4.2.0"
file-saver "^2.0.5"
lodash "^4.17.21"
pako "^2.1.0"
pako "^3.0.1"
path-browserify "^1.0.1"
postman-code-generators "^2.0.0"
postman-collection "^5.0.2"
@@ -7988,7 +7988,7 @@ docusaurus-theme-openapi-docs@^5.1.2:
rehype-raw "^7.0.0"
remark-gfm "4.0.1"
sass "^1.89.2"
sass-loader "^16.0.5"
sass-loader "^17.0.0"
unist-util-visit "^5.0.0"
url "^0.11.4"
xml-formatter "^3.6.6"
@@ -8048,9 +8048,9 @@ domhandler@^5.0.2, domhandler@^5.0.3:
domelementtype "^2.3.0"
dompurify@^3.3.3, dompurify@^3.4.12:
version "3.4.12"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.12.tgz#6fa2265e9bbdce882c4ace4107626051b448ffa8"
integrity sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==
version "3.4.13"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.13.tgz#fc28949d59f92d62e28a3a764bcbeee35897a1be"
integrity sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==
optionalDependencies:
"@types/trusted-types" "^2.0.7"
@@ -8151,7 +8151,7 @@ encodeurl@~2.0.0:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
enhanced-resolve@^5.24.2:
enhanced-resolve@^5.24.4:
version "5.24.5"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz#b4dad3255b7545f07ba5535189868e9f85f47573"
integrity sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==
@@ -10949,9 +10949,9 @@ merge2@^1.3.0, merge2@^1.4.1:
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
mermaid@>=11.6.0:
version "11.16.0"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-11.16.0.tgz#dc946bc84bde9d093ba14940d49df1d9f7d8c32f"
integrity sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==
version "11.16.1"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-11.16.1.tgz#57ae2342f6c45b967113b04c9258430bdd057ee8"
integrity sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g==
dependencies:
"@braintree/sanitize-url" "^7.1.2"
"@iconify/utils" "^3.0.2"
@@ -11737,9 +11737,9 @@ mz@^2.7.0:
thenify-all "^1.0.0"
nanoid@^3.3.16:
version "3.3.16"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.16.tgz#a04d8ec4b1f10009d2d533947aefe4293737816c"
integrity sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==
version "3.3.18"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.18.tgz#f66a2de1199ffde0fcf21c8a5f13106b1c081913"
integrity sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==
natural-compare@^1.4.0:
version "1.4.0"
@@ -12290,10 +12290,10 @@ package-manager-detector@^1.3.0:
resolved "https://registry.yarnpkg.com/package-manager-detector/-/package-manager-detector-1.7.0.tgz#0a6d6d3856627b8ac9331f95fc891ea81247aafd"
integrity sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==
pako@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/pako/-/pako-2.2.0.tgz#246b9d4c841a8d308e484c0d03786651b143e63a"
integrity sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==
pako@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/pako/-/pako-3.0.1.tgz#3156f8fa86e3cc2ae5a36369475b14d4f05696ce"
integrity sha512-GupotUUI0mlhugKjUs4bjOwLt3nrehy9Ys2dxC0GtgVef5cnKggkDMmf2bq2poCCuVXopWPmqsc9VDT2iJUy+w==
param-case@^3.0.4:
version "3.0.4"
@@ -14182,12 +14182,10 @@ safe-regex-test@^1.1.0:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sass-loader@^16.0.5:
version "16.0.8"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-16.0.8.tgz#b7eef3b7947b32e2988dd760a2f7f0d547838e6b"
integrity sha512-hcov4ZwZJIGbEuyNr9EmiTmZueyrxSToE6GOzoZnq5JM7ecRO7ttyvilPn+VmRsqiP16+VYZzVnGZj/hzZgKBA==
dependencies:
neo-async "^2.6.2"
sass-loader@^17.0.0:
version "17.0.0"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-17.0.0.tgz#8f4f8864d834c1abb18f0f3d2bc653bbf29dde2a"
integrity sha512-0Ybm8ohBQ9LcrycVrFQp/KQBNX5a3Wda9/smS0mE/xLffzEnwvV8nykOzrbiSWNzTE3IB/jiXx8O4QmDPG2+Gw==
sass@^1.89.2:
version "1.101.0"
@@ -14702,10 +14700,10 @@ stop-iteration-iterator@^1.1.0:
es-errors "^1.3.0"
internal-slot "^1.1.0"
storybook@^10.5.4:
version "10.5.4"
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.4.tgz#3190fc10568053bd32f198ef164c045904cd2582"
integrity sha512-bmLxPsxVSPnbeiZqYQpozyNOiJXfk+pf7WfHZflvPkwT6Y+rvYz3Cj/D6H4Kf2jHpuDNiMXBKO3yawLN2OWirg==
storybook@^10.5.5:
version "10.5.5"
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.5.tgz#6b720e7cf5cfba1a06f5f1c9ab476e6af27cd72a"
integrity sha512-UscBIBJDloUeqntukHOhP1a5W/vouePDJbzPSxj466WK801FZtzQiMffMtkjzJiWSuj20wfaYlB2QQKh9aOYAg==
dependencies:
"@storybook/global" "^5.0.0"
"@storybook/icons" "^2.0.2"
@@ -14723,7 +14721,7 @@ storybook@^10.5.4:
recast "^0.23.5"
semver "^7.7.3"
use-sync-external-store "^1.5.0"
ws "^8.18.0"
ws "^8.21.1"
string-convert@^0.2.0:
version "0.2.1"
@@ -15945,10 +15943,10 @@ webpack-virtual-modules@^0.6.2:
resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
webpack@^5.109.0, webpack@^5.88.1, webpack@^5.95.0:
version "5.109.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.109.0.tgz#871d8eee5e2d5e6eaf5ec8d1a6db74ea65491030"
integrity sha512-vomrngskVVXEZF9sMZfYAd4pXZUnfaWdJGlF+BTNF+gJBCKYCQBnOeVPlrh39Ewl7nlCsirDplMy6o5g9xJHBg==
webpack@^5.109.2, webpack@^5.88.1, webpack@^5.95.0:
version "5.109.2"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.109.2.tgz#b58dc289561c3282db35c210a99379a836a4c28d"
integrity sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw==
dependencies:
"@types/estree" "^1.0.8"
"@types/json-schema" "^7.0.15"
@@ -15958,7 +15956,7 @@ webpack@^5.109.0, webpack@^5.88.1, webpack@^5.95.0:
acorn "^8.16.0"
browserslist "^4.28.1"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.24.2"
enhanced-resolve "^5.24.4"
es-module-lexer "^2.1.0"
eslint-scope "5.1.1"
events "^3.2.0"
@@ -16118,7 +16116,7 @@ write-file-atomic@^3.0.3:
signal-exit "^3.0.2"
typedarray-to-buffer "^3.1.5"
ws@^7.3.1, ws@^8.18.0, ws@^8.21.0:
ws@^7.3.1, ws@^8.18.0, ws@^8.21.0, ws@^8.21.1:
version "8.21.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.0.tgz#012e413fc07429945121b0c153158c4343086951"
integrity sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==
+84 -29
View File
@@ -60,18 +60,27 @@ dependencies = [
"flask-login>=0.6.0, < 1.0",
"flask-migrate>=4.1.0, <5.0",
"flask-session>=0.4.0, <1.0",
# Pinned explicitly below 3.0: 3.0.5 resolves without conflict and
# supports both SQLAlchemy 1.4 and 2.0, but real CI runs surfaced a
# structural incompatibility with Superset's current session/app-context
# handling across Celery task boundaries (see PR #42542) -- widespread
# "NoneType has no attribute X" failures and MySQL lock-wait timeouts,
# not just a connection-pool quirk. Needs dedicated investigation, not a
# driver-compat-prep bump; revisit alongside the actual SQLAlchemy 2.0
# core bump (discussion #40273, step 6).
"flask-sqlalchemy>=2.5.1, <4.0",
"flask-wtf>=1.3.0, <2.0",
"geopy",
"greenlet<=3.5.4, >=3.5.4",
"gunicorn>=26.0.0, <27; sys_platform != 'win32'",
"hashids>=1.3.1, <2",
# holidays>=0.45 required for security fix
"holidays>=0.99, <1",
"holidays>=0.101, <1",
"humanize",
"isodate",
"jsonpath-ng>=1.8.0, <2",
"Mako>=1.2.2",
"markdown>=3.10.2",
"markdown>=3.10.3",
# marshmallow 4 compatibility: see superset/marshmallow_compatibility.py for a
# Flask-AppBuilder workaround. Tracking issue:
# https://github.com/apache/superset/issues/33162
@@ -79,7 +88,7 @@ dependencies = [
"marshmallow-union>=0.1.15.post1",
"msgpack>=1.2.0, <1.3",
"nh3>=0.3.5, <0.4",
"numpy>1.23.5, <2.3",
"numpy>=1.23.5, <2.5",
"packaging",
# --------------------------
# pandas and related (wanting pandas[performance] without numba as it's 100+MB and not needed)
@@ -99,17 +108,17 @@ dependencies = [
"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
"pyyaml>=6.0.3, <7.0.0",
"PyJWT>=2.4.0, <3.0",
"redis>=5.0.0, <9.0",
"redis>=8.1.0, <9.0",
"rison>=2.0.1, <3.0",
"selenium>=4.45.0, <5.0",
"shillelagh[gsheetsapi]>=1.4.4, <2.0",
"selenium>=4.46.0, <5.0",
"shillelagh[gsheetsapi]>=1.4.5, <2.0",
"sshtunnel>=0.4.0, <0.5",
"simplejson>=4.1.1",
"slack_sdk>=3.43.0, <4",
"sqlalchemy>=1.4.43, <2", # 1.4.43 adds the python-oracledb (oracle+oracledb) dialect
"sqlalchemy-continuum>=1.6.0, <2.0.0",
"sqlalchemy-utils>=0.42.1, <0.43", # expanding lowerbound to work with pydoris
"sqlglot>=30.12.0, <31",
"sqlglot>=30.14.0, <31",
# newer pandas needs 0.9+
"tabulate>=0.10.0, <1.0",
"typing-extensions>=4.16.0, <5",
@@ -122,16 +131,26 @@ dependencies = [
[project.optional-dependencies]
athena = ["pyathena[pandas]>=3.35.2, <4"]
athena = ["pyathena[pandas]>=3.35.4, <4"]
# No SQLAlchemy 2.0 support anywhere in this dialect's ecosystem today: our
# own preset-io/sqlalchemy-aurora-data-api fork is dead since 2021, and the
# more active community fork (cloud-utils/sqlalchemy-aurora-data-api) has an
# unresolved SQLAlchemy 2.0 break (upstream issue #43). See
# superset/db_engine_specs/aurora.py's known_incompatibilities metadata.
aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
bigquery = [
"pandas-gbq>=0.35.0",
"sqlalchemy-bigquery>=1.17.0",
"google-cloud-bigquery>=3.42.2",
# 1.17.1 is likely the final release: googleapis/python-bigquery-sqlalchemy
# was archived 2026-05-16. Both 1.17.0 and 1.17.1 support SQLAlchemy 1.4/2.0.
"sqlalchemy-bigquery>=1.17.1",
"google-cloud-bigquery>=3.42.3",
]
clickhouse = ["clickhouse-connect>=1.6.0, <2.0"]
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
crate = ["sqlalchemy-cratedb>=0.41.0, <1"]
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
# known_incompatibilities metadata.
d1 = [
"superset-engine-d1>=0.1.0",
"sqlalchemy-d1>=0.1.0",
@@ -145,30 +164,47 @@ databricks = [
datafusion = ["flightsql-dbapi>=0.2.2, <0.3"]
db2 = ["ibm-db-sa<=0.4.4, >=0.4.4"]
denodo = ["denodo-sqlalchemy>=2.0.5,<2.1.0"]
dremio = ["sqlalchemy-dremio>=1.2.1, <4"]
drill = ["sqlalchemy-drill>=1.1.10, <2"]
# sqlalchemy-dremio 3.0.5+ hard-pins sqlalchemy~=2.0.41, dropping 1.4; 3.0.4
# is the last dual-compat release. Capped below 3.0.5 for now; widen back to
# <4 in lockstep with Superset's own SQLAlchemy 2.0 core bump (discussion
# #40273), not before.
dremio = ["sqlalchemy-dremio>=1.2.1, <3.0.5"]
# <2 was an artificial ceiling; upstream has no SQLAlchemy version cap and
# 1.1.10 already supports SQLAlchemy 2.0 (added `import_dbapi` in 1.1.7).
drill = ["sqlalchemy-drill>=1.1.10, <3"]
druid = ["pydruid>=0.6.5,<0.7"]
duckdb = ["duckdb>=1.5.4,<2", "duckdb-engine>=0.17.0"]
duckdb = ["duckdb>=1.5.5,<2", "duckdb-engine>=0.17.0"]
dynamodb = ["pydynamodb>=0.8.2"]
solr = ["sqlalchemy-solr >= 0.2.4.3"]
# Effectively unmaintained (only dependabot bumps since 2024); hard-pinned to
# SQLAlchemy ~1.4.7 upstream, no SQLAlchemy 2.0 work. See
# superset/db_engine_specs/solr.py's known_incompatibilities metadata.
solr = ["sqlalchemy-solr>=0.2.4.3"]
elasticsearch = ["elasticsearch-dbapi>=0.2.13, <0.3.0"]
exasol = ["sqlalchemy-exasol>=2.4.0, <8.0"]
# sqlalchemy-exasol cuts hard from SQLAlchemy 1.4-only (<6.0.0) to 2.0-only
# (>=6.0.0) with no dual-compat release. Capped below 6.0.0 for now; bump to
# >=6.0.0,<8.0 in lockstep with Superset's own SQLAlchemy 2.0 core bump
# (discussion #40273), not before.
exasol = ["sqlalchemy-exasol>=2.4.0, <6.0.0"]
excel = ["xlrd>=2.0.2, <2.1"]
# Async dashboard "Export Data/Images to Excel": uploads the workbook to S3 and
# emails a pre-signed link. boto3 is imported lazily by superset.utils.s3, so
# installing this extra is only required to actually run exports.
excel-export = ["boto3"]
fastmcp = [
"fastmcp>=3.4.3,<4.0",
"fastmcp>=3.4.5,<4.0",
# tiktoken backs the response-size-guard token estimator. Without
# it, the middleware falls back to a coarser character-based
# heuristic that under-counts JSON-heavy MCP responses.
"tiktoken>=0.13.0,<1.0",
]
firebird = ["sqlalchemy-firebird>=0.8.0, <2.2"]
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
gevent = ["gevent>=26.4.0"]
gsheets = ["shillelagh[gsheetsapi]>=1.4.4, <2"]
# 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
# release. Capped below 2.0.0 for now; bump to >=2.2.0 in lockstep with
# Superset's own SQLAlchemy 2.0 core bump (discussion #40273), not before.
firebird = ["sqlalchemy-firebird>=0.8.0, <2.0.0"]
firebolt = ["firebolt-sqlalchemy>=1.1.2, <2"]
gevent = ["gevent>=26.7.0"]
gsheets = ["shillelagh[gsheetsapi]>=1.4.5, <2"]
hana = ["hdbcli==2.29.25", "sqlalchemy_hana==3.0.3"]
hive = [
"pyhive[hive_pure_sasl]>=0.7.0",
@@ -177,6 +213,9 @@ hive = [
"thrift_sasl>=0.4.3, < 1.0.0",
]
impala = ["impyla>=0.24.0, <0.25"]
# Actively maintained upstream, but setup.py on main hard-pins
# sqlalchemy==1.4.*, no SQLAlchemy 2.0 work yet. See
# superset/db_engine_specs/kusto.py's known_incompatibilities metadata.
kusto = ["sqlalchemy-kusto>=3.1.2, <4"]
kylin = ["kylinpy>=2.8.4, <2.9"]
mssql = ["pymssql>=2.3.13, <3"]
@@ -184,7 +223,10 @@ mssql = ["pymssql>=2.3.13, <3"]
motherduck = ["apache-superset[duckdb]"]
mysql = ["mysqlclient>=2.2.8, <3"]
ocient = [
"sqlalchemy-ocient>=1.0.0, <4",
# 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.
"sqlalchemy-ocient>=3.0.0, <4",
"pyocient>=1.0.15, <4",
"shapely",
"geojson",
@@ -192,14 +234,22 @@ ocient = [
oracle = ["oracledb>=4.0.2, <5"]
parseable = ["sqlalchemy-parseable>=0.1.6,<0.2.0"]
pinot = ["pinotdb>=5.0.0, <10.0.0"]
playwright = ["playwright>=1.61.0, <2"]
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"]
# sqlalchemy-redshift cuts hard from SQLAlchemy 1.4-only (0.8.x) to 2.0-only
# (>=1.0.0) with no dual-compat release; the existing <0.9 ceiling already
# keeps this on the 1.4-only line. Bump to >=1.0.0 in lockstep with
# Superset's own SQLAlchemy 2.0 core bump (discussion #40273), not before.
redshift = ["sqlalchemy-redshift>=0.8.1, <0.9"]
risingwave = ["sqlalchemy-risingwave"]
shillelagh = ["shillelagh[all]>=1.4.4, <2"]
# No release of sqlalchemy-risingwave has ever supported both SQLAlchemy 1.4
# and 2.0 (version numbers don't track SQLAlchemy compat monotonically); pin
# to the newest 1.4-only release for now. Bump to >=2.0.0 in lockstep with
# Superset's own SQLAlchemy 2.0 core bump (discussion #40273), not before.
risingwave = ["sqlalchemy-risingwave>=1.4.1, <3.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"]
@@ -209,14 +259,14 @@ spark = [
"thrift>=0.24.0, <1",
]
tdengine = [
"taospy>=2.8.9",
"taospy>=2.8.10",
"taos-ws-py>=0.7.0"
]
teradata = ["teradatasql>=20.0.0.62"]
teradata = ["teradatasql>=20.0.0.64"]
thumbnails = [] # deprecated, will be removed in 7.0
vertica = ["sqlalchemy-vertica-python>= 0.6.3, < 0.7"]
netezza = ["nzalchemy>= 11.1.2, < 11.2"]
starrocks = ["starrocks>=1.3.3, <2"]
starrocks = ["starrocks>=1.3.4, <2"]
doris = ["pydoris>=1.2.0, <2.0.0"]
oceanbase = ["oceanbase_py>=0.0.1.2"]
ydb = ["ydb-sqlalchemy>=0.1.22", "ydb-sqlglot-plugin>=0.2.8"]
@@ -236,7 +286,7 @@ development = [
"progress>=1.6.1,<2",
"psutil",
"pyfakefs",
"pyinstrument>=5.1.2,<6",
"pyinstrument>=5.1.3,<6",
"pylint",
"pytest<10.0.0", # cap below the next major until validated; the earlier <8 pin (current_app proxy timing) no longer reproduces
"pytest-asyncio",
@@ -485,6 +535,11 @@ authorized_licenses = [
# Seems ok, might need legal review
# https://github.com/urschrei/pypolyline/blob/master/LICENSE.md
polyline = "2"
# NumPy 2.x reports a combined SPDX license expression covering vendored
# code (BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0), all of which
# are permissive OSI-approved licenses; the package itself remains
# BSD-3-Clause. https://github.com/numpy/numpy/blob/main/LICENSE.txt
numpy = "2"
# --------------------------------------------------------------
# TODO REMOVE THESE DEPS FROM CODEBASE
+3
View File
@@ -30,9 +30,12 @@ filterwarnings =
always::sqlalchemy.exc.RemovedIn20Warning
error:Passing a string to Connection.execute\(\) is deprecated:sqlalchemy.exc.RemovedIn20Warning
error:"Query" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"ReportExecutionLog" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"ReportRecipients" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"SavedQuery" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"SqlaTable" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"SqlMetric" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"SSHTunnel" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"TableColumn" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"TaggedObject" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:The autoload parameter is deprecated:sqlalchemy.exc.RemovedIn20Warning
+8 -7
View File
@@ -150,6 +150,7 @@ flask-session==0.8.0
# via apache-superset (pyproject.toml)
flask-sqlalchemy==2.5.1
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
# flask-migrate
flask-talisman==1.1.0
@@ -177,7 +178,7 @@ h11==0.16.0
# via wsproto
hashids==1.3.1
# via apache-superset (pyproject.toml)
holidays==0.100
holidays==0.102
# via apache-superset (pyproject.toml)
humanize==4.12.3
# via apache-superset (pyproject.toml)
@@ -218,7 +219,7 @@ mako==1.3.12
# -r requirements/base.in
# apache-superset (pyproject.toml)
# alembic
markdown==3.10.2
markdown==3.10.3
# via apache-superset (pyproject.toml)
markdown-it-py==3.0.0
# via rich
@@ -250,7 +251,7 @@ nh3==0.3.6
# via apache-superset (pyproject.toml)
numexpr==2.10.2
# via -r requirements/base.in
numpy==1.26.4
numpy==2.4.6
# via
# apache-superset (pyproject.toml)
# bottleneck
@@ -356,7 +357,7 @@ pyyaml==6.0.3
# via
# apache-superset (pyproject.toml)
# apispec
redis==8.0.1
redis==8.1.0
# via apache-superset (pyproject.toml)
referencing==0.36.2
# via
@@ -379,11 +380,11 @@ rpds-py==0.25.0
# via
# jsonschema
# referencing
selenium==4.45.0
selenium==4.46.0
# via apache-superset (pyproject.toml)
setuptools==80.9.0
# via -r requirements/base.in
shillelagh==1.4.4
shillelagh==1.4.5
# via apache-superset (pyproject.toml)
simplejson==4.1.1
# via apache-superset (pyproject.toml)
@@ -417,7 +418,7 @@ sqlalchemy-utils==0.42.1
# apache-superset (pyproject.toml)
# apache-superset-core
# flask-appbuilder
sqlglot==30.12.0
sqlglot==30.15.0
# via
# apache-superset (pyproject.toml)
# apache-superset-core
+15 -14
View File
@@ -225,7 +225,7 @@ docstring-parser==0.17.0
# via cyclopts
docutils==0.22.2
# via rich-rst
duckdb==1.5.4
duckdb==1.5.5
# via
# apache-superset
# duckdb-engine
@@ -242,9 +242,9 @@ et-xmlfile==2.0.0
# openpyxl
exceptiongroup==1.3.0
# via fastmcp-slim
fastmcp==3.4.4
fastmcp==3.4.5
# via apache-superset
fastmcp-slim==3.4.4
fastmcp-slim==3.4.5
# via fastmcp
filelock==3.20.3
# via
@@ -312,6 +312,7 @@ flask-session==0.8.0
flask-sqlalchemy==2.5.1
# via
# -c requirements/base-constraint.txt
# apache-superset
# flask-appbuilder
# flask-migrate
flask-talisman==1.1.0
@@ -339,7 +340,7 @@ geopy==2.4.1
# via
# -c requirements/base-constraint.txt
# apache-superset
gevent==26.4.0
gevent==26.7.0
# via apache-superset
google-api-core==2.33.0
# via
@@ -362,7 +363,7 @@ google-auth-oauthlib==1.2.1
# via
# pandas-gbq
# pydata-google-auth
google-cloud-bigquery==3.42.2
google-cloud-bigquery==3.43.0
# via
# apache-superset
# pandas-gbq
@@ -407,7 +408,7 @@ hashids==1.3.1
# via
# -c requirements/base-constraint.txt
# apache-superset
holidays==0.100
holidays==0.102
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -520,7 +521,7 @@ mako==1.3.12
# -c requirements/base-constraint.txt
# alembic
# apache-superset
markdown==3.10.2
markdown==3.10.3
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -580,7 +581,7 @@ nh3==0.3.6
# apache-superset
nodeenv==1.8.0
# via pre-commit
numpy==1.26.4
numpy==2.4.6
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -775,7 +776,7 @@ pygments==2.20.0
# rich
pyhive==0.7.0
# via apache-superset
pyinstrument==5.1.2
pyinstrument==5.1.3
# via apache-superset
pyjwt==2.13.0
# via
@@ -873,7 +874,7 @@ pyyaml==6.0.3
# fastmcp-slim
# jsonschema-path
# pre-commit
redis==8.0.1
redis==8.1.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -933,7 +934,7 @@ s3transfer==0.16.0
# via boto3
secretstorage==3.5.0
# via keyring
selenium==4.45.0
selenium==4.46.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -947,7 +948,7 @@ setuptools==80.9.0
# pydata-google-auth
# zope-event
# zope-interface
shillelagh==1.4.4
shillelagh==1.4.5
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -989,7 +990,7 @@ sqlalchemy==1.4.54
# sqlalchemy-bigquery
# sqlalchemy-continuum
# sqlalchemy-utils
sqlalchemy-bigquery==1.17.0
sqlalchemy-bigquery==1.17.1
# via apache-superset
sqlalchemy-continuum==1.7.0
# via
@@ -1001,7 +1002,7 @@ sqlalchemy-utils==0.42.1
# apache-superset
# apache-superset-core
# flask-appbuilder
sqlglot==30.12.0
sqlglot==30.15.0
# via
# -c requirements/base-constraint.txt
# apache-superset
+3 -3
View File
@@ -3042,9 +3042,9 @@
"dev": true
},
"node_modules/fast-uri": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
"integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
"dev": true,
"funding": [
{
@@ -1,82 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SAMPLE_DASHBOARD_1 } from 'cypress/utils/urls';
import { drag } from 'cypress/utils';
import { interceptGet } from './utils';
import { interceptFiltering as interceptCharts } from '../explore/utils';
function editDashboard() {
cy.getBySel('edit-dashboard-button').click();
}
function dragComponent(
component = 'Unicode Cloud',
target = 'card-title',
withFiltering = true,
) {
if (withFiltering) {
cy.getBySel('dashboard-charts-filter-search-input').type(component, {
force: true,
});
cy.wait('@filtering');
}
cy.wait(500);
drag(`[data-test="${target}"]`, component).to(
'[data-test="grid-content"] [data-test="dragdroppable-object"]',
);
}
function visitEdit(sampleDashboard = SAMPLE_DASHBOARD_1) {
interceptCharts();
interceptGet();
if (sampleDashboard === SAMPLE_DASHBOARD_1) {
cy.createSampleDashboards([0]);
}
cy.visit(sampleDashboard);
cy.wait('@get');
editDashboard();
cy.get('.grid-container').should('exist');
cy.wait('@filtering');
cy.wait(500);
}
describe('Dashboard edit', () => {
describe('Components', () => {
beforeEach(() => {
visitEdit();
});
it('should add charts', () => {
cy.get('body').then($body => {
if ($body.find('.ant-modal-wrap').length > 0) {
cy.get('body').type('{esc}', { force: true });
cy.wait(1000);
cy.get('.ant-modal-close').click({ force: true });
cy.wait(500);
}
});
cy.get('input[type="checkbox"]').scrollIntoView();
cy.get('input[type="checkbox"]').click({ force: true });
dragComponent();
cy.getBySel('dashboard-component-chart-holder').should('have.length', 1);
});
});
});
@@ -138,6 +138,7 @@ export function prepareDashboardFilters(
chart_customization_config: [],
timed_refresh_immune_slices: [],
expanded_slices: {},
expand_all_slices: false,
refresh_frequency: 0,
color_scheme: '',
label_colors: {},
@@ -79,34 +79,6 @@ export function waitForChartLoad(chart: ChartSpec) {
});
}
/**
* Drag an element and drop it to another element.
* Usage:
* drag(source).to(target);
*/
export function drag(selector: string, content: string | number | RegExp) {
const dataTransfer = { data: {} };
return {
to(target: string | Cypress.Chainable) {
cy.get('.dragdroppable')
.contains(selector, content)
.trigger('mousedown', { which: 1, force: true });
cy.get('.dragdroppable')
.contains(selector, content)
.trigger('dragstart', { dataTransfer, force: true });
cy.get('.dragdroppable')
.contains(selector, content)
.trigger('drag', { force: true });
(typeof target === 'string' ? cy.get(target) : target)
.trigger('dragover', { dataTransfer, force: true })
.trigger('drop', { dataTransfer, force: true })
.trigger('dragend', { dataTransfer, force: true })
.trigger('mouseup', { which: 1, force: true });
},
};
}
export function resize(selector: string) {
return {
to(cordX: number, cordY: number) {
+69 -6
View File
@@ -2086,6 +2086,14 @@
"node": ">=8"
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -2099,6 +2107,18 @@
"node": ">=8"
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz",
"integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -4296,6 +4316,18 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/esquery": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
@@ -4456,9 +4488,9 @@
"peer": true
},
"node_modules/fast-uri": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
"integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
"funding": [
{
"type": "github",
@@ -7815,6 +7847,11 @@
"node": ">=8"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
},
"node_modules/sshpk": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
@@ -10179,6 +10216,14 @@
"resolve-from": "^5.0.0"
},
"dependencies": {
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
"sprintf-js": "~1.0.2"
}
},
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -10188,6 +10233,14 @@
"path-exists": "^4.0.0"
}
},
"js-yaml": {
"version": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz",
"integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==",
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -11927,6 +11980,11 @@
"eslint-visitor-keys": "^5.0.1"
}
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
},
"esquery": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
@@ -12052,9 +12110,9 @@
"peer": true
},
"fast-uri": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
"integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
"peer": true
},
"fd-slicer": {
@@ -14324,6 +14382,11 @@
"which": "^2.0.1"
}
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
},
"sshpk": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
+1414 -2482
View File
File diff suppressed because it is too large Load Diff
+31 -37
View File
@@ -65,7 +65,6 @@
"ensure-oxc": "echo 'OXC linter is ready' && npx oxlint --version",
"lint-stats": "node ./scripts/oxlint-metrics-uploader.js",
"plugins:build": "node ./scripts/build.js",
"plugins:build-assets": "node ./scripts/copyAssets.js",
"plugins:create-conventional-version": "npm run prune && lerna version --conventional-commits --create-release github --no-private --yes --tag-version-prefix=\"plugins-and-packages-v\"",
"plugins:create-minor-version": "npm run prune && lerna version minor --no-private --yes --tag-version-prefix=\"plugins-and-packages-v\"",
"plugins:create-patch-version": "npm run prune && lerna version patch --no-private --yes --tag-version-prefix=\"plugins-and-packages-v\"",
@@ -87,7 +86,7 @@
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80% --silent",
"test-loud": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80%",
"type": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" tsc --noEmit",
"update-maps": "cd plugins/legacy-plugin-chart-country-map/scripts && jupyter nbconvert --to notebook --execute --inplace --allow-errors --ExecutePreprocessor.timeout=1200 'Country Map GeoJSON Generator.ipynb'",
"update-maps": "cd plugins/plugin-chart-country-map/scripts && jupyter nbconvert --to notebook --execute --inplace --allow-errors --ExecutePreprocessor.timeout=1200 'Country Map GeoJSON Generator.ipynb'",
"validate-release": "../RELEASING/validate_this_release.sh"
},
"dependencies": {
@@ -128,24 +127,22 @@
"@scarf/scarf": "^1.4.0",
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
"@superset-ui/core": "file:./packages/superset-ui-core",
"@superset-ui/legacy-plugin-chart-calendar": "file:./plugins/legacy-plugin-chart-calendar",
"@superset-ui/legacy-plugin-chart-chord": "file:./plugins/legacy-plugin-chart-chord",
"@superset-ui/legacy-plugin-chart-country-map": "file:./plugins/legacy-plugin-chart-country-map",
"@superset-ui/legacy-plugin-chart-horizon": "file:./plugins/legacy-plugin-chart-horizon",
"@superset-ui/legacy-plugin-chart-paired-t-test": "file:./plugins/legacy-plugin-chart-paired-t-test",
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "file:./plugins/legacy-plugin-chart-parallel-coordinates",
"@superset-ui/legacy-plugin-chart-partition": "file:./plugins/legacy-plugin-chart-partition",
"@superset-ui/legacy-plugin-chart-rose": "file:./plugins/legacy-plugin-chart-rose",
"@superset-ui/legacy-plugin-chart-world-map": "file:./plugins/legacy-plugin-chart-world-map",
"@superset-ui/legacy-preset-chart-nvd3": "file:./plugins/legacy-preset-chart-nvd3",
"@superset-ui/plugin-chart-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table",
"@superset-ui/plugin-chart-calendar": "file:./plugins/plugin-chart-calendar",
"@superset-ui/plugin-chart-cartodiagram": "file:./plugins/plugin-chart-cartodiagram",
"@superset-ui/plugin-chart-chord": "file:./plugins/plugin-chart-chord",
"@superset-ui/plugin-chart-country-map": "file:./plugins/plugin-chart-country-map",
"@superset-ui/plugin-chart-echarts": "file:./plugins/plugin-chart-echarts",
"@superset-ui/plugin-chart-handlebars": "file:./plugins/plugin-chart-handlebars",
"@superset-ui/plugin-chart-horizon": "file:./plugins/plugin-chart-horizon",
"@superset-ui/plugin-chart-paired-t-test": "file:./plugins/plugin-chart-paired-t-test",
"@superset-ui/plugin-chart-parallel-coordinates": "file:./plugins/plugin-chart-parallel-coordinates",
"@superset-ui/plugin-chart-partition": "file:./plugins/plugin-chart-partition",
"@superset-ui/plugin-chart-pivot-table": "file:./plugins/plugin-chart-pivot-table",
"@superset-ui/plugin-chart-point-cluster-map": "file:./plugins/plugin-chart-point-cluster-map",
"@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table",
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
"@superset-ui/plugin-chart-world-map": "file:./plugins/plugin-chart-world-map",
"@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl",
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
"@types/d3-format": "^3.0.1",
@@ -169,8 +166,6 @@
"dom-to-image-more": "^3.10.2",
"dom-to-pdf": "^0.3.2",
"echarts": "^6.1.0",
"fast-glob": "^3.3.2",
"fs-extra": "^11.4.0",
"fuse.js": "^7.5.0",
"geolib": "^3.3.14",
"geostyler": "^18.6.0",
@@ -178,7 +173,7 @@
"geostyler-openlayers-parser": "^5.7.1",
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^10.9.1",
"google-auth-library": "^11.0.0",
"immer": "^11.1.15",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
@@ -188,24 +183,24 @@
"lodash": "^4.18.1",
"lodash-es": "^4.18.1",
"mapbox-gl": "^3.27.0",
"markdown-to-jsx": "^9.9.0",
"markdown-to-jsx": "^9.10.1",
"match-sorter": "^8.3.0",
"memoize-one": "^6.0.0",
"mousetrap": "^1.6.5",
"mustache": "^4.2.0",
"nanoid": "^6.0.0",
"ol": "^10.9.0",
"ol": "^10.10.0",
"query-string": "9.4.1",
"re-resizable": "^6.11.2",
"react": "^18.3.0",
"react-arborist": "^3.15.1",
"react-arborist": "^3.16.0",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^4.4.0",
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^18.3.0",
"react-google-recaptcha": "^3.1.0",
"react-intersection-observer": "^10.1.0",
"react-intersection-observer": "^11.0.0",
"react-json-tree": "^0.20.0",
"react-lines-ellipsis": "^0.16.1",
"react-loadable": "^5.5.0",
@@ -218,14 +213,14 @@
"react-table": "^7.8.0",
"react-transition-group": "^4.4.5",
"react-virtualized-auto-sizer": "^1.0.26",
"react-window": "^1.8.10",
"react-window": "^2.3.0",
"redux": "^4.2.1",
"redux-localstorage": "^0.4.1",
"redux-thunk": "^2.1.0",
"redux-undo": "^1.0.0-beta9-9-7",
"rison": "^0.1.1",
"scroll-into-view-if-needed": "^3.1.0",
"simple-zstd": "^2.1.0",
"simple-zstd": "^1.4.2",
"stream-browserify": "^3.0.0",
"tinycolor2": "^1.6.0",
"urijs": "^1.19.8",
@@ -257,14 +252,14 @@
"@emotion/jest": "^11.14.2",
"@formatjs/intl-durationformat": "^0.10.18",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.61.1",
"@playwright/test": "^1.62.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.5.4",
"@storybook/addon-links": "10.5.4",
"@storybook/react-webpack5": "10.5.4",
"@storybook/addon-docs": "10.5.5",
"@storybook/addon-links": "10.5.5",
"@storybook/react-webpack5": "10.5.5",
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.46",
"@swc/core": "^1.15.47",
"@swc/plugin-emotion": "^14.15.0",
"@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^10.4.1",
@@ -279,14 +274,13 @@
"@types/json-bigint": "^1.0.4",
"@types/lodash-es": "^4.17.12",
"@types/mousetrap": "^1.6.15",
"@types/node": "^26.1.1",
"@types/node": "^26.1.2",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@types/react-loadable": "^5.5.11",
"@types/react-redux": "^7.1.10",
"@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "^4.4.12",
"@types/react-window": "^1.8.8",
"@types/redux-localstorage": "^1.0.8",
"@types/redux-mock-store": "^1.0.6",
"@types/rison": "0.1.0",
@@ -298,7 +292,7 @@
"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.5",
"baseline-browser-mapping": "^2.11.8",
"cheerio": "1.2.0",
"concurrently": "^10.0.4",
"copy-webpack-plugin": "^14.0.0",
@@ -310,12 +304,12 @@
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
"eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest-dom": "^5.5.0",
"eslint-plugin-jest-dom": "^5.7.0",
"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.4",
"eslint-plugin-storybook": "10.5.5",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
@@ -328,14 +322,14 @@
"jest-html-reporter": "^4.4.0",
"jest-websocket-mock": "^2.5.0",
"js-yaml-loader": "^1.2.2",
"jsdom": "^29.1.1",
"lerna": "^9.0.4",
"jsdom": "^30.0.1",
"lerna": "^10.0.0",
"lightningcss": "^1.33.0",
"mini-css-extract-plugin": "^2.10.2",
"minimizer-webpack-plugin": "^5.6.1",
"open-cli": "^9.0.0",
"oxfmt": "^0.60.0",
"oxlint": "^1.74.0",
"oxfmt": "^0.61.0",
"oxlint": "^1.76.0",
"po2json": "^0.4.5",
"postcss-styled-syntax": "^0.7.2",
"process": "^0.11.10",
@@ -346,7 +340,7 @@
"source-map": "^0.8.0",
"source-map-support": "^0.5.21",
"speed-measure-webpack-plugin": "^1.6.0",
"storybook": "10.5.4",
"storybook": "10.5.5",
"style-loader": "^4.0.0",
"stylelint": "^17.14.1",
"swc-loader": "^0.2.7",
@@ -356,7 +350,7 @@
"typescript": "5.4.5",
"unzipper": "^0.12.5",
"wait-on": "^9.1.0",
"webpack": "^5.109.0",
"webpack": "^5.109.2",
"webpack-bundle-analyzer": "^5.3.1",
"webpack-cli": "^7.0.3",
"webpack-dev-server": "^5.2.5",
@@ -20,8 +20,7 @@
import { dirname, join } from 'path';
// @ts-ignore -- yeoman-test type resolution differs between local and Docker environments
import helpers from 'yeoman-test';
// @ts-ignore -- fs-extra/esm has no type declarations
import { copySync } from 'fs-extra/esm';
import { cpSync } from 'fs';
import { fileURLToPath } from 'url';
import pluginChartModule from '../generators/plugin-chart';
@@ -31,9 +30,10 @@ test('generator-superset:plugin-chart:creates files', async () => {
.onTargetDirectory((dir: string) => {
// `dir` is the path to the new temporary directory
const generatorDirname = dirname(fileURLToPath(import.meta.url));
copySync(
cpSync(
join(generatorDirname, '../generators/plugin-chart/templates'),
join(dir, 'unknown/templates'),
{ recursive: true },
);
})
.withPrompts({
@@ -109,7 +109,6 @@
"@types/lodash": "^4.17.24",
"@types/react": "*",
"@types/react-loadable": "*",
"@types/react-window": "^1.8.8",
"@types/tinycolor2": "*",
"typescript": "^5.0.0"
},
@@ -36,6 +36,7 @@ import type {
QueryResponse,
TimeFormatter,
} from '@superset-ui/core';
import { type RGBColor } from '@superset-ui/core/components';
import { GenericDataType } from '@apache-superset/core/common';
import { sharedControls, sharedControlComponents } from './shared-controls';
@@ -494,7 +495,7 @@ export type ConditionalFormattingConfig = {
targetValueLeft?: number;
targetValueRight?: number;
column?: string;
colorScheme?: string;
colorScheme?: RGBColor | string;
toAllRow?: boolean;
toTextColor?: boolean;
useGradient?: boolean;
@@ -0,0 +1,87 @@
/**
* 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 { ensureIsArray, getMetricLabel } from '@superset-ui/core';
import type { QueryFormMetric, QueryFormOrderBy } from '@superset-ui/core';
export interface BuildSortMetricOrderbyConfig {
/** The query's already-resolved metrics list. */
metrics: QueryFormMetric[];
/** The raw `timeseries_limit_metric` form-data value (single or multi). */
timeseriesLimitMetric?: QueryFormMetric | QueryFormMetric[] | null;
order_desc?: boolean;
/**
* Falls back to the first selected metric when no sort metric is set.
* Charts ported from a legacy viz whose query_obj always had a sort
* metric (defaulting to the first one) should set this; charts whose
* legacy query_obj left ordering absent without one should not.
*/
fallbackToFirstMetric?: boolean;
/**
* When true, only order when `order_desc` is set (matching legacy vizzes
* whose query_obj left the result unordered unless the operator asked
* for descending). When false, always order (ascending unless
* order_desc), matching legacy vizzes that ordered unconditionally.
*/
orderOnlyWhenDesc?: boolean;
}
export interface SortMetricOrderby {
/** `metrics`, with the sort metric appended if it wasn't already selected. */
metrics: QueryFormMetric[];
orderby: QueryFormOrderBy[];
}
/**
* Resolves a chart's sort metric and builds the corresponding query_obj
* `orderby`, appending the sort metric to `metrics` if it isn't already
* selected (so its value is present in the result to sort by). Several
* charts ported from the legacy chart-data pipeline share this exact
* shape with only the fallback/gating policy differing per their own
* legacy `query_obj` behavior -- see `fallbackToFirstMetric` and
* `orderOnlyWhenDesc`.
*/
export function buildSortMetricOrderby({
metrics,
timeseriesLimitMetric,
order_desc: orderDesc,
fallbackToFirstMetric = false,
orderOnlyWhenDesc = false,
}: BuildSortMetricOrderbyConfig): SortMetricOrderby {
const sortByMetric =
ensureIsArray(timeseriesLimitMetric)[0] ??
(fallbackToFirstMetric ? metrics[0] : undefined);
if (!sortByMetric) {
return { metrics, orderby: [] };
}
const sortByLabel = getMetricLabel(sortByMetric);
const nextMetrics = metrics.some(
metric => getMetricLabel(metric) === sortByLabel,
)
? metrics
: [...metrics, sortByMetric];
const shouldOrder = orderOnlyWhenDesc ? Boolean(orderDesc) : true;
return {
metrics: nextMetrics,
orderby: shouldOrder ? [[sortByMetric, !orderDesc]] : [],
};
}
@@ -19,7 +19,7 @@
import memoizeOne from 'memoize-one';
import { isString, isBoolean } from 'lodash-es';
import { isBlank } from '@apache-superset/core/utils';
import { addAlpha, DataRecord } from '@superset-ui/core';
import { addAlpha, DataRecord, rgbaToHex } from '@superset-ui/core';
import tinycolor from 'tinycolor2';
import {
ColorFormatters,
@@ -27,6 +27,7 @@ import {
ConditionalFormattingConfig,
MultipleValueComparators,
ResolvedColorFormatterResult,
ColorSchemeEnum,
} from '../types';
export const round = (num: number, precision = 0) =>
@@ -71,6 +72,9 @@ export const getOpacity = (
);
};
const isSpecialColor = (value: unknown): value is ColorSchemeEnum =>
Object.values(ColorSchemeEnum).includes(value as ColorSchemeEnum);
export const getColorFunction = (
{
operator,
@@ -270,19 +274,51 @@ export const getColorFunction = (
if (compareResult === false) return undefined;
const { cutoffValue, extremeValue } = compareResult;
// If useGradient is explicitly false, return solid color
if (useGradient === false) {
if (typeof colorScheme === 'string') {
if (isSpecialColor(colorScheme)) {
return colorScheme;
}
if (
useGradient === false ||
(useGradient === undefined && colorScheme.length === 9)
) {
if (alpha === false) {
return colorScheme.length === 9
? colorScheme.slice(0, 7)
: colorScheme;
}
return colorScheme;
}
const cleanHex =
colorScheme.length === 9 ? colorScheme.slice(0, 7) : colorScheme;
if (alpha === undefined || alpha) {
return addAlpha(
cleanHex,
getOpacity(value, cutoffValue, extremeValue, minOpacity, maxOpacity),
);
}
return colorScheme;
}
// If useGradient is explicitly false, return solid color
if (useGradient === false || useGradient === undefined) {
if (alpha === false) {
return rgbaToHex({ ...colorScheme, a: 1 });
}
return rgbaToHex(colorScheme);
}
const baseHexColor = rgbaToHex({ ...colorScheme, a: 1 });
// Otherwise apply gradient (default behavior for backward compatibility)
if (alpha === undefined || alpha) {
return addAlpha(
colorScheme,
baseHexColor,
getOpacity(value, cutoffValue, extremeValue, minOpacity, maxOpacity),
);
}
return colorScheme;
return baseHexColor;
};
};
@@ -30,3 +30,4 @@ export * from './getTemporalColumns';
export * from './displayTimeRelatedControls';
export * from './colorControls';
export * from './metricColumnFilter';
export * from './buildSortMetricOrderby';
@@ -0,0 +1,96 @@
/**
* 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 { buildSortMetricOrderby } from '../../src';
test('is a no-op when there is no sort metric and no fallback', () => {
const result = buildSortMetricOrderby({
metrics: ['sum__num'],
timeseriesLimitMetric: undefined,
});
expect(result).toEqual({ metrics: ['sum__num'], orderby: [] });
});
test('falls back to the first metric when configured to', () => {
const result = buildSortMetricOrderby({
metrics: ['sum__num', 'avg__num'],
timeseriesLimitMetric: undefined,
fallbackToFirstMetric: true,
});
expect(result.metrics).toEqual(['sum__num', 'avg__num']);
expect(result.orderby).toEqual([['sum__num', true]]);
});
test('appends the sort metric when it is not already selected', () => {
const result = buildSortMetricOrderby({
metrics: ['sum__num'],
timeseriesLimitMetric: 'count',
});
expect(result.metrics).toEqual(['sum__num', 'count']);
});
test('does not duplicate the sort metric when already selected', () => {
const result = buildSortMetricOrderby({
metrics: ['sum__num', 'count'],
timeseriesLimitMetric: 'count',
});
expect(result.metrics).toEqual(['sum__num', 'count']);
});
test('unconditional ordering (orderOnlyWhenDesc: false) always orders, flipping direction', () => {
const ascending = buildSortMetricOrderby({
metrics: ['sum__num'],
timeseriesLimitMetric: 'count',
order_desc: false,
});
expect(ascending.orderby).toEqual([['count', true]]);
const descending = buildSortMetricOrderby({
metrics: ['sum__num'],
timeseriesLimitMetric: 'count',
order_desc: true,
});
expect(descending.orderby).toEqual([['count', false]]);
});
test('gated ordering (orderOnlyWhenDesc: true) only orders when order_desc is set', () => {
const withoutDesc = buildSortMetricOrderby({
metrics: ['sum__num'],
timeseriesLimitMetric: 'count',
orderOnlyWhenDesc: true,
});
expect(withoutDesc.metrics).toEqual(['sum__num', 'count']);
expect(withoutDesc.orderby).toEqual([]);
const withDesc = buildSortMetricOrderby({
metrics: ['sum__num'],
timeseriesLimitMetric: 'count',
order_desc: true,
orderOnlyWhenDesc: true,
});
expect(withDesc.orderby).toEqual([['count', false]]);
});
test('resolves a multi-value timeseriesLimitMetric to its first entry', () => {
const result = buildSortMetricOrderby({
metrics: ['sum__num'],
timeseriesLimitMetric: ['count', 'avg__num'],
});
expect(result.metrics).toEqual(['sum__num', 'count']);
expect(result.orderby).toEqual([['count', true]]);
});
@@ -952,3 +952,167 @@ test('correct column boolean config', () => {
expect(colorFormatters[3].getColorFromValue(true)).toEqual('#FF0000FF');
expect(colorFormatters[3].getColorFromValue(false)).toEqual('#FF0000FF');
});
test('should return hex color when colorScheme is an RGB object', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: { r: 255, g: 128, b: 0, a: 1 },
column: 'name',
},
strValues,
);
expect(colorFunction('Diana')).toEqual('#ff8000');
expect(colorFunction('Carlos')).toEqual('#ff8000');
expect(colorFunction('Brian')).toEqual('#ff8000');
});
test('should return token name as-is when colorScheme is a string token', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: 'Green',
column: 'name',
},
strValues,
);
expect(colorFunction('Diana')).toEqual('Green');
expect(colorFunction('Carlos')).toEqual('Green');
expect(colorFunction('Brian')).toEqual('Green');
});
test('should return solid hex color when useGradient is false or true', () => {
const columnConfig = [
{
operator: Comparator.GreaterThan,
targetValue: 50,
colorScheme: { r: 0, g: 47, b: 255, a: 1 },
column: 'count',
useGradient: false,
},
{
operator: Comparator.GreaterThan,
targetValue: 50,
colorScheme: { r: 255, g: 166, b: 0, a: 1 },
column: 'count',
useGradient: true,
},
];
const colorFormatters = getColorFormatters(columnConfig, mockData);
expect(colorFormatters.length).toEqual(2);
// First formatter with useGradient: false should return solid color
expect(colorFormatters[0].column).toEqual('count');
expect(colorFormatters[0].getColorFromValue(100)).toEqual('#002fff');
// Second formatter with useGradient: true should return gradient color
expect(colorFormatters[1].column).toEqual('count');
expect(colorFormatters[1].getColorFromValue(100)).toEqual('#ffa600FF');
});
test('should return hex color without alpha for GreaterThan operator with RGB colorScheme', () => {
const config = {
operator: Comparator.GreaterThan,
targetValue: 50,
colorScheme: { r: 255, g: 0, b: 0, a: 1 },
useGradient: true,
};
const columnValues = [10, 50, 100];
const alpha = false;
const colorFunction = getColorFunction(config, columnValues, alpha);
expect(colorFunction(100)).toEqual('#ff0000');
});
test('should preserve alpha from colorScheme when useGradient is false', () => {
const config = {
operator: Comparator.None,
colorScheme: { r: 255, g: 0, b: 0, a: 0.5 },
useGradient: false,
};
const colorFunction = getColorFunction(config, [10, 20, 30]);
const result = colorFunction(20);
expect(result).not.toBe('#ff0000');
expect(result).not.toBe('rgb(255, 0, 0)');
});
test('should force opaque color when useGradient is false but alpha is explicitly false', () => {
const config = {
operator: Comparator.None,
colorScheme: { r: 255, g: 0, b: 0, a: 0.5 },
useGradient: false,
};
const colorFunction = getColorFunction(config, [10, 20, 30], false);
const result = colorFunction(20);
expect(result).toBe('#ff0000');
});
test('should return colorScheme as-is when alpha is false and length is 7', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.GreaterThan,
targetValue: 50,
colorScheme: '#FF0000',
useGradient: false,
column: 'count',
},
countValues,
false,
);
expect(colorFunction(100)).toEqual('#FF0000');
});
test('should preserve alpha when alpha is undefined and colorScheme has 9 chars', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.GreaterThan,
targetValue: 50,
colorScheme: '#FF000080',
useGradient: false,
column: 'count',
},
countValues,
);
expect(colorFunction(100)).toEqual('#FF000080');
});
test('should preserve alpha when alpha is true and colorScheme has 9 chars', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.GreaterThan,
targetValue: 50,
colorScheme: '#FF000080',
useGradient: false,
column: 'count',
},
countValues,
true,
);
expect(colorFunction(100)).toEqual('#FF000080');
});
test('should strip alpha channel when alpha is false and colorScheme has 9 chars', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.GreaterThan,
targetValue: 50,
colorScheme: '#FF000080',
useGradient: false,
column: 'count',
},
countValues,
false,
);
expect(colorFunction(100)).toEqual('#FF0000');
expect(colorFunction(100)).toHaveLength(7);
});
@@ -68,7 +68,7 @@
"d3-time": "^3.1.0",
"d3-time-format": "^4.1.0",
"dayjs": "^1.11.21",
"dompurify": "^3.4.12",
"dompurify": "^3.4.13",
"fetch-retry": "^6.0.0",
"handlebars": "^4.7.9",
"jed": "^1.1.1",
@@ -78,7 +78,7 @@
"parse-ms": "^4.0.0",
"re-resizable": "^6.11.2",
"react-ace": "^14.0.1",
"react-draggable": "^4.7.0",
"react-draggable": "^4.7.1",
"react-error-boundary": "^6.1.2",
"react-js-cron": "^6.0.2",
"react-markdown": "^10.1.0",
@@ -103,7 +103,7 @@
"@types/d3-time-format": "^4.0.3",
"@types/jquery": "^4.0.1",
"@types/lodash": "^4.17.24",
"@types/node": "^26.1.1",
"@types/node": "^26.1.2",
"@types/prop-types": "^15.7.15",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-table": "^7.7.20",
@@ -123,7 +123,6 @@
"@testing-library/user-event": "*",
"@types/react": "*",
"@types/react-loadable": "*",
"@types/react-window": "^1.8.8",
"@types/tinycolor2": "*",
"antd": "^6.0.0",
"nanoid": "*",
@@ -104,30 +104,19 @@ export default class ChartClient {
const buildQueryRegistry = getChartBuildQueryRegistry();
if (metaDataRegistry.has(visType)) {
const { useLegacyApi } = metaDataRegistry.get(visType)!;
const buildQuery =
(await buildQueryRegistry.get(visType)) ?? (() => formData);
const requestConfig: RequestConfig = useLegacyApi
? {
endpoint: '/explore_json/',
postPayload: {
form_data: buildQuery(formData),
},
...options,
}
: {
endpoint: '/api/v1/chart/data',
jsonPayload: {
query_context: buildQuery(formData),
},
...options,
};
const requestConfig: RequestConfig = {
endpoint: '/api/v1/chart/data',
jsonPayload: buildQuery(formData),
...options,
};
return this.client
.post(requestConfig)
.then(response =>
Array.isArray(response.json) ? response.json : [response.json],
);
return this.client.post(requestConfig).then(response => {
const { result } = response.json as { result?: QueryData[] };
return Array.isArray(result) ? result : [response.json as QueryData];
});
}
return Promise.reject(new Error(`Unknown chart type: ${visType}`));
@@ -21,7 +21,6 @@ import { render, waitFor, configure, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import StatefulChart from './StatefulChart';
import getChartControlPanelRegistry from '../registries/ChartControlPanelRegistrySingleton';
import getChartMetadataRegistry from '../registries/ChartMetadataRegistrySingleton';
import getChartBuildQueryRegistry from '../registries/ChartBuildQueryRegistrySingleton';
// Configure testing library to use data-test attribute
@@ -29,7 +28,6 @@ configure({ testIdAttribute: 'data-test' });
// Mock the registries
jest.mock('../registries/ChartControlPanelRegistrySingleton');
jest.mock('../registries/ChartMetadataRegistrySingleton');
jest.mock('../registries/ChartBuildQueryRegistrySingleton');
jest.mock('../clients/ChartClient');
@@ -67,12 +65,6 @@ beforeEach(() => {
jest.clearAllMocks();
// Setup default registry mocks
jest.mocked(getChartMetadataRegistry).mockReturnValue({
get: jest.fn().mockReturnValue({
useLegacyApi: false,
}),
} as unknown as ReturnType<typeof getChartMetadataRegistry>);
jest.mocked(getChartBuildQueryRegistry).mockReturnValue({
get: jest.fn().mockResolvedValue(null),
} as unknown as ReturnType<typeof getChartBuildQueryRegistry>);
@@ -746,11 +738,10 @@ test('resolves async (202) responses via the injected handleAsyncChartData hook'
await waitFor(() => {
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
});
// Delegates the raw response + job metadata (and useLegacyApi + abort signal)
// Delegates the raw response + job metadata (and abort signal)
expect(handleAsyncChartData).toHaveBeenCalledWith(
{ status: 202 },
asyncJob,
false,
expect.any(AbortSignal),
);
// Chart renders once the async data resolves
@@ -798,43 +789,6 @@ test('renders synchronous (200) responses that include a response object', async
expect(mockChartClient.client.post).toHaveBeenCalledTimes(1);
});
test('wraps the legacy async body as { result: [body] } for the async handler', async () => {
const legacyBody = { job_id: 'j1', channel_id: 'c1', status: 'running' };
mockChartClient.client.post.mockResolvedValue({
response: { status: 202 } as Response,
json: legacyBody,
});
// Force the legacy API path for this viz type
jest.mocked(getChartMetadataRegistry).mockReturnValue({
get: jest.fn().mockReturnValue({ useLegacyApi: true }),
} as unknown as ReturnType<typeof getChartMetadataRegistry>);
const handleAsyncChartData = jest
.fn()
.mockResolvedValue([{ data: 'legacy result' }]);
const { getByTestId } = render(
<StatefulChart
formData={mockFormData}
chartType="test_chart"
hooks={{ handleAsyncChartData }}
/>,
);
await waitFor(() => {
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
});
// Legacy body must be wrapped to match the V1 response signature
expect(handleAsyncChartData).toHaveBeenCalledWith(
{ status: 202 },
{ result: [legacyBody] },
true,
expect.any(AbortSignal),
);
await waitFor(() => {
expect(getByTestId('super-chart')).toBeInTheDocument();
});
});
test('does not apply a superseded async response over a newer one', async () => {
mockChartClient.client.post.mockResolvedValue({
response: { status: 202 } as Response,
@@ -1024,7 +978,7 @@ test('passes an abort signal to the async handler and aborts it on unmount', asy
response: { status: 202 } as Response,
json: { job_id: 'j', channel_id: 'c' },
});
// Typed with a rest param so mock.calls is indexable (the 4th arg is the signal)
// Typed with a rest param so mock.calls is indexable (the 3rd arg is the signal)
const handleAsyncChartData = jest.fn(
(..._args: unknown[]) => new Promise<never>(() => {}), // never resolves
);
@@ -1040,7 +994,7 @@ test('passes an abort signal to the async handler and aborts it on unmount', asy
await waitFor(() => {
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
});
const signal = handleAsyncChartData.mock.calls[0][3] as AbortSignal;
const signal = handleAsyncChartData.mock.calls[0][2] as AbortSignal;
expect(signal).toBeInstanceOf(AbortSignal);
expect(signal.aborted).toBe(false);
@@ -34,7 +34,6 @@ import {
import { Loading } from '../../components/Loading';
import ChartClient from '../clients/ChartClient';
import getChartBuildQueryRegistry from '../registries/ChartBuildQueryRegistrySingleton';
import getChartMetadataRegistry from '../registries/ChartMetadataRegistrySingleton';
import getChartControlPanelRegistry from '../registries/ChartControlPanelRegistrySingleton';
import SuperChart from './SuperChart';
@@ -281,9 +280,6 @@ export default function StatefulChart(props: StatefulChartProps) {
}
finalFormData.viz_type = vizType;
// Get chart metadata
const { useLegacyApi } = getChartMetadataRegistry().get(vizType) || {};
// Build query using the chart's buildQuery function
const buildQuery = await getChartBuildQueryRegistry().get(vizType);
let queryContext;
@@ -295,31 +291,20 @@ export default function StatefulChart(props: StatefulChartProps) {
queryContext = buildQueryContext(finalFormData);
}
// Ensure query_context is properly formatted for new API
if (!useLegacyApi && !queryContext.queries) {
// Ensure query_context is properly formatted for the API
if (!queryContext.queries) {
queryContext = { queries: [queryContext] };
}
const endpoint = useLegacyApi ? '/explore_json/' : '/api/v1/chart/data';
const requestConfig: RequestConfig = {
endpoint,
endpoint: '/api/v1/chart/data',
signal: controller.signal,
...(timeout && { timeout: timeout * 1000 }),
};
if (useLegacyApi) {
requestConfig.postPayload = {
form_data: {
...finalFormData,
...(force && { force: true }),
},
};
} else {
requestConfig.jsonPayload = {
jsonPayload: {
...queryContext,
...(force && { force: true }),
};
}
},
};
const clientResponse =
await chartClientRef.current!.client.post(requestConfig);
@@ -347,18 +332,10 @@ export default function StatefulChart(props: StatefulChartProps) {
'the async handler or disable GLOBAL_ASYNC_QUERIES for this chart.',
);
}
// The async handler (handleChartDataResponse) expects the V1 chart data
// response signature. The legacy endpoint returns a flat body, so wrap
// it as { result: [body] } exactly like legacyChartDataRequest does for
// the standard chart path; the V1 body is already correctly shaped.
const asyncPayload = useLegacyApi
? ({ result: [clientResponse.json] } as JsonObject)
: (clientResponse.json as JsonObject);
responseData = ensureIsArray(
await hooks.handleAsyncChartData(
rawResponse,
asyncPayload,
useLegacyApi,
clientResponse.json as JsonObject,
controller.signal,
),
);
@@ -374,10 +351,8 @@ export default function StatefulChart(props: StatefulChartProps) {
: [clientResponse.json]
) as JsonObject[];
// Handle the nested result structure from the new API
responseData = (
!useLegacyApi && rows[0]?.result ? rows[0].result : rows
) as QueryData[];
// Handle the nested result structure from the API
responseData = (rows[0]?.result ? rows[0].result : rows) as QueryData[];
}
// Don't pair this request's data with newer props or fire a stale onLoad
@@ -40,7 +40,6 @@ export interface ChartMetadataConfig {
supportedAnnotationTypes?: string[];
thumbnail: string;
thumbnailDark?: string;
useLegacyApi?: boolean;
behaviors?: Behavior[];
exampleGallery?: ExampleImage[];
tags?: string[];
@@ -75,8 +74,6 @@ export default class ChartMetadata {
thumbnailDark?: string;
useLegacyApi: boolean;
behaviors: Behavior[];
datasourceCount: number;
@@ -112,7 +109,6 @@ export default class ChartMetadata {
supportedAnnotationTypes = [],
thumbnail,
thumbnailDark,
useLegacyApi = false,
behaviors = [],
datasourceCount = 1,
enableNoResults = true,
@@ -144,7 +140,6 @@ export default class ChartMetadata {
this.supportedAnnotationTypes = supportedAnnotationTypes;
this.thumbnail = thumbnail;
this.thumbnailDark = thumbnailDark;
this.useLegacyApi = useLegacyApi;
this.behaviors = behaviors;
this.datasourceCount = datasourceCount;
this.enableNoResults = enableNoResults;
@@ -75,7 +75,6 @@ type Hooks = {
handleAsyncChartData?: (
response: Response,
json: JsonObject,
useLegacyApi?: boolean,
signal?: AbortSignal,
) => Promise<QueryData[]> | QueryData[];
} & PlainObject;
@@ -17,6 +17,7 @@
* under the License.
*/
import tinycolor from 'tinycolor2';
import { type RGBColor } from '@superset-ui/core/components';
const rgbRegex = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/;
export function getContrastingColor(color: string, thresholds = 186) {
@@ -120,3 +121,45 @@ export function rgbToHex(red: number, green: number, blue: number) {
return `#${r}${g}${b}`;
}
export function rgbaToHex(rgb: RGBColor): string {
const { r, g, b, a = 1 } = rgb;
const clampChannel = (value: number) =>
Math.min(255, Math.max(0, Math.round(value)));
const clampAlpha = (value: number) => Math.min(1, Math.max(0, value));
const toHex = (value: number) => {
const hex = value.toString(16);
return hex.length === 1 ? `0${hex}` : hex;
};
const hexColor = `#${toHex(clampChannel(r))}${toHex(clampChannel(g))}${toHex(clampChannel(b))}`;
const clampedAlpha = clampAlpha(a);
if (clampedAlpha !== 1) {
return `${hexColor}${toHex(Math.round(clampedAlpha * 255))}`;
}
return hexColor;
}
export const forceHexAlpha = (color: string | RGBColor): string => {
if (typeof color === 'object' && color !== null) {
return rgbaToHex({ ...color, a: 0.6 });
}
let hex = color.startsWith('#') ? color : `#${color}`;
// Expand shorthand hex (#rgb, #rgba) to full length before appending or
// replacing the alpha channel, otherwise the result is not a valid 6- or
// 8-digit CSS hex color.
if (hex.length === 4 || hex.length === 5) {
hex = `#${hex
.slice(1)
.split('')
.map(char => char + char)
.join('')}`;
}
if (hex.length === 9) {
return `${hex.slice(0, -2)}99`;
}
return `${hex}99`;
};
@@ -34,8 +34,9 @@ export const DropdownButton = ({
const { type: buttonType } = rest;
// divider implementation for default (non-primary) buttons
const defaultBtnCss = css`
${(!buttonType || buttonType === 'default') &&
`.ant-dropdown-trigger {
${
(!buttonType || buttonType === 'default') &&
`.ant-dropdown-trigger {
position: relative;
&:before {
content: '';
@@ -48,7 +49,8 @@ export const DropdownButton = ({
.anticon {
vertical-align: middle;
}
}`}
}`
}
`;
const button = (
<Dropdown.Button
@@ -58,13 +60,13 @@ export const DropdownButton = ({
defaultBtnCss,
css`
.ant-btn {
height: ${styleConfig?.controlHeight ??
theme.buttonControlHeightSM ??
30}px;
height: ${
styleConfig?.controlHeight ?? theme.buttonControlHeightSM ?? 30
}px;
box-shadow: ${styleConfig?.boxShadow ?? 'none'};
font-size: ${styleConfig?.fontSize ??
theme.buttonFontSize ??
theme.fontSizeSM}px;
font-size: ${
styleConfig?.fontSize ?? theme.buttonFontSize ?? theme.fontSizeSM
}px;
font-weight: ${styleConfig?.fontWeight ?? theme.fontWeightStrong};
}
`,
@@ -221,18 +221,22 @@ export const DynamicEditableTitle = memo(
onPressEnter={handleKeyPress}
placeholder={placeholder}
css={css`
${!canEdit &&
`&[disabled] {
${
!canEdit &&
`&[disabled] {
cursor: default;
}
`}
`
}
font-size: ${theme.fontSizeXL}px;
transition: auto;
${inputWidth &&
inputWidth > 0 &&
css`
width: ${inputWidth}px;
`}
${
inputWidth &&
inputWidth > 0 &&
css`
width: ${inputWidth}px;
`
}
`}
disabled={!canEdit}
/>
@@ -171,9 +171,11 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
{image && <ImageContainer image={image} size={size} />}
<div
css={(theme: SupersetTheme) => css`
max-width: ${containerSize === 'large'
? theme.sizeUnit * 150
: theme.sizeUnit * 100}px;
max-width: ${
containerSize === 'large'
? theme.sizeUnit * 150
: theme.sizeUnit * 100
}px;
`}
>
{title && <Title size={effectiveTextSize}>{title}</Title>}
@@ -73,14 +73,16 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
const closeButtonWidth = theme.sizeUnit * 14;
return css`
${responsive &&
css`
max-width: ${maxWidth ?? '900px'};
padding-left: ${theme.sizeUnit * 3}px;
padding-right: ${theme.sizeUnit * 3}px;
padding-bottom: 0;
top: 0;
`}
${
responsive &&
css`
max-width: ${maxWidth ?? '900px'};
padding-left: ${theme.sizeUnit * 3}px;
padding-right: ${theme.sizeUnit * 3}px;
padding-bottom: 0;
top: 0;
`
}
.ant-modal-container {
background-color: ${theme.colorBgContainer};
@@ -168,40 +170,46 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
padding: 0;
}
${draggable &&
css`
.ant-modal-header {
padding: 0;
${
draggable &&
css`
.ant-modal-header {
padding: 0;
.draggable-trigger {
cursor: move;
padding: ${theme.sizeUnit * 4}px ${closeButtonWidth}px
${theme.sizeUnit * 4}px ${theme.sizeUnit * 4}px;
width: 100%;
}
}
`}
${resizable &&
css`
.resizable {
pointer-events: all;
.resizable-wrapper {
height: 100%;
}
.ant-modal-container {
height: 100%;
.ant-modal-body {
height: ${hideFooter
? `calc(100% - ${MODAL_HEADER_HEIGHT}px)`
: `calc(100% - ${MODAL_HEADER_HEIGHT}px - ${MODAL_FOOTER_HEIGHT}px)`};
.draggable-trigger {
cursor: move;
padding: ${theme.sizeUnit * 4}px ${closeButtonWidth}px
${theme.sizeUnit * 4}px ${theme.sizeUnit * 4}px;
width: 100%;
}
}
}
`}
`
}
${
resizable &&
css`
.resizable {
pointer-events: all;
.resizable-wrapper {
height: 100%;
}
.ant-modal-container {
height: 100%;
.ant-modal-body {
height: ${
hideFooter
? `calc(100% - ${MODAL_HEADER_HEIGHT}px)`
: `calc(100% - ${MODAL_HEADER_HEIGHT}px - ${MODAL_FOOTER_HEIGHT}px)`
};
}
}
}
`
}
`;
}}
`;
@@ -78,19 +78,78 @@ export function transformLinkUri(uri: string): string {
return DANGEROUS_LINK_PROTOCOLS.includes(scheme) ? '' : url;
}
// A hast-util-sanitize attribute definition is either a bare property name
// (any value allowed) or a tuple of `[propertyName, ...allowedValues]` (only
// the listed values allowed). See hast-util-sanitize's `PropertyDefinition`.
type AttributeDefinition = string | readonly [string, ...unknown[]];
function getAttributeDefinitionKey(
definition: AttributeDefinition,
): string | undefined {
if (typeof definition === 'string') return definition;
// htmlSchemaOverrides comes from runtime config and isn't guaranteed to
// match the expected shape; a malformed element (e.g. `null`) has no key
// rather than crashing the lookup.
return Array.isArray(definition) ? definition[0] : undefined;
}
/**
* Merge an operator-supplied list of attribute definitions for a tag (or the
* `'*'` wildcard) with the corresponding default definitions.
*
* hast-util-sanitize's `findDefinition` returns only the FIRST definition it
* finds for a given property name, so a naive concat leaves whichever list
* happens to declare that property first in charge. The default schema
* already declares restrictive tuples for some properties (e.g.
* `li: [['className', 'task-list-item']]`), so appending an operator's
* override after it never took effect. Because a failed allowlist check
* returns `[]` rather than `undefined`, hast-util-sanitize's own `'*'`
* fallback never kicked in either.
*
* To fix that, an override definition replaces the default definition for
* the same property (by property name) instead of being appended alongside
* it.
*/
function mergeAttributeDefinitions(
defaults: readonly AttributeDefinition[],
overrides: readonly AttributeDefinition[],
): AttributeDefinition[] {
const overriddenKeys = new Set(
overrides.map(getAttributeDefinitionKey).filter(Boolean),
);
const remainingDefaults = defaults.filter(
definition => !overriddenKeys.has(getAttributeDefinitionKey(definition)),
);
return [...remainingDefaults, ...overrides];
}
export function getOverrideHtmlSchema(
originalSchema: typeof defaultSchema,
htmlSchemaOverrides: SafeMarkdownProps['htmlSchemaOverrides'],
) {
// Merge into a fresh clone: mergeWith mutates its first argument, and the
// array customizer concatenates, so merging into the shared defaultSchema
// import would progressively widen the sanitization allowlist for every
// SafeMarkdown instance app-wide.
// Merge into a fresh clone: mergeWith mutates its first argument, so
// merging into the shared defaultSchema import would progressively widen
// the sanitization allowlist for every SafeMarkdown instance app-wide.
const target = cloneDeep(originalSchema);
return mergeWith(
cloneDeep(originalSchema),
target,
htmlSchemaOverrides,
(objValue, srcValue) =>
Array.isArray(objValue) ? objValue.concat(srcValue) : undefined,
(objValue, srcValue, _key, object) => {
if (!Array.isArray(objValue)) return undefined;
// Only the per-tag (and `'*'`) arrays nested under `attributes` hold
// property definitions that need dedup-by-key; every other array in
// the schema (e.g. `tagNames`, `protocols.href`) is a plain list where
// concatenation is the correct merge.
if (object === target.attributes) {
// htmlSchemaOverrides comes from runtime config and isn't guaranteed
// to match the expected shape; fall back to the default definitions
// for a tag rather than throwing if an operator supplies something
// other than an array of attribute definitions.
if (!Array.isArray(srcValue)) return objValue;
return mergeAttributeDefinitions(objValue, srcValue);
}
return objValue.concat(srcValue);
},
);
}
@@ -16,10 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import type { LabeledValue as AntdLabeledValue } from 'antd/es/select';
import {
splitWithQuoteEscaping,
stripSurroundingQuotes,
makeQuoteAwareTokenizer,
propertyComparator,
} from './utils';
test('stripSurroundingQuotes removes matching surrounding double quotes', () => {
@@ -138,3 +140,47 @@ test('makeQuoteAwareTokenizer detects any separator from the list outside quotes
expect(tokenize('a\nb')).toEqual(['a', 'b']);
expect(tokenize('"a\nb"')).toEqual(['"a\nb"']);
});
test('propertyComparator sorts string properties lexicographically', () => {
const compare = propertyComparator('label');
const ten = { label: '10' } as AntdLabeledValue;
const two = { label: '2' } as AntdLabeledValue;
const hundred = { label: '100' } as AntdLabeledValue;
expect([ten, two, hundred].sort(compare)).toEqual([ten, hundred, two]);
});
test('propertyComparator sorts number properties numerically', () => {
const compare = propertyComparator('value');
const ten = { value: 10 } as unknown as AntdLabeledValue;
const two = { value: 2 } as unknown as AntdLabeledValue;
const hundred = { value: 100 } as unknown as AntdLabeledValue;
expect([ten, two, hundred].sort(compare)).toEqual([two, ten, hundred]);
});
test('propertyComparator sorts bigint properties numerically, not lexicographically', () => {
const compare = propertyComparator('value');
const ten = { value: 10n } as unknown as AntdLabeledValue;
const two = { value: 2n } as unknown as AntdLabeledValue;
const hundred = { value: 100n } as unknown as AntdLabeledValue;
expect([ten, two, hundred].sort(compare)).toEqual([two, ten, hundred]);
});
test('propertyComparator sorts diverging-digit-length bigint values numerically', () => {
const compare = propertyComparator('value');
const a = { value: 10000000000000002n } as unknown as AntdLabeledValue;
const b = { value: 9000000000000001n } as unknown as AntdLabeledValue;
const c = { value: 10000000000000001n } as unknown as AntdLabeledValue;
expect([a, b, c].sort(compare)).toEqual([b, c, a]);
});
test('propertyComparator sorts mixed bigint and number properties numerically', () => {
const compare = propertyComparator('value');
const bigTen = { value: 10n } as unknown as AntdLabeledValue;
const two = { value: 2 } as unknown as AntdLabeledValue;
const bigHundred = { value: 100n } as unknown as AntdLabeledValue;
expect([bigTen, two, bigHundred].sort(compare)).toEqual([
two,
bigTen,
bigHundred,
]);
});
@@ -79,7 +79,7 @@ export function hasOption(
/**
* It creates a comparator to check for a specific property.
* Can be used with string and number property values.
* Can be used with string, number, and bigint property values.
* */
export const propertyComparator =
(property: string) => (a: AntdLabeledValue, b: AntdLabeledValue) => {
@@ -91,6 +91,18 @@ export const propertyComparator =
if (typeof propertyA === 'number' && typeof propertyB === 'number') {
return propertyA - propertyB;
}
// BIGINT columns can decode to native `bigint` values (see json-bigint
// parsing of large numeric values). Compare numerically rather than
// falling through to the string fallback below, which would sort them
// lexicographically (e.g. "10", "100", "2").
if (
(typeof propertyA === 'bigint' || typeof propertyA === 'number') &&
(typeof propertyB === 'bigint' || typeof propertyB === 'number')
) {
if (propertyA < propertyB) return -1;
if (propertyA > propertyB) return 1;
return 0;
}
return String(propertyA).localeCompare(String(propertyB)); // fallback to string comparison
};
@@ -0,0 +1,122 @@
/**
* 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 { render, screen, waitFor } from '@superset-ui/core/spec';
import type { ColumnsType } from 'antd/es/table';
import { Table } from './index';
// These tests exercise VirtualTable's react-window v2 `Grid` wiring
// (`cellComponent`/`cellProps`/`gridRef`), which previously had no direct
// coverage - `Table.test.tsx` only exercises the non-virtualized code path.
interface BasicData {
columnName: string;
columnType: string;
}
const testData: BasicData[] = [
{ columnName: 'Number', columnType: 'Numerical' },
{ columnName: 'String', columnType: 'Physical' },
{ columnName: 'Date', columnType: 'Virtual' },
];
const testColumns: ColumnsType<BasicData> = [
{
title: 'Column Name',
dataIndex: 'columnName',
key: 'columnName',
width: 150,
},
{
title: 'Column Type',
dataIndex: 'columnType',
key: 'columnType',
width: 150,
},
];
test('virtualized table renders headers and row content through the react-window Grid', async () => {
render(
<Table
columns={testColumns}
data={testData}
virtualize
height={200}
usePagination={false}
/>,
);
await waitFor(() =>
testColumns.forEach(column =>
expect(
screen
.getAllByText(column.title as string)
.find(el => el.closest('th')),
).toBeInTheDocument(),
),
);
testData.forEach(row => {
expect(screen.getByText(row.columnName)).toBeInTheDocument();
});
});
test('virtualized table cells keep the DOM hooks other code (cypress, downloadAsImage) relies on', async () => {
const { container } = render(
<Table
columns={testColumns}
data={testData}
virtualize
height={200}
usePagination={false}
/>,
);
await waitFor(() => {
expect(container.querySelector('.virtual-grid')).toBeInTheDocument();
expect(
container.querySelectorAll('.virtual-table-cell').length,
).toBeGreaterThan(0);
});
});
test('cell render functions receive their row data via cellProps rather than a stale closure', async () => {
const columnsWithRender: ColumnsType<BasicData> = [
{
title: 'Column Name',
dataIndex: 'columnName',
key: 'columnName',
width: 150,
render: (value: string) => `rendered:${value}`,
},
];
render(
<Table
columns={columnsWithRender}
data={testData}
virtualize
height={200}
usePagination={false}
/>,
);
await waitFor(() => {
expect(screen.getByText('rendered:Number')).toBeInTheDocument();
});
});
@@ -24,10 +24,14 @@ import {
} from 'antd/es/table';
import classNames from 'classnames';
import { useResizeDetector } from 'react-resize-detector';
import { useEffect, useRef, useState, useCallback, CSSProperties } from 'react';
import { VariableSizeGrid as Grid } from 'react-window';
import { useRef, useState, useCallback, type UIEvent } from 'react';
import {
Grid,
type CellComponentProps,
type GridImperativeAPI,
} from 'react-window';
import { safeHtmlSpan } from '@superset-ui/core';
import { useTheme, styled } from '@apache-superset/core/theme';
import { useTheme, styled, SupersetTheme } from '@apache-superset/core/theme';
import { TableSize, ETableAction } from './index';
@@ -71,6 +75,59 @@ const StyledTable = styled(AntTable)(
const SMALL = 39;
const MIDDLE = 47;
interface VirtualGridCellProps {
mergedColumns: AntTableProps<any>['columns'];
rawData: readonly object[];
cellSize: number;
allowHTML: boolean;
theme: SupersetTheme;
}
// Rendered via `cellComponent`, so it must be a stable reference (module scope)
// rather than defined inline on every render of the enclosing table -
// otherwise react-window would treat it as a new component type each render
// and remount every cell. All the data it needs is threaded through
// `cellProps` instead of being closed over.
const VirtualGridCell = ({
columnIndex,
rowIndex,
style,
mergedColumns,
rawData,
cellSize,
allowHTML,
theme,
}: CellComponentProps<VirtualGridCellProps>) => {
const data: any = rawData?.[rowIndex];
// Set default content
let content = data?.[(mergedColumns as any)?.[columnIndex]?.dataIndex];
// Check if the column has a render function
const render = mergedColumns?.[columnIndex]?.render;
if (typeof render === 'function') {
// Use render function to generate formatted content using column's render function
content = render(content, data, rowIndex);
}
if (allowHTML && typeof content === 'string') {
content = safeHtmlSpan(content);
}
return (
<StyledCell
className={classNames('virtual-table-cell', {
'virtual-table-cell-last':
columnIndex === (mergedColumns?.length ?? 0) - 1,
})}
style={style}
title={typeof content === 'string' ? content : undefined}
theme={theme}
height={cellSize}
>
{content}
</StyledCell>
);
};
const VirtualTable = <RecordType extends object>(
props: VirtualTableProps<RecordType>,
) => {
@@ -126,19 +183,15 @@ const VirtualTable = <RecordType extends object>(
(lastColumn.width as number) + Math.floor(tableWidth - totalWidth);
}
const gridRef = useRef<any>();
const gridRef = useRef<GridImperativeAPI>(null);
const [connectObject] = useState<any>(() => {
const obj = {};
Object.defineProperty(obj, 'scrollLeft', {
get: () => {
if (gridRef.current) {
return gridRef.current?.state?.scrollLeft;
}
return 0;
},
get: () => gridRef.current?.element?.scrollLeft ?? 0,
set: (scrollLeft: number) => {
if (gridRef.current) {
gridRef.current.scrollTo({ scrollLeft });
const element = gridRef.current?.element;
if (element) {
element.scrollLeft = scrollLeft;
}
},
});
@@ -146,14 +199,11 @@ const VirtualTable = <RecordType extends object>(
return obj;
});
const resetVirtualGrid = () => {
gridRef.current?.resetAfterIndices({
columnIndex: 0,
shouldForceUpdate: true,
});
};
useEffect(() => resetVirtualGrid, [tableWidth, columns, size]);
// No manual cache-reset is needed here (react-window v2 has no
// `resetAfterIndices`-style API): `columnWidth` below is a fresh inline
// closure over `mergedColumns` on every render, so react-window's internal
// size cache - which is invalidated whenever the `columnWidth`/`rowHeight`
// function reference changes - recomputes automatically.
/*
* antd Table has a runtime error when it tries to fire the onChange event triggered from a pageChange
@@ -166,7 +216,10 @@ const VirtualTable = <RecordType extends object>(
* We intentionally leave horizontal scroll where it was so user can focus on
* specific range of columns as they page through data
*/
gridRef.current?.scrollTo?.({ scrollTop: 0 });
const element = gridRef.current?.element;
if (element) {
element.scrollTop = 0;
}
onChange?.(
{
@@ -192,61 +245,31 @@ const VirtualTable = <RecordType extends object>(
const cellSize = size === TableSize.Middle ? MIDDLE : SMALL;
return (
<Grid
ref={gridRef}
gridRef={gridRef}
className="virtual-grid"
columnCount={mergedColumns.length}
columnWidth={(index: number) => {
const { width = DEFAULT_COL_WIDTH } = mergedColumns[index];
return width as number;
}}
height={height || (scroll!.y as number)}
rowCount={rawData.length}
rowHeight={() => cellSize}
width={tableWidth}
onScroll={({ scrollLeft }: { scrollLeft: number }) => {
onScroll({ scrollLeft });
style={{
height: height || (scroll!.y as number),
width: tableWidth,
}}
>
{({
columnIndex,
rowIndex,
style,
}: {
columnIndex: number;
rowIndex: number;
style: CSSProperties;
}) => {
const data: any = rawData?.[rowIndex];
// Set default content
let content =
data?.[(mergedColumns as any)?.[columnIndex]?.dataIndex];
// Check if the column has a render function
const render = mergedColumns[columnIndex]?.render;
if (typeof render === 'function') {
// Use render function to generate formatted content using column's render function
content = render(content, data, rowIndex);
}
if (allowHTML && typeof content === 'string') {
content = safeHtmlSpan(content);
}
return (
<StyledCell
className={classNames('virtual-table-cell', {
'virtual-table-cell-last':
columnIndex === mergedColumns.length - 1,
})}
style={style}
title={typeof content === 'string' ? content : undefined}
theme={theme}
height={cellSize}
>
{content}
</StyledCell>
);
cellComponent={VirtualGridCell}
cellProps={{
mergedColumns,
rawData,
cellSize,
allowHTML,
theme,
}}
</Grid>
onScroll={(event: UIEvent<HTMLDivElement>) => {
onScroll({ scrollLeft: event.currentTarget.scrollLeft });
}}
/>
);
};
@@ -51,8 +51,10 @@ const StyledTabs = ({
.ant-tabs-body-holder {
overflow: ${allowOverflow ? 'visible' : 'auto'};
${fullHeight && 'height: 100%;'}
${contentHeight &&
`height: ${typeof contentHeight === 'number' ? `${contentHeight}px` : contentHeight};`}
${
contentHeight &&
`height: ${typeof contentHeight === 'number' ? `${contentHeight}px` : contentHeight};`
}
${contentPadding}
}
.ant-tabs-body {
@@ -66,9 +68,11 @@ const StyledTabs = ({
margin: 0;
}
.ant-tabs-nav-wrap {
${!(tabBarStyle && 'paddingLeft' in tabBarStyle)
? `padding: 0 ${theme.sizeUnit * 4}px;`
: ''}
${
!(tabBarStyle && 'paddingLeft' in tabBarStyle)
? `padding: 0 ${theme.sizeUnit * 4}px;`
: ''
}
}
.ant-tabs-tab {
flex: 1 1 auto;
@@ -29,6 +29,19 @@ export interface DataRecord {
[key: string]: DataRecordValue;
}
export interface ChartDataQueryTiming {
query_planning_ms: number | null;
cache_resolution_ms: number | null;
data_acquisition_ms: number | null;
payload_assembly_ms: number | null;
total_ms: number;
}
export interface ChartDataTiming {
version: 1;
query: ChartDataQueryTiming;
}
/**
* Queried data for charts. The `queries` field from `POST /chart/data`.
* See superset/charts/schemas.py for the class of the same name.
@@ -84,6 +97,10 @@ export interface ChartDataResponseResult {
* or null if multiple currencies are present.
*/
detected_currency?: string | null;
/**
* Versioned query lifecycle timing in milliseconds.
*/
timing?: ChartDataTiming;
}
export interface TimeseriesChartDataResponseResult extends ChartDataResponseResult {
@@ -66,6 +66,7 @@ export enum FeatureFlag {
TaggingSystem = 'TAGGING_SYSTEM',
Thumbnails = 'THUMBNAILS',
UseAnalogousColors = 'USE_ANALOGOUS_COLORS',
VersionHistory = 'VERSION_HISTORY',
ForceSqlLabRunAsync = 'SQLLAB_FORCE_RUN_ASYNC',
SlackEnableAvatars = 'SLACK_ENABLE_AVATARS',
EnableDashboardScreenshotEndpoints = 'ENABLE_DASHBOARD_SCREENSHOT_ENDPOINTS',
@@ -18,7 +18,9 @@
*/
import {
DEFAULT_DECK_MULTI_MAX_SLICES,
getBootstrapDataFromDocument,
getDeckMultiMaxSlices,
getDefaultMapRenderer,
getMapProviderMapStyle,
getMapboxApiKeyFromBootstrap,
@@ -58,6 +60,16 @@ test('Mapbox key helpers report absence and presence from bootstrap data', () =>
).toBe(true);
});
test('deck.gl multi-slice cap falls back to the default when unset', () => {
expect(getDeckMultiMaxSlices({ common: { conf: {} } })).toBe(
DEFAULT_DECK_MULTI_MAX_SLICES,
);
expect(getDeckMultiMaxSlices(undefined)).toBe(DEFAULT_DECK_MULTI_MAX_SLICES);
expect(
getDeckMultiMaxSlices({ common: { conf: { DECK_MULTI_MAX_SLICES: 5 } } }),
).toBe(5);
});
test('bootstrap data helper parses document data safely', () => {
document.body.innerHTML = `<div id="app" data-bootstrap='${JSON.stringify({
common: { conf: { MAPBOX_API_KEY: 'pk.document' } },
@@ -83,10 +83,13 @@ type BootstrapData = {
conf?: {
DEFAULT_MAP_RENDERER?: unknown;
MAPBOX_API_KEY?: unknown;
DECK_MULTI_MAX_SLICES?: unknown;
};
};
};
export const DEFAULT_DECK_MULTI_MAX_SLICES = 50;
export function getBootstrapDataFromDocument(): unknown {
/* istanbul ignore if -- a missing document only occurs in SSR/worker
contexts, which Jest cannot simulate: jsdom pins `document` as a
@@ -118,6 +121,16 @@ export function hasMapboxApiKey(
return getMapboxApiKeyFromBootstrap(bootstrapData).trim().length > 0;
}
export function getDeckMultiMaxSlices(
bootstrapData: unknown = getBootstrapDataFromDocument(),
): number {
const maxSlices = (bootstrapData as BootstrapData | undefined)?.common?.conf
?.DECK_MULTI_MAX_SLICES;
return typeof maxSlices === 'number' && Number.isFinite(maxSlices)
? maxSlices
: DEFAULT_DECK_MULTI_MAX_SLICES;
}
export function getDefaultMapRenderer(
bootstrapData: unknown = getBootstrapDataFromDocument(),
): MapProvider {
@@ -122,7 +122,7 @@ describe('ChartClient', () => {
});
describe('.loadQueryData(formData, options)', () => {
test('returns a promise of query data for known chart type', () => {
test('returns a promise of query data for known chart type', async () => {
getChartMetadataRegistry().registerValue(
VizType.WordCloud,
new ChartMetadata({ name: 'Word Cloud', thumbnail: '' }),
@@ -132,14 +132,18 @@ describe('ChartClient', () => {
VizType.WordCloud,
(formData: QueryFormData) => buildQueryContext(formData),
);
fetchMock.post('glob:*/api/v1/chart/data', [
{
field1: 'abc',
field2: 'def',
},
]);
// The real /api/v1/chart/data endpoint wraps its results in
// `{ result: [...] }`, not a bare array.
fetchMock.post('glob:*/api/v1/chart/data', {
result: [
{
field1: 'abc',
field2: 'def',
},
],
});
return expect(
await expect(
chartClient.loadQueryData({
granularity: 'minute',
viz_type: VizType.WordCloud,
@@ -151,6 +155,16 @@ describe('ChartClient', () => {
field2: 'def',
},
]);
// The query context fields must be posted at the top level of the
// request body -- the endpoint's schema does not expect them nested
// under a `query_context` key.
const calls = fetchMock.callHistory.calls('glob:*/api/v1/chart/data');
const requestBody = JSON.parse(
(calls[0].options as RequestInit).body as string,
);
expect(requestBody.query_context).toBeUndefined();
expect(requestBody.datasource).toEqual({ id: 1, type: 'table' });
});
test('returns a promise that rejects for unknown chart type', () =>
expect(
@@ -160,42 +174,6 @@ describe('ChartClient', () => {
datasource: '1__table',
}),
).rejects.toEqual(new Error('Unknown chart type: rainbow_3d_pie')));
test('fetches data from the legacy API if ChartMetadata has useLegacyApi=true,', () => {
// note legacy charts do not register a buildQuery function in the registry
getChartMetadataRegistry().registerValue(
'word_cloud_legacy',
new ChartMetadata({
name: 'Legacy Word Cloud',
thumbnail: '.png',
useLegacyApi: true,
}),
);
fetchMock.post('glob:*/api/v1/chart/data', () =>
Promise.reject(new Error('Unexpected all to v1 API')),
);
// post `Superset.route_base = ""`, the legacy endpoint
// collapsed from `/superset/explore_json/` to `/explore_json/`.
fetchMock.post('glob:*/explore_json/', {
field1: 'abc',
field2: 'def',
});
return expect(
chartClient.loadQueryData({
granularity: 'minute',
viz_type: 'word_cloud_legacy',
datasource: '1__table',
}),
).resolves.toEqual([
{
field1: 'abc',
field2: 'def',
},
]);
});
});
describe('.loadDatasource(datasourceKey, options)', () => {
@@ -22,6 +22,8 @@ import {
addAlpha,
hexToRgb,
rgbToHex,
rgbaToHex,
forceHexAlpha,
} from '@superset-ui/core';
describe('color utils', () => {
@@ -106,4 +108,51 @@ describe('color utils', () => {
expect(rgbToHex(0, 0, 0)).toBe('#000000');
});
});
describe('rgbaToHex', () => {
test('omits the alpha channel for opaque colors', () => {
expect(rgbaToHex({ r: 255, g: 0, b: 0 })).toBe('#ff0000');
expect(rgbaToHex({ r: 255, g: 0, b: 0, a: 1 })).toBe('#ff0000');
});
test('appends the alpha channel for translucent colors', () => {
expect(rgbaToHex({ r: 0, g: 150, b: 0, a: 0.2 })).toBe('#00960033');
expect(rgbaToHex({ r: 0, g: 0, b: 0, a: 0.5 })).toBe('#00000080');
});
test('fully transparent colors keep an explicit 00 alpha', () => {
expect(rgbaToHex({ r: 255, g: 255, b: 255, a: 0 })).toBe('#ffffff00');
});
test('zero-pads single-digit channels', () => {
expect(rgbaToHex({ r: 1, g: 2, b: 3 })).toBe('#010203');
});
test('rounds fractional channel values', () => {
expect(rgbaToHex({ r: 254.6, g: 0.4, b: 0 })).toBe('#ff0000');
});
test('clamps out-of-range channel and alpha values', () => {
expect(rgbaToHex({ r: 300, g: -10, b: 0 })).toBe('#ff0000');
expect(rgbaToHex({ r: 0, g: 0, b: 0, a: 1.5 })).toBe('#000000');
expect(rgbaToHex({ r: 0, g: 0, b: 0, a: -0.5 })).toBe('#00000000');
});
});
describe('forceHexAlpha', () => {
test('appends 60% alpha to a 6-digit hex string', () => {
expect(forceHexAlpha('#ff0000')).toBe('#ff000099');
});
test('adds the # prefix when missing', () => {
expect(forceHexAlpha('ff0000')).toBe('#ff000099');
});
test('replaces the existing alpha on an 8-digit hex string', () => {
expect(forceHexAlpha('#ff000033')).toBe('#ff000099');
});
test('converts an RGBColor object using 60% alpha', () => {
expect(forceHexAlpha({ r: 255, g: 0, b: 0 })).toBe('#ff000099');
});
test('overrides the alpha of a translucent RGBColor object', () => {
expect(forceHexAlpha({ r: 0, g: 150, b: 0, a: 0.2 })).toBe('#00960099');
});
test('expands a shorthand 3-digit hex string before adding alpha', () => {
expect(forceHexAlpha('#fff')).toBe('#ffffff99');
});
test('expands a shorthand 4-digit hex string before replacing alpha', () => {
expect(forceHexAlpha('#ff03')).toBe('#ffff0099');
});
});
});
@@ -83,6 +83,111 @@ describe('getOverrideHtmlSchema', () => {
(second.tagNames ?? []).filter(name => name === 'iframe'),
).toHaveLength(1);
});
// Regression tests for #34191: hast-util-sanitize's real default schema
// restricts `li`/`ul`/`ol` `className` to specific task-list values, e.g.
// `li: [['className', 'task-list-item']]` (see hast-util-sanitize's
// `lib/schema.js`). `rehype-sanitize` is mocked globally in this jest
// project (see NOTE above), so `defaultSchema` isn't usable here; this
// fixture mirrors just the shape that matters for these tests.
//
// findDefinition only ever returns the FIRST definition it finds for a
// given property name, so simply concatenating the operator's override
// after the default restrictive tuple left the default in charge: any
// other class value was silently dropped, and (since a failed allowlist
// check returns `[]` rather than `undefined`) hast-util-sanitize's own
// `'*'` wildcard fallback never kicked in either.
const taskListSchemaFixture: typeof defaultSchema = {
attributes: {
li: [['className', 'task-list-item']],
ul: [['className', 'contains-task-list']],
ol: [['className', 'contains-task-list']],
},
};
test('an override for a property replaces the same-named default definition', () => {
const result = getOverrideHtmlSchema(taskListSchemaFixture, {
attributes: { li: ['className'] },
});
// The override should fully replace the default `li` className
// restriction, not merely be appended after it.
expect(result.attributes?.li).toEqual(['className']);
});
test('a per-tag override leaves a sibling tag definition untouched', () => {
const result = getOverrideHtmlSchema(taskListSchemaFixture, {
attributes: { li: ['className'] },
});
// Only the overridden tag (`li`) is replaced; `ul`/`ol` keep their
// default restrictive definitions since the merge recurses per-tag.
expect(result.attributes?.ul).toEqual(taskListSchemaFixture.attributes?.ul);
expect(result.attributes?.ol).toEqual(taskListSchemaFixture.attributes?.ol);
});
test('a "*" wildcard override alone does not widen li, which has its own default definition', () => {
const result = getOverrideHtmlSchema(taskListSchemaFixture, {
attributes: { '*': ['className'] },
});
expect(result.attributes?.['*']).toContainEqual('className');
// The restrictive `li`-specific default is still present (untouched by
// this override), but since findDefinition matches on the specific
// `li` entry first, this alone documents why an `li`-specific override
// is required to unblock arbitrary classes on `li` -- a `'*'` override
// does not, by itself, widen a tag that already has its own definition.
expect(result.attributes?.li).toEqual(taskListSchemaFixture.attributes?.li);
});
test('a malformed (non-array) attribute override for a tag falls back to the default definition instead of throwing', () => {
expect(() =>
getOverrideHtmlSchema(taskListSchemaFixture, {
// Runtime config isn't guaranteed to match the expected shape; a
// string here (rather than an array of attribute definitions) must
// not crash markdown rendering.
attributes: { li: 'className' as unknown as string[] },
}),
).not.toThrow();
const result = getOverrideHtmlSchema(taskListSchemaFixture, {
attributes: { li: 'className' as unknown as string[] },
});
expect(result.attributes?.li).toEqual(taskListSchemaFixture.attributes?.li);
});
test('a malformed (null) element within an attribute override array falls back to the default definition instead of throwing', () => {
expect(() =>
getOverrideHtmlSchema(taskListSchemaFixture, {
// Runtime config isn't guaranteed to match the expected shape; a
// `null` element here must not crash the key lookup used to
// dedup overrides against the default definitions.
attributes: { li: [null] as unknown as string[] },
}),
).not.toThrow();
const result = getOverrideHtmlSchema(taskListSchemaFixture, {
attributes: { li: [null] as unknown as string[] },
});
expect(result.attributes?.li).toEqual([
...taskListSchemaFixture.attributes!.li!,
null,
]);
});
test('the default task-list class restriction on li/ul/ol still applies with no override', () => {
const result = getOverrideHtmlSchema(taskListSchemaFixture, {});
expect(result.attributes?.li).toEqual([['className', 'task-list-item']]);
expect(result.attributes?.ul).toContainEqual([
'className',
'contains-task-list',
]);
expect(result.attributes?.ol).toContainEqual([
'className',
'contains-task-list',
]);
});
});
describe('transformLinkUri', () => {
@@ -19,10 +19,16 @@
import { expect } from '@playwright/test';
import { Modal, Input } from '../core';
import { isFeatureEnabled } from '../../helpers/featureFlags';
/**
* Delete confirmation modal that requires typing "DELETE" to confirm.
* Used throughout Superset for destructive delete operations.
* Delete confirmation modal, used throughout Superset for delete operations.
*
* The modal has two modes. Destructive mode demands the user type "DELETE"
* before the action is enabled. Recoverable mode what `SOFT_DELETE` turns
* on archives instead of removing, so the action reads "Archive" and the
* type-to-confirm friction is deliberately dropped: reduced friction is what
* a reversible action earns.
*
* Provides primitives for tests to compose deletion flows.
*/
@@ -96,4 +102,40 @@ export class DeleteConfirmationModal extends Modal {
await expect(confirmButton).toBeEnabled({ timeout: options?.timeout });
await confirmButton.click(options);
}
/**
* Confirms the deletion using whichever interaction the modal is in.
*
* Which mode is in force is a property of the instance, not of the caller,
* so the flag decides rather than the test the same spec then covers the
* hard-delete and archive paths without being rewritten when the default
* flips, and keeps covering hard delete for deployments that turn the
* toggle back off.
*
* Neither branch is merely tolerant: the recoverable branch asserts the
* confirmation input is genuinely *absent* rather than skipping past it,
* so a regression that dropped the typed confirmation from destructive
* mode still fails here instead of quietly passing.
*
* Assumes the modal's mode follows `SOFT_DELETE` alone. That holds
* everywhere except a bulk selection containing semantic views, which
* stays destructive even with the flag on such a flow should drive
* {@link fillConfirmationInput} and {@link clickDelete} directly.
*
* @param confirmationText - Text typed in destructive mode
*
* @example
* const deleteModal = new DeleteConfirmationModal(page);
* await deleteModal.waitForVisible();
* await deleteModal.confirmDeletion();
* await deleteModal.waitForHidden();
*/
async confirmDeletion(confirmationText = 'DELETE'): Promise<void> {
if (await isFeatureEnabled(this.page, 'SOFT_DELETE')) {
await expect(this.confirmationInput.element).toHaveCount(0);
} else {
await this.fillConfirmationInput(confirmationText);
}
await this.clickDelete();
}
}
@@ -0,0 +1,74 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Locator, Page } from '@playwright/test';
/**
* Unconditional pause between drag events, letting react-dnd's HTML5 backend
* commit its monitor state before the next event fires. Deliberately not in
* `TIMEOUT`: that object holds wait *ceilings* (a wait may finish sooner),
* whereas this is a fixed sleep that always costs what it says.
*/
const REACT_DND_SETTLE_MS = 50;
/**
* Drives an HTML5 drag-and-drop using synthetic native drag events.
*
* The dashboard grid uses react-dnd with the HTML5 backend
* (`react-dnd-html5-backend`), which listens for native `dragstart` /
* `dragenter` / `dragover` / `drop` events rather than the mouse events that
* Playwright's built-in `locator.dragTo()` produces. To trigger it we dispatch
* the native drag sequence ourselves, threading a single shared `DataTransfer`
* object through every event so react-dnd's monitor sees a consistent payload.
*
* Mirrors the synthetic-event sequence used by the deprecated Cypress `drag`
* helper (cypress-base/cypress/utils/index.ts).
*
* @param page - Playwright page (used to mint the shared DataTransfer)
* @param source - The draggable element (or a descendant; drag events bubble)
* @param target - The drop target element
*/
export async function html5DragAndDrop(
page: Page,
source: Locator,
target: Locator,
): Promise<void> {
// Note: we intentionally do not scrollIntoView the source. The chart card list
// is virtualized, so a separate scroll action can detach the element between
// resolution and use; dispatchEvent only requires the node to be attached.
// A single DataTransfer shared across every event in the sequence: react-dnd's
// HTML5 backend reads/writes drag state through it, so reusing one handle is
// what makes the monitor treat this as one coherent drag.
const dataTransfer = await page.evaluateHandle(() => new DataTransfer());
await source.dispatchEvent('dragstart', { dataTransfer });
// react-dnd's HTML5 backend commits monitor state (the active drag source) on a
// microtask after dragstart; a short settle avoids a race where dragover/drop
// fire before the backend considers a drag to be in progress.
await page.waitForTimeout(REACT_DND_SETTLE_MS);
// dragenter must precede dragover for react-dnd to register the hover target.
await target.dispatchEvent('dragenter', { dataTransfer });
await target.dispatchEvent('dragover', { dataTransfer });
await page.waitForTimeout(REACT_DND_SETTLE_MS);
await target.dispatchEvent('drop', { dataTransfer });
await source.dispatchEvent('dragend', { dataTransfer });
await dataTransfer.dispose();
}
@@ -17,12 +17,29 @@
* under the License.
*/
import { Page, Download, Locator } from '@playwright/test';
import { Menu } from '../components/core';
import { Page, Download, Locator, expect } from '@playwright/test';
import { Button, Input, Menu, Tabs } from '../components/core';
import { DashboardFilterBar } from '../components/dashboard';
import { gotoWithRetry } from '../helpers/navigation';
import { html5DragAndDrop } from '../helpers/dnd';
import { TIMEOUT } from '../utils/constants';
/** Tabs of the dashboard builder side pane, by their rendered label. */
type BuilderTab = 'Charts' | 'Layout elements';
/**
* Built-in draggable layout elements, by their rendered label (see
* `src/dashboard/components/gridComponents/new/`). Extension-provided elements
* carry dynamic names and are not covered here.
*/
type LayoutElementLabel =
| 'Tabs'
| 'Row'
| 'Column'
| 'Header'
| 'Text / Markdown'
| 'Divider';
/**
* Dashboard Page object for interacting with dashboards.
*/
@@ -32,9 +49,28 @@ export class DashboardPage {
private static readonly SELECTORS = {
DASHBOARD_HEADER: '[data-test="dashboard-header-container"]',
CHART_GRID_COMPONENT: '[data-test="chart-grid-component"]',
// `:visible` so the locator empties out as loaders hide; see
// waitForLoadersToSettle.
LOADING_INDICATOR: '[aria-label="Loading"]:visible',
DASHBOARD_MENU_TRIGGER: '[data-test="actions-trigger"]',
// The header-actions-menu is the data-test for the dropdown menu content
HEADER_ACTIONS_MENU: '[data-test="header-actions-menu"]',
EDIT_BUTTON: '[data-test="edit-dashboard-button"]',
BUILDER_PANE: '[data-test="dashboard-builder-sidepane"]',
CHARTS_SEARCH: '[data-test="dashboard-charts-filter-search-input"]',
CHART_CARD: '[data-test="chart-card"]',
EMPTY_DROPTARGET: '[data-test="grid-content"] .empty-droptarget',
NEW_COMPONENT: '[data-test="new-component"]',
CHART_HOLDER: '[data-test="dashboard-component-chart-holder"]',
GRID_CONTENT: '[data-test="grid-content"]',
DELETE_COMPONENT: '[data-test="dashboard-delete-component-button"]',
MARKDOWN_EDITOR: '[data-test="dashboard-markdown-editor"]',
EDITABLE_TITLE: '[data-test="editable-title-input"]',
// Ace exposes no data-test hooks; these are its own stable DOM classes.
ACE_CONTENT: '.ace_content',
ACE_TEXT_INPUT: '.ace_text-input',
RESIZE_HANDLE_BOTTOM: '.resizable-container-handle--bottom',
} as const;
constructor(page: Page) {
@@ -60,12 +96,16 @@ export class DashboardPage {
/**
* Wait for the dashboard header to be visible.
*
* The header container renders well before the grid does, so this only
* establishes that the dashboard route mounted pair it with
* {@link waitForChartsToLoad} before asserting on chart content.
*/
async waitForLoad(options?: { timeout?: number }): Promise<void> {
const timeout = options?.timeout ?? TIMEOUT.PAGE_LOAD;
await this.page.waitForSelector(DashboardPage.SELECTORS.DASHBOARD_HEADER, {
timeout,
});
await this.page
.locator(DashboardPage.SELECTORS.DASHBOARD_HEADER)
.waitFor({ state: 'visible', timeout });
}
/**
@@ -73,37 +113,80 @@ export class DashboardPage {
*/
getChart(chartId: number): Locator {
return this.page.locator(
`[data-test="chart-grid-component"][data-test-chart-id="${chartId}"]`,
`${DashboardPage.SELECTORS.CHART_GRID_COMPONENT}[data-test-chart-id="${chartId}"]`,
);
}
/**
* Wait for all charts on the dashboard to finish loading.
* Waits until no loading indicators are visible on the page.
* Wait for the dashboard's charts to mount and finish loading.
*
* Waiting only for loading indicators to clear is not enough: the grid mounts
* its spinners after the header renders, so a "no visible loader" check
* called straight after {@link waitForLoad} passes instantly against a
* dashboard that has not started rendering anything. Waiting for at least one
* chart grid component first makes the absence of loaders mean "charts
* finished" rather than "charts have not begun".
*
* Only for dashboards that have charts on an empty one this waits out
* `timeout` rather than returning. Use {@link waitForGridToLoad} there.
*/
async waitForChartsToLoad(options?: { timeout?: number }): Promise<void> {
const timeout = options?.timeout ?? TIMEOUT.API_RESPONSE;
// Use browser-context evaluation to check visibility directly.
// Loading indicators ([aria-label="Loading"]) may persist in the DOM as hidden
// elements after charts finish loading. This checks that none are currently visible,
// returning immediately when charts are already loaded (no timeout penalty).
await this.page.waitForFunction(
() => {
const loaders = document.querySelectorAll('[aria-label="Loading"]');
if (loaders.length === 0) return true;
return Array.from(loaders).every(el => {
const style = getComputedStyle(el);
return (
style.display === 'none' ||
style.visibility === 'hidden' ||
style.opacity === '0'
);
});
},
undefined,
{ timeout },
);
await this.page
.locator(DashboardPage.SELECTORS.CHART_GRID_COMPONENT)
.first()
.waitFor({ state: 'attached', timeout });
await this.waitForLoadersToSettle(timeout);
}
/**
* Wait for the dashboard grid to mount and any loading indicators to clear.
*
* The counterpart to {@link waitForChartsToLoad} for a dashboard with no
* charts on it: the grid container renders whatever the grid holds, so it
* gives the "the page got past the header" evidence that a chart component
* cannot. Prefer {@link waitForChartsToLoad} whenever charts are expected
* this cannot tell a grid that rendered empty from one whose charts have not
* begun rendering.
*/
async waitForGridToLoad(options?: { timeout?: number }): Promise<void> {
const timeout = options?.timeout ?? TIMEOUT.API_RESPONSE;
// Attached rather than visible: an empty grid collapses to zero height,
// which Playwright counts as not visible.
await this.page
.locator(DashboardPage.SELECTORS.GRID_CONTENT)
.first()
.waitFor({ state: 'attached', timeout });
await this.waitForLoadersToSettle(timeout);
}
/**
* Resolve once no loading indicator is visible.
*
* Loading indicators persist in the DOM as hidden elements after charts
* finish, so this waits for none to be *visible* rather than for none to
* exist. The `:visible` engine resolves to zero elements when they are all
* hidden, which is what `detached` then matches and it returns immediately
* when they are already settled, with no timeout penalty.
*
* Deliberately not a `getComputedStyle` check in an evaluated function:
* `display` does not inherit, so a loader inside a `display: none` ancestor
* computes to its own `display: block` and reads as visible, hanging the wait
* until the timeout. Playwright's visibility check accounts for ancestors.
*
* Loader absence is also the state of a dashboard that has not started
* rendering, which is why every caller pairs this with a wait for the content
* it expects.
*/
private async waitForLoadersToSettle(timeout: number): Promise<void> {
await this.page
.locator(DashboardPage.SELECTORS.LOADING_INDICATOR)
.first()
.waitFor({ state: 'detached', timeout });
}
/**
@@ -136,14 +219,13 @@ export class DashboardPage {
* Open the dashboard header actions menu (three-dot menu)
*/
async openHeaderActionsMenu(): Promise<void> {
await this.page.click(DashboardPage.SELECTORS.DASHBOARD_MENU_TRIGGER);
await this.page
.locator(DashboardPage.SELECTORS.DASHBOARD_MENU_TRIGGER)
.click();
// Wait for the dropdown menu to appear
await this.page.waitForSelector(
DashboardPage.SELECTORS.HEADER_ACTIONS_MENU,
{
state: 'visible',
},
);
await this.page
.locator(DashboardPage.SELECTORS.HEADER_ACTIONS_MENU)
.waitFor({ state: 'visible' });
}
/**
@@ -178,4 +260,198 @@ export class DashboardPage {
await menu.selectSubmenuItem('Download', optionText);
return downloadPromise;
}
/**
* Enter dashboard edit mode and wait for the builder side pane to appear.
*/
async enterEditMode(): Promise<void> {
const editButton = new Button(
this.page,
DashboardPage.SELECTORS.EDIT_BUTTON,
);
await editButton.click();
await this.page
.locator(DashboardPage.SELECTORS.BUILDER_PANE)
.waitFor({ state: 'visible' });
}
/**
* The builder side pane's tab bar (Charts / Layout elements).
*/
/**
* Switch the builder side pane to one of its tabs.
* @param tab - 'Charts' (existing slices) or 'Layout elements' (new components)
*/
private async openBuilderTab(tab: BuilderTab): Promise<void> {
// Scoped to `.ant-tabs` because that is the root the shared Tabs component
// expects.
const builderTabs = new Tabs(
this.page,
this.page
.locator(`${DashboardPage.SELECTORS.BUILDER_PANE} .ant-tabs`)
.first(),
);
await builderTabs.clickTab(tab);
}
/**
* Locator for chart-holder components currently placed on the grid.
* Markdown components are chart holders too use
* {@link getMarkdownEditors} when the assertion must exclude them.
*/
getChartHolders(): Locator {
return this.page.locator(DashboardPage.SELECTORS.CHART_HOLDER);
}
/**
* Drag an existing chart from the Charts pane onto the dashboard grid.
* Requires edit mode to be active.
* @param sliceName - The slice name to search for and drag
*/
async addChartByName(sliceName: string): Promise<void> {
await this.openBuilderTab('Charts');
const search = new Input(this.page, DashboardPage.SELECTORS.CHARTS_SEARCH);
await search.fill(sliceName);
const card = this.page
.locator(DashboardPage.SELECTORS.CHART_CARD)
.filter({ hasText: sliceName })
.first();
await card.waitFor({ state: 'visible' });
await html5DragAndDrop(this.page, card, this.dropTarget());
}
/**
* Drag a new Layout element (by its label) onto the dashboard grid.
* Requires edit mode to be active.
* @param label - The new-component label, e.g. 'Text / Markdown'
*/
async addLayoutElement(label: LayoutElementLabel): Promise<void> {
await this.openBuilderTab('Layout elements');
const source = this.page
.locator(DashboardPage.SELECTORS.NEW_COMPONENT)
.filter({ hasText: label })
.first();
await source.waitFor({ state: 'visible' });
await html5DragAndDrop(this.page, source, this.dropTarget());
}
/**
* The grid's empty drop target, which the grid renders while in edit mode.
*
* Only resolves while the grid is still empty. Dropping a second component
* needs a target relative to the already-placed one, not this.
*/
private dropTarget(): Locator {
return this.page.locator(DashboardPage.SELECTORS.EMPTY_DROPTARGET).first();
}
/**
* Hover the first placed chart-holder and click its delete button (edit mode).
*/
async deleteChartHolder(): Promise<void> {
const holder = this.getChartHolders().first();
await holder.hover();
const deleteButton = new Button(
this.page,
holder.locator(DashboardPage.SELECTORS.DELETE_COMPONENT),
);
await deleteButton.click();
}
/**
* Locator for markdown editor components on the grid.
*/
getMarkdownEditors(): Locator {
return this.page.locator(DashboardPage.SELECTORS.MARKDOWN_EDITOR);
}
/**
* The rendered ace document inside a markdown component. Present only once
* the component has entered its editing state.
*
* Exposed as a locator rather than routed through the `AceEditor` component:
* that component reads and writes through `ace.edit(...)` in page context,
* which both bypasses the real keystroke path under test and gives up
* web-first retries on assertions.
*
* @param markdownEditor - A locator from {@link getMarkdownEditors}
*/
getMarkdownAceContent(markdownEditor: Locator): Locator {
return markdownEditor.locator(DashboardPage.SELECTORS.ACE_CONTENT);
}
/**
* Ace's hidden textarea inside a markdown component the element that
* receives keystrokes.
*
* @param markdownEditor - A locator from {@link getMarkdownEditors}
*/
getMarkdownAceInput(markdownEditor: Locator): Locator {
return markdownEditor.locator(DashboardPage.SELECTORS.ACE_TEXT_INPUT);
}
/**
* Click the dashboard title, moving focus off whichever grid component holds
* it. Committing a markdown edit needs a click on some other element, and the
* title is the one that is always present regardless of what is on the grid.
*
* In edit mode the click focuses the title's input. That is a state change,
* not a no-op but it edits nothing on its own, so it leaves the component
* under test untouched.
*/
async blurToDashboardTitle(): Promise<void> {
await this.page
.locator(DashboardPage.SELECTORS.EDITABLE_TITLE)
.first()
.click();
}
/**
* Drag a grid component's bottom resize handle down by `deltaY` pixels.
* Requires edit mode. Uses the mouse because the resize handle is driven by
* `react-resizable`, which tracks real pointer movement.
*
* @param component - The grid component to resize
* @param deltaY - Pixels to drag downwards (positive grows the component)
* @returns The component's height before and after the drag
*/
async resizeComponent(
component: Locator,
deltaY: number,
): Promise<{ heightBefore: number; heightAfter: number }> {
const boxBefore = await component.boundingBox();
if (!boxBefore) {
throw new Error('Cannot resize a component that is not visible');
}
const handle = component
.locator(DashboardPage.SELECTORS.RESIZE_HANDLE_BOTTOM)
.last();
const handleBox = await handle.boundingBox();
if (!handleBox) {
throw new Error('Resize handle is not visible');
}
const startX = handleBox.x + handleBox.width / 2;
const startY = handleBox.y + handleBox.height / 2;
await this.page.mouse.move(startX, startY);
await this.page.mouse.down();
// Multiple steps so react-resizable sees a drag rather than a teleport.
await this.page.mouse.move(startX, startY + deltaY, { steps: 10 });
await this.page.mouse.up();
await expect
.poll(async () => (await component.boundingBox())?.height, {
message: 'Component height did not change after resize',
})
.not.toBe(boxBefore.height);
const boxAfter = await component.boundingBox();
if (!boxAfter) {
throw new Error('Component disappeared during resize');
}
return { heightBefore: boxBefore.height, heightAfter: boxAfter.height };
}
}
@@ -76,11 +76,9 @@ test('should delete a chart with confirmation', async ({
const deleteModal = new DeleteConfirmationModal(page);
await deleteModal.waitForVisible();
// Type "DELETE" to confirm
await deleteModal.fillConfirmationInput('DELETE');
// Click the Delete button
await deleteModal.clickDelete();
// Confirm: types "DELETE" while the modal is destructive, and goes straight
// through once SOFT_DELETE makes it a recoverable archive instead.
await deleteModal.confirmDeletion();
// Modal should close
await deleteModal.waitForHidden();
@@ -237,11 +235,9 @@ test('should bulk delete multiple charts', async ({
const deleteModal = new DeleteConfirmationModal(page);
await deleteModal.waitForVisible();
// Type "DELETE" to confirm
await deleteModal.fillConfirmationInput('DELETE');
// Click the Delete button
await deleteModal.clickDelete();
// Confirm: types "DELETE" while the modal is destructive, and goes straight
// through once SOFT_DELETE makes it a recoverable archive instead.
await deleteModal.confirmDeletion();
// Modal should close
await deleteModal.waitForHidden();
@@ -25,8 +25,13 @@ import {
buildSingleRowDashboardLayout,
} from '../../helpers/api/dashboard';
import { getDatasetByName } from '../../helpers/api/dataset';
import { extractIdFromResponse } from '../../helpers/api/assertions';
import { DashboardPage } from '../../pages/DashboardPage';
import { TIMEOUT } from '../../utils/constants';
import {
buildFilterJsonMetadata,
buildSelectFilter,
} from './dashboard-test-helpers';
const DATASET_NAME = 'birth_names';
const FILTER_COLUMN = 'gender';
@@ -59,12 +64,10 @@ testWithAssets(
params: JSON.stringify(chartParams),
});
expect(chartResp.ok()).toBe(true);
const chart = await chartResp.json();
const chartId: number = chart.id ?? chart.result?.id;
const chartId = await extractIdFromResponse(chartResp);
testAssets.trackChart(chartId);
// Create dashboard with chart in position_json and a native filter in json_metadata
const filterId = `NATIVE_FILTER-${Math.random().toString(36).slice(2, 10)}`;
const positionJson = buildSingleRowDashboardLayout([
{
id: chartId,
@@ -74,39 +77,17 @@ testWithAssets(
},
]);
const jsonMetadata = {
native_filter_configuration: [
{
id: filterId,
name: 'Gender',
filterType: 'filter_select',
type: 'NATIVE_FILTER',
targets: [
{
datasetId,
column: { name: FILTER_COLUMN },
},
],
controlValues: {
multiSelect: false,
enableEmptyFilter: false,
defaultToFirstItem: false,
inverseSelection: false,
searchAllOptions: false,
},
defaultDataMask: { filterState: {}, extraFormData: {} },
cascadeParentIds: [],
scope: { rootPath: ['ROOT_ID'], excluded: [] },
const jsonMetadata = buildFilterJsonMetadata({
chartsInScope: [chartId],
nativeFilters: [
buildSelectFilter({
datasetId,
column: FILTER_COLUMN,
chartsInScope: [chartId],
},
name: 'Gender',
}),
],
chart_configuration: {},
cross_filters_enabled: false,
global_chart_configuration: {
scope: { rootPath: ['ROOT_ID'], excluded: [] },
chartsInScope: [chartId],
},
};
});
const dashResp = await apiPostDashboard(page, {
dashboard_title: `clear_all_repro_${Date.now()}`,
@@ -115,8 +96,7 @@ testWithAssets(
json_metadata: JSON.stringify(jsonMetadata),
});
expect(dashResp.ok()).toBe(true);
const dashBody = await dashResp.json();
const dashboardId: number = dashBody.result?.id ?? dashBody.id;
const dashboardId = await extractIdFromResponse(dashResp);
testAssets.trackDashboard(dashboardId);
// Associate chart with the dashboard so it actually renders
@@ -81,11 +81,10 @@ test('should delete a dashboard with confirmation', async ({
const deleteModal = new DeleteConfirmationModal(page);
await deleteModal.waitForVisible();
// Type "DELETE" to confirm
await deleteModal.fillConfirmationInput('DELETE');
// Click the Delete button (waits for it to become enabled)
await deleteModal.clickDelete();
// Confirm: types "DELETE" while the modal is destructive, and goes straight
// through once SOFT_DELETE makes it a recoverable archive instead. Either
// way it waits for the action to become enabled.
await deleteModal.confirmDeletion();
// Modal should close
await deleteModal.waitForHidden();
@@ -191,11 +190,9 @@ test('should bulk delete multiple dashboards', async ({
const deleteModal = new DeleteConfirmationModal(page);
await deleteModal.waitForVisible();
// Type "DELETE" to confirm
await deleteModal.fillConfirmationInput('DELETE');
// Click the Delete button
await deleteModal.clickDelete();
// Confirm: types "DELETE" while the modal is destructive, and goes straight
// through once SOFT_DELETE makes it a recoverable archive instead.
await deleteModal.confirmDeletion();
// Modal should close
await deleteModal.waitForHidden();
@@ -62,6 +62,8 @@ interface TestDashboardResult {
interface CreateTestDashboardOptions {
/** Prefix for generated name (default: 'test_dashboard') */
prefix?: string;
/** Publish the dashboard on creation (default: false, the API default) */
published?: boolean;
}
/**
@@ -86,6 +88,8 @@ export async function createTestDashboard(
const response = await apiPostDashboard(page, {
dashboard_title: name,
// Serialized as JSON, which drops undefined — no need to omit the key.
published: options?.published,
});
if (!response.ok()) {
@@ -106,6 +110,113 @@ export async function createTestDashboard(
return { id, name };
}
/** Scope covering the whole dashboard — every filter built here is unscoped. */
const ROOT_SCOPE = { rootPath: ['ROOT_ID'], excluded: [] };
interface DataMask {
filterState: Record<string, unknown>;
extraFormData: Record<string, unknown>;
}
export interface NativeFilterConfig {
id: string;
name: string;
filterType: string;
type: string;
targets: Array<{ datasetId: number; column: { name: string } }>;
controlValues: Record<string, boolean>;
defaultDataMask: DataMask;
cascadeParentIds: string[];
scope: typeof ROOT_SCOPE;
chartsInScope: number[];
}
interface SelectFilterOptions {
/** Dataset backing the filtered column. */
datasetId: number;
/** Column the filter targets. */
column: string;
/** Charts the filter applies to. */
chartsInScope: number[];
/** Label shown in the filter bar (default: the column name). */
name?: string;
/**
* Value preselected when the dashboard loads. Omit for a filter that starts
* unset the distinction is load-bearing: a preselected filter is applied to
* the initial chart-data request, an unset one is not.
*/
defaultValue?: string;
}
/**
* Builds one `filter_select` native filter for a dashboard's `json_metadata`.
* The filter id is generated here because no test needs to know it filters are
* addressed through the filter bar UI, not by id.
*/
export function buildSelectFilter(
options: SelectFilterOptions,
): NativeFilterConfig {
const { datasetId, column, chartsInScope, name, defaultValue } = options;
return {
id: `NATIVE_FILTER-${Math.random().toString(36).slice(2, 10)}`,
name: name ?? column,
filterType: 'filter_select',
type: 'NATIVE_FILTER',
targets: [{ datasetId, column: { name: column } }],
controlValues: {
multiSelect: false,
enableEmptyFilter: false,
defaultToFirstItem: false,
inverseSelection: false,
searchAllOptions: false,
},
defaultDataMask:
defaultValue === undefined
? { filterState: {}, extraFormData: {} }
: {
filterState: { value: [defaultValue] },
extraFormData: {
filters: [{ col: column, op: 'IN', val: [defaultValue] }],
},
},
cascadeParentIds: [],
scope: ROOT_SCOPE,
chartsInScope,
};
}
interface FilterMetadataOptions {
/** Charts the dashboard's global filter scope covers. */
chartsInScope: number[];
nativeFilters: NativeFilterConfig[];
/**
* Display Controls, serialized as-is. Kept untyped and pass-through: only one
* spec builds them, so a second builder would be speculative.
*/
chartCustomizations?: Record<string, unknown>[];
}
/**
* Builds the `json_metadata` envelope a filtered dashboard needs. Cross-filters
* are off so a click on one chart cannot perturb another test's assertions.
*/
export function buildFilterJsonMetadata(
options: FilterMetadataOptions,
): Record<string, unknown> {
return {
native_filter_configuration: options.nativeFilters,
...(options.chartCustomizations && {
chart_customization_config: options.chartCustomizations,
}),
chart_configuration: {},
cross_filters_enabled: false,
global_chart_configuration: {
scope: ROOT_SCOPE,
chartsInScope: options.chartsInScope,
},
};
}
export interface DashboardChartSpec {
/** Sent as the chart's top-level `viz_type` and injected into its params. */
viz_type: string;
@@ -30,7 +30,12 @@ import {
buildSingleRowDashboardLayout,
} from '../../helpers/api/dashboard';
import { getDatasetByName } from '../../helpers/api/dataset';
import { extractIdFromResponse } from '../../helpers/api/assertions';
import { DashboardPage } from '../../pages/DashboardPage';
import {
buildFilterJsonMetadata,
buildSelectFilter,
} from './dashboard-test-helpers';
// Record video regardless of pass/fail (before/after clips).
testWithAssets.use({ video: 'on' });
@@ -72,8 +77,7 @@ testWithAssets(
params: JSON.stringify(chartParams),
});
expect(chartResp.ok()).toBe(true);
const chart = await chartResp.json();
const chartId: number = chart.id ?? chart.result?.id;
const chartId = await extractIdFromResponse(chartResp);
testAssets.trackChart(chartId);
const positionJson = buildSingleRowDashboardLayout([
@@ -86,33 +90,21 @@ testWithAssets(
]);
// 2. json_metadata: one dashboard filter + one Display Control.
const filterId = `NATIVE_FILTER-${Math.random().toString(36).slice(2, 10)}`;
const customizationId = `CHART_CUSTOMIZATION-${Math.random()
.toString(36)
.slice(2, 10)}`;
const jsonMetadata = {
native_filter_configuration: [
{
id: filterId,
name: 'Gender',
filterType: 'filter_select',
type: 'NATIVE_FILTER',
targets: [{ datasetId, column: { name: FILTER_COLUMN } }],
controlValues: {
multiSelect: false,
enableEmptyFilter: false,
defaultToFirstItem: false,
inverseSelection: false,
searchAllOptions: false,
},
defaultDataMask: { filterState: {}, extraFormData: {} },
cascadeParentIds: [],
scope: { rootPath: ['ROOT_ID'], excluded: [] },
const jsonMetadata = buildFilterJsonMetadata({
chartsInScope: [chartId],
nativeFilters: [
buildSelectFilter({
datasetId,
column: FILTER_COLUMN,
chartsInScope: [chartId],
},
name: 'Gender',
}),
],
chart_customization_config: [
chartCustomizations: [
{
id: customizationId,
type: 'CHART_CUSTOMIZATION',
@@ -127,13 +119,7 @@ testWithAssets(
removed: false,
},
],
chart_configuration: {},
cross_filters_enabled: false,
global_chart_configuration: {
scope: { rootPath: ['ROOT_ID'], excluded: [] },
chartsInScope: [chartId],
},
};
});
const dashResp = await apiPostDashboard(page, {
dashboard_title: `display_control_repro_${Date.now()}`,
@@ -142,8 +128,7 @@ testWithAssets(
json_metadata: JSON.stringify(jsonMetadata),
});
expect(dashResp.ok()).toBe(true);
const dashBody = await dashResp.json();
const dashboardId: number = dashBody.result?.id ?? dashBody.id;
const dashboardId = await extractIdFromResponse(dashResp);
testAssets.trackDashboard(dashboardId);
const linkResp = await apiPut(page, `api/v1/chart/${chartId}`, {
@@ -155,14 +140,22 @@ testWithAssets(
const dashboardPage = new DashboardPage(page);
await dashboardPage.gotoById(dashboardId);
await dashboardPage.waitForLoad({ timeout: 30000 });
await dashboardPage.waitForChartsToLoad({ timeout: 8000 }).catch(() => {});
/**
* Best-effort settle after each mutation. Every assertion below targets the
* filter bar rather than chart content, so a chart that is still querying
* must not fail the test but giving charts a chance to finish keeps the
* bar from being re-rendered underneath the assertions.
*/
const settleCharts = () =>
dashboardPage.waitForChartsToLoad({ timeout: 8000 }).catch(() => {});
await settleCharts();
const filterBar = await dashboardPage.waitForFilterBar();
// Both the Gender filter and the Time grain Display Control should render.
await expect(dashboardPage.getDisplayControlsHeader()).toBeVisible();
await expect(dashboardPage.getDisplayControl('Time grain')).toBeVisible();
// eslint-disable-next-line no-console
console.log('STEP 1: Display control "Time grain" is present in the bar.');
await shot('01-initial-bar');
// 4. Open the filters config modal via the settings gear.
@@ -172,40 +165,26 @@ testWithAssets(
// 5. Delete the "Time grain" Display Control in the modal sidebar.
await modal.removeDisplayControl('Time grain');
await expect(modal.getRemovedMarker()).toBeVisible();
// eslint-disable-next-line no-console
console.log('STEP 2: Display control marked (Removed) in modal.');
await shot('03-modal-removed');
// 6. Save the modal.
await modal.clickSave();
await modal.waitForHidden({ timeout: 20000 });
await dashboardPage.waitForChartsToLoad({ timeout: 8000 }).catch(() => {});
await settleCharts();
await shot('04-after-save');
const goneAfterSave = await dashboardPage
.getDisplayControl('Time grain')
.isVisible()
.catch(() => false);
// eslint-disable-next-line no-console
console.log(
`STEP 3: After save, "Time grain" visible in bar = ${goneAfterSave}`,
);
// 7. Click Apply Filters.
await filterBar.applyIfEnabled();
await dashboardPage.waitForChartsToLoad({ timeout: 8000 }).catch(() => {});
await settleCharts();
/**
* Hold before asserting. The bug this guards against is the control coming
* *back*, and `toHaveCount(0)` passes the instant it is absent so without
* a pause the assertion can sample the gap before the re-render and pass on
* a dashboard that is about to fail. The wait is the reappearance window.
*/
await page.waitForTimeout(1500);
await shot('05-after-apply');
const reappeared = await dashboardPage
.getDisplayControl('Time grain')
.isVisible()
.catch(() => false);
// eslint-disable-next-line no-console
console.log(
`STEP 4: After Apply Filters, "Time grain" reappeared = ${reappeared}`,
);
// The deleted Display Control must stay gone.
await expect(
dashboardPage.getDisplayControl('Time grain'),
@@ -0,0 +1,200 @@
/**
* 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 edit-mode component tests these replace the deprecated Cypress
* spec cypress-base/cypress/e2e/dashboard/editmode.test.ts, deleted in the same
* change. They cover the chart/markdown drag-and-drop workflows that the
* upstream Cypress notes flagged as the one part of edit mode that genuinely
* requires E2E coverage ("Chart drag/drop functionality requires true E2E
* testing"). The grid uses react-dnd with the HTML5 backend, so drags are
* driven by synthetic native drag events (see helpers/dnd.ts).
*
* Coverage here is a superset of the Cypress spec, which by the time of the
* migration held a single "should add charts" test its "Color consistency"
* block had already been dropped upstream as permanently skipped (it read
* per-series colors off an `.nv-legend-symbol` SVG `fill` that ECharts, which
* renders to <canvas>, no longer produces). That color-precedence logic is
* covered by Jest/RTL, not by E2E.
*/
import {
testWithAssets,
expect,
type TestAssets,
} from '../../helpers/fixtures';
import { apiPostChart } from '../../helpers/api/chart';
import { getDatasetByName } from '../../helpers/api/dataset';
import { extractIdFromResponse } from '../../helpers/api/assertions';
import { DashboardPage } from '../../pages/DashboardPage';
import { createTestDashboard } from './dashboard-test-helpers';
import type { Page, TestInfo } from '@playwright/test';
const DATASET_NAME = 'birth_names';
/**
* How long one click on the markdown component gets to bring up the ace editor
* before the retry loop tries again, and how long the whole loop gets. The
* per-attempt budget is deliberately short: the failure mode is a swallowed
* click, and retrying is cheaper than waiting out the full budget once.
*/
const MARKDOWN_EDIT_ATTEMPT_TIMEOUT = 2000;
const MARKDOWN_EDIT_TOTAL_TIMEOUT = 20000;
/** Downward drag distance for the resize assertion — several grid rows. */
const RESIZE_DELTA_PX = 150;
/** Create a hermetic chart from birth_names, NOT placed on any dashboard. */
async function createChart(
page: Page,
testAssets: TestAssets,
testInfo: TestInfo,
): Promise<string> {
const dataset = await getDatasetByName(page, DATASET_NAME);
if (!dataset) {
throw new Error(`Dataset ${DATASET_NAME} not found`);
}
const sliceName = `edit_mode_chart_${Date.now()}_${testInfo.parallelIndex}`;
const resp = await apiPostChart(page, {
slice_name: sliceName,
viz_type: 'big_number_total',
datasource_id: dataset.id,
datasource_type: 'table',
params: JSON.stringify({
datasource: `${dataset.id}__table`,
viz_type: 'big_number_total',
metric: 'count',
}),
});
expect(resp.ok()).toBe(true);
testAssets.trackChart(await extractIdFromResponse(resp));
return sliceName;
}
/**
* Create the empty published dashboard every test in this file starts from,
* open it, and enter edit mode. Returns the page object positioned on the
* builder, ready for a drag.
*/
async function openEmptyDashboardInEditMode(
page: Page,
testAssets: TestAssets,
testInfo: TestInfo,
): Promise<DashboardPage> {
const { id } = await createTestDashboard(page, testAssets, testInfo, {
prefix: 'edit_mode',
published: true,
});
const dashboard = new DashboardPage(page);
await dashboard.gotoById(id);
await dashboard.waitForLoad();
await dashboard.enterEditMode();
return dashboard;
}
testWithAssets(
'edit mode: add a chart to the dashboard via drag-and-drop',
async ({ page, testAssets }, testInfo) => {
const sliceName = await createChart(page, testAssets, testInfo);
const dashboard = await openEmptyDashboardInEditMode(
page,
testAssets,
testInfo,
);
await expect(dashboard.getChartHolders()).toHaveCount(0);
await dashboard.addChartByName(sliceName);
await expect(dashboard.getChartHolders()).toHaveCount(1);
},
);
testWithAssets(
'edit mode: remove an added chart from the dashboard',
async ({ page, testAssets }, testInfo) => {
const sliceName = await createChart(page, testAssets, testInfo);
const dashboard = await openEmptyDashboardInEditMode(
page,
testAssets,
testInfo,
);
await dashboard.addChartByName(sliceName);
await expect(dashboard.getChartHolders()).toHaveCount(1);
await dashboard.deleteChartHolder();
await expect(dashboard.getChartHolders()).toHaveCount(0);
},
);
testWithAssets(
'edit mode: add a markdown component via drag-and-drop',
async ({ page, testAssets }, testInfo) => {
// Heaviest edit-mode flow (drag + ace edit + commit + mouse resize); give it
// extra headroom so it stays reliable when the suite runs in parallel.
testWithAssets.slow();
const dashboard = await openEmptyDashboardInEditMode(
page,
testAssets,
testInfo,
);
await dashboard.addLayoutElement('Text / Markdown');
const editor = dashboard.getMarkdownEditors().first();
await expect(editor).toBeVisible();
// Enter edit mode by focusing the component. The markdown enters edit on a
// document-level focus handler attached after mount, so a single early click
// can be missed under load; retry until the ace editor appears. Click the
// rendered "Header 1" heading element specifically (never the trailing
// hyperlink in the default content), so a stray click can't navigate away.
const aceContent = dashboard.getMarkdownAceContent(editor);
const heading = editor.locator('h1', { hasText: 'Header 1' });
await expect(async () => {
if (await aceContent.isVisible()) return;
await heading.click();
await expect(aceContent).toBeVisible({
timeout: MARKDOWN_EDIT_ATTEMPT_TIMEOUT,
});
}).toPass({ timeout: MARKDOWN_EDIT_TOTAL_TIMEOUT });
await expect(aceContent).toContainText('Header 1');
await expect(aceContent).toContainText('markdown formatting');
// Replace the content and confirm the edit is reflected.
const aceInput = dashboard.getMarkdownAceInput(editor);
await aceInput.press('ControlOrMeta+a');
await aceInput.press('Delete');
await aceInput.pressSequentially('Test resize');
await expect(aceContent).toContainText('Test resize');
// Commit by clicking outside the component. Ace unmounting is what proves
// the component left its editing state — the wrapper contains "Test resize"
// either way, since ace holds that text before the click too.
await dashboard.blurToDashboardTitle();
await expect(aceContent).toBeHidden();
await expect(editor).toContainText('Test resize');
// Resize via the bottom handle and confirm the component grew taller.
const { heightBefore, heightAfter } = await dashboard.resizeComponent(
editor,
RESIZE_DELTA_PX,
);
expect(heightAfter).toBeGreaterThan(heightBefore);
},
);
@@ -39,26 +39,26 @@ import {
apiPostDashboard,
buildSingleRowDashboardLayout,
} from '../../helpers/api/dashboard';
import { getDatasetByName } from '../../helpers/api/dataset';
import { extractIdFromResponse } from '../../helpers/api/assertions';
import { DashboardPage } from '../../pages/DashboardPage';
import {
buildFilterJsonMetadata,
buildSelectFilter,
} from './dashboard-test-helpers';
const DATASET_NAME = 'birth_names';
const FILTER_COLUMN = 'gender';
const FILTER_VALUE = 'boy';
async function findDatasetIdByName(page: any, name: string): Promise<number> {
const query = `(filters:!((col:table_name,opr:eq,value:'${name}')))`;
const resp = await page.request.get(`api/v1/dataset/?q=${query}`);
const body = await resp.json();
if (!body.result?.length) {
throw new Error(`Dataset ${name} not found`);
}
return body.result[0].id;
}
testWithAssets(
'Mixed chart applies dashboard filter to both queries (#29519)',
async ({ page, testAssets }) => {
const datasetId = await findDatasetIdByName(page, DATASET_NAME);
const dataset = await getDatasetByName(page, DATASET_NAME);
if (!dataset) {
throw new Error(`Dataset ${DATASET_NAME} not found`);
}
const datasetId = dataset.id;
const chartParams = {
datasource: `${datasetId}__table`,
@@ -86,10 +86,9 @@ testWithAssets(
params: JSON.stringify(chartParams),
});
expect(chartResp.ok()).toBe(true);
const chartId: number = (await chartResp.json()).id;
const chartId = await extractIdFromResponse(chartResp);
testAssets.trackChart(chartId);
const filterId = `NATIVE_FILTER-${Math.random().toString(36).slice(2, 10)}`;
const positionJson = buildSingleRowDashboardLayout([
{
id: chartId,
@@ -98,39 +97,20 @@ testWithAssets(
height: 60,
},
]);
const jsonMetadata = {
native_filter_configuration: [
{
id: filterId,
name: 'Gender',
filterType: 'filter_select',
type: 'NATIVE_FILTER',
targets: [{ datasetId, column: { name: FILTER_COLUMN } }],
controlValues: {
multiSelect: false,
enableEmptyFilter: false,
defaultToFirstItem: false,
inverseSelection: false,
searchAllOptions: false,
},
defaultDataMask: {
filterState: { value: [FILTER_VALUE] },
extraFormData: {
filters: [{ col: FILTER_COLUMN, op: 'IN', val: [FILTER_VALUE] }],
},
},
cascadeParentIds: [],
scope: { rootPath: ['ROOT_ID'], excluded: [] },
// Preselect the filter value so it is already applied on the dashboard's
// first chart-data request — that request is what the assertions inspect.
const jsonMetadata = buildFilterJsonMetadata({
chartsInScope: [chartId],
nativeFilters: [
buildSelectFilter({
datasetId,
column: FILTER_COLUMN,
chartsInScope: [chartId],
},
name: 'Gender',
defaultValue: FILTER_VALUE,
}),
],
chart_configuration: {},
cross_filters_enabled: false,
global_chart_configuration: {
scope: { rootPath: ['ROOT_ID'], excluded: [] },
chartsInScope: [chartId],
},
};
});
const dashResp = await apiPostDashboard(page, {
dashboard_title: `mixed_filter_repro_${Date.now()}`,
published: true,
@@ -138,8 +118,7 @@ testWithAssets(
json_metadata: JSON.stringify(jsonMetadata),
});
expect(dashResp.ok()).toBe(true);
const dashBody = await dashResp.json();
const dashboardId: number = dashBody.result?.id ?? dashBody.id;
const dashboardId = await extractIdFromResponse(dashResp);
testAssets.trackDashboard(dashboardId);
await apiPut(page, `api/v1/chart/${chartId}`, {
@@ -128,16 +128,15 @@ test('non-admin user can view a themed dashboard without 403 or infinite spinner
// --- NON-ADMIN USER PHASE (page has no cached auth via test.use) ---
// 4. Instrument network: track any /api/v1/theme/ requests and 403 responses
// 4. Instrument network: track any /api/v1/theme/ request, with its status.
// Recording the status rather than asserting on a separate 403-only array
// keeps the diagnostic — a failure prints whether the calls were forbidden
// or merely unexpected — without a second, subsumed assertion.
const themeApiRequests: string[] = [];
const forbiddenResponses: string[] = [];
page.on('response', response => {
const url = response.url();
if (url.includes('/api/v1/theme/')) {
themeApiRequests.push(url);
}
if (response.status() === 403 && url.includes('/api/v1/theme/')) {
forbiddenResponses.push(url);
themeApiRequests.push(`${response.status()} ${url}`);
}
});
@@ -152,14 +151,19 @@ test('non-admin user can view a themed dashboard without 403 or infinite spinner
const dashboardPage = new DashboardPage(page);
await dashboardPage.gotoById(dashboardId!);
// 7. Assert dashboard fully loads (not stuck on infinite spinner)
// 7. Assert dashboard fully loads (not stuck on infinite spinner).
// The dashboard is created with no position_json, so its grid renders
// empty — there is no chart to wait for, only the grid itself.
await dashboardPage.waitForLoad({ timeout: TIMEOUT.PAGE_LOAD });
await dashboardPage.waitForChartsToLoad();
await dashboardPage.waitForGridToLoad();
// 8. Assert no /api/v1/theme/ requests were made (theme data comes from dashboard response)
expect(themeApiRequests).toHaveLength(0);
// Assert no 403 responses on /api/v1/theme/ (scoped to avoid login/unrelated 403 noise)
expect(forbiddenResponses).toHaveLength(0);
// 8. A non-admin must render the themed dashboard without ever calling the
// theme API — theme data rides along on the dashboard response, and the
// endpoint itself is admin-only, so any call here would 403 and break them.
expect(
themeApiRequests,
'Non-admin dashboard load must not call the theme API',
).toHaveLength(0);
} finally {
// Cleanup: delete test resources using admin context
if (dashboardId) {
@@ -37,6 +37,7 @@ import {
ENDPOINTS,
} from '../../helpers/api/dataset';
import { createTestDataset } from './dataset-test-helpers';
import { isFeatureEnabled } from '../../helpers/featureFlags';
import {
waitForGet,
waitForPost,
@@ -120,19 +121,21 @@ test('should delete a dataset with confirmation', async ({
const deleteModal = new DeleteConfirmationModal(page);
await deleteModal.waitForVisible();
// Type "DELETE" to confirm
await deleteModal.fillConfirmationInput('DELETE');
// Click the Delete button
await deleteModal.clickDelete();
// Confirm: types "DELETE" while the modal is destructive, and goes straight
// through once SOFT_DELETE makes it a recoverable archive instead.
await deleteModal.confirmDeletion();
// Modal should close
await deleteModal.waitForHidden();
// Verify success toast appears with correct message.
// Verify success toast appears with correct message. The copy names what
// actually happened, so it tracks the mode: archiving is not deleting, and
// a toast saying otherwise would misreport a recoverable action as final.
const toast = new Toast(page);
await expect(toast.getSuccess()).toBeVisible();
await expect(toast.getMessage()).toContainText('Deleted');
await expect(toast.getMessage()).toContainText(
(await isFeatureEnabled(page, 'SOFT_DELETE')) ? 'Archived' : 'Deleted',
);
// Verify dataset is removed from list (deleted rows are removed from the DOM, so assert count rather than visibility)
await expect(datasetListPage.getDatasetRow(datasetName)).toHaveCount(0, {
@@ -431,11 +434,9 @@ test('should bulk delete multiple datasets', async ({
const deleteModal = new DeleteConfirmationModal(page);
await deleteModal.waitForVisible();
// Type "DELETE" to confirm
await deleteModal.fillConfirmationInput('DELETE');
// Click the Delete button
await deleteModal.clickDelete();
// Confirm: types "DELETE" while the modal is destructive, and goes straight
// through once SOFT_DELETE makes it a recoverable archive instead.
await deleteModal.confirmDeletion();
// Modal should close
await deleteModal.waitForHidden();
@@ -38,6 +38,14 @@
*
* NOTE: the embedded suite only runs when the embedded SDK bundle is built and
* INCLUDE_EMBEDDED=true (CI sets both). It is skipped otherwise.
*
* NOTE: the embedded project runs with admin storageState, and the session
* cookie the iframe ends up with is racy: the /embedded/<uuid> response
* rotates it to an anonymous session while the SDK's parallel csrf and
* guest-token fetches rewrite the admin one, so chart data requests are
* evaluated as either admin or guest depending on which response lands last.
* Fixture charts must therefore hold up under GUEST evaluation see the
* query_context note below or the suite only passes when admin wins.
*/
import { test, expect, Browser, BrowserContext, Page } from '@playwright/test';
import { createServer, IncomingMessage, ServerResponse, Server } from 'http';
@@ -182,12 +190,43 @@ test.describe('Embedded Pivot Table collapse state (#33406)', () => {
row_limit: 1000,
order_desc: true,
};
// Charts saved through Explore always persist a query_context alongside
// params. Store one here too: guest (embedded) requests are validated
// against the stored chart, and a params-only chart makes the guest
// payload look tampered (its query `columns` aren't found on the chart),
// failing every chart data request with a 403.
const queryContext = {
datasource: { id: datasetId, type: 'table' },
force: false,
queries: [
{
filters: [],
extras: { having: '', where: '' },
applied_time_extras: {},
columns: ['state', 'name'],
metrics: ['count'],
orderby: [['count', false]],
annotation_layers: [],
row_limit: 1000,
series_limit: 0,
order_desc: true,
url_params: {},
custom_params: {},
custom_form_data: {},
},
],
form_data: params,
result_format: 'json',
result_type: 'full',
};
const chartResp = await apiPost(setupPage, 'api/v1/chart/', {
slice_name: `pivot_collapse_repro_${Date.now()}`,
viz_type: 'pivot_table_v2',
datasource_id: datasetId,
datasource_type: 'table',
params: JSON.stringify(params),
query_context: JSON.stringify(queryContext),
query_context_generation: true,
});
chartId = (await chartResp.json()).id;
@@ -0,0 +1,173 @@
/**
* 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.
*/
/**
* Activity-log (version history) coverage for charts.
*
* Requires the running instance to have VERSION_HISTORY +
* ENABLE_VERSIONING_CAPTURE enabled (the docker dev stack does).
*
* The test creates its own chart and renames it twice via the authenticated
* REST API to guarantee a deterministic, non-"first tracked save" entry, then
* opens the Explore version-history panel and asserts the rendered entries
* exercising the descriptive-row rendering (no raw layout node ids / UUIDs
* leaking into the timeline).
*
* It deliberately does not touch the seeded example charts. Renaming those
* made the test order-dependent (it picked the most recently changed charts),
* unsafe to run on parallel workers, and left version records behind that no
* revert could remove version history being append-only is the point.
*/
import { Page } from '@playwright/test';
import rison from 'rison';
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 { TIMEOUT } from '../../utils/constants';
const DATASET_NAME = 'birth_names';
// 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.
const OPAQUE_ID =
/\b(CHART|ROW|COLUMN|TAB|TABS|HEADER|MARKDOWN|DIVIDER|GRID)-|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
/**
* Resolve the current user's USER-type subject id. Chart `editors` (and the
* `can_overwrite` gate derived from them) live in the Subject id space, not
* the user id space.
*/
async function currentUserSubjectId(page: Page): Promise<number> {
const meRes = await apiGet(page, 'api/v1/me/');
expect(meRes.ok(), 'current user request').toBeTruthy();
const userId = (await meRes.json()).result.id;
const q = encodeURIComponent(
`(filters:!((col:user_id,opr:eq,value:${userId})))`,
);
const res = await apiGet(page, `api/v1/security/subject/?q=${q}`);
expect(res.ok(), 'subject lookup request').toBeTruthy();
const subjects = (await res.json()).result;
expect(
subjects.length,
`USER subject exists for user ${userId}`,
).toBeGreaterThan(0);
return subjects[0].id;
}
/**
* Any dataset will do: the chart exists only to carry version records, and
* nothing here asserts on rendered data. Prefer the conventional example so
* 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> {
const named = await getDatasetByName(page, DATASET_NAME);
if (named) {
return named.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;
}
/** Open the Explore "Additional actions → View version history" panel. */
async function openVersionHistory(page: Page): Promise<void> {
await page.locator('[data-test="actions-trigger"]').click();
await page.getByRole('menuitem', { name: 'View version history' }).click();
await expect(
page.locator('[aria-label="Version history"]'),
'version history panel opens',
).toBeVisible();
}
testWithAssets(
'chart activity log renders deterministic descriptive entries',
async ({ page, testAssets }) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
const datasetId = await anyDatasetId(page);
const baseName = `version_history_${Date.now()}`;
const chartResp = await apiPostChart(page, {
slice_name: baseName,
viz_type: 'table',
datasource_id: datasetId,
datasource_type: 'table',
// Raw mode with no column references, so the chart is valid against any
// dataset.
params: JSON.stringify({
datasource: `${datasetId}__table`,
viz_type: 'table',
query_mode: 'raw',
all_columns: [],
adhoc_filters: [],
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);
// Version history is only offered on charts the user can overwrite, i.e.
// can edit — so claim editorship. A chart created through the API starts
// without editors.
// Two renames: the first edit on an as-yet-untracked chart collapses into
// "first tracked save"; the second is a normal descriptive save.
const adminSubjectId = await currentUserSubjectId(page);
await apiPutChart(page, chartId, {
editors: [adminSubjectId],
slice_name: `${baseName} ·vh1`,
});
await apiPutChart(page, chartId, { slice_name: `${baseName} ·vh2` });
await page.goto(`explore/?slice_id=${chartId}`);
await openVersionHistory(page);
const panel = page.locator('[aria-label="Version history"]');
const groups = panel.locator('[data-test="version-history-save-group"]');
await expect(groups.first(), 'shows a save group').toBeVisible();
// The newest save is the live one and is tagged "Current".
await expect(panel.getByText('Current')).toBeVisible();
// Expand the newest group and confirm the rename rendered as a descriptive
// action row — not a raw field path.
await groups.first().getByRole('button').first().click();
const rows = panel.locator('[data-test="version-history-action-row"]');
await expect(rows.first(), 'shows action rows').toBeVisible();
await expect(panel.getByText(/Chart renamed to /).first()).toBeVisible();
// No synthetic identifiers may surface in any visible timeline row.
const panelText = (await panel.innerText()) ?? '';
expect(
OPAQUE_ID.test(panelText),
`panel leaks a raw id/uuid:\n${panelText}`,
).toBeFalsy();
},
);
@@ -1,143 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable react/no-array-index-key */
import { memo } from 'react';
import { styled } from '@apache-superset/core/theme';
import TTestTable, { DataEntry } from './TTestTable';
interface PairedTTestProps {
alpha?: number;
className?: string;
data: Record<string, DataEntry[]>;
groups: string[];
liftValPrec?: number;
metrics: string[];
pValPrec?: number;
}
const StyledDiv = styled.div`
${({ theme }) => `
.superset-legacy-chart-paired-t-test .scrollbar-container {
overflow: auto;
}
.paired-ttest-table .scrollbar-content {
padding-left: ${theme.sizeUnit}px;
padding-right: ${theme.sizeUnit}px;
margin-bottom: 0;
}
.paired-ttest-table table {
margin-bottom: 0;
}
.paired-ttest-table h1 {
margin-left: ${theme.sizeUnit}px;
}
.reactable-data tr {
font-feature-settings: 'tnum' 1;
}
.reactable-data tr,
.reactable-header-sortable {
-webkit-transition: ease-in-out 0.1s;
transition: ease-in-out 0.1s;
}
.reactable-data tr:hover {
background-color: ${theme.colorFillTertiary};
}
.reactable-data tr .false {
color: ${theme.colorError};
}
.reactable-data tr .true {
color: ${theme.colorSuccess};
}
.reactable-data tr .control {
color: ${theme.colorPrimary};
}
.reactable-data tr .invalid {
color: ${theme.colorWarning};
}
.reactable-data .control td {
background-color: ${theme.colorFillTertiary};
}
.reactable-header-sortable:hover,
.reactable-header-sortable:focus,
.reactable-header-sort-asc,
.reactable-header-sort-desc {
background-color: ${theme.colorFillTertiary};
position: relative;
}
.reactable-header-sort-asc:after {
content: '\\25bc';
position: absolute;
right: ${theme.sizeUnit * 3}px;
}
.reactable-header-sort-desc:after {
content: '\\25b2';
position: absolute;
right: ${theme.sizeUnit * 3}px;
}
`}
`;
function PairedTTest({
alpha = 0.05,
className = '',
data,
groups,
liftValPrec = 4,
metrics,
pValPrec = 6,
}: PairedTTestProps) {
return (
<StyledDiv>
<div className={`superset-legacy-chart-paired-t-test ${className}`}>
<div className="paired-ttest-table">
<div className="scrollbar-content">
{metrics.map((metric, i) => (
<TTestTable
key={i}
metric={metric}
groups={groups}
data={data[metric]}
alpha={alpha}
pValPrec={Math.min(pValPrec, 32)}
liftValPrec={Math.min(liftValPrec, 32)}
/>
))}
</div>
</div>
</div>
</StyledDiv>
);
}
// memo preserves the shallow-prop render bailout of the PureComponent original
export default memo(PairedTTest);
@@ -1,61 +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.
-->
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.20.0](https://github.com/apache/superset/compare/v2021.41.0...v0.20.0) (2024-09-09)
### Bug Fixes
- **Dashboard:** Color inconsistency on refreshes and conflicts ([#27439](https://github.com/apache/superset/issues/27439)) ([313ee59](https://github.com/apache/superset/commit/313ee596f5435894f857d72be7269d5070c8c964))
- **explore:** make SORT-Descending visible if Sort-by has value ([#17726](https://github.com/apache/superset/issues/17726)) ([d5768ab](https://github.com/apache/superset/commit/d5768ab649a70fd4f541ad4982498f622160b220))
### Features
- apply standardized form data to tier 2 charts ([#20530](https://github.com/apache/superset/issues/20530)) ([de524bc](https://github.com/apache/superset/commit/de524bc59f011fd361dcdb7d35c2cb51f7eba442))
- **explore:** improve UI in the control panel ([#19748](https://github.com/apache/superset/issues/19748)) ([e3a54aa](https://github.com/apache/superset/commit/e3a54aa3c15bdd0c970aa73f898288a408205c97))
- improve color consistency (save all labels) ([#19038](https://github.com/apache/superset/issues/19038)) ([dc57508](https://github.com/apache/superset/commit/dc575080d7e43d40b1734bb8f44fdc291cb95b11))
- update time comparison choices (again) ([#17968](https://github.com/apache/superset/issues/17968)) ([05d9cde](https://github.com/apache/superset/commit/05d9cde203b99f8c63106446f0be58668cc9f0c9))
- update time comparison choices (again) ([#22458](https://github.com/apache/superset/issues/22458)) ([9e81c3a](https://github.com/apache/superset/commit/9e81c3a1192a18226d505178d16e1e395917a719))
# [0.19.0](https://github.com/apache/superset/compare/v2021.41.0...v0.19.0) (2024-09-07)
### Bug Fixes
- **Dashboard:** Color inconsistency on refreshes and conflicts ([#27439](https://github.com/apache/superset/issues/27439)) ([313ee59](https://github.com/apache/superset/commit/313ee596f5435894f857d72be7269d5070c8c964))
- **explore:** make SORT-Descending visible if Sort-by has value ([#17726](https://github.com/apache/superset/issues/17726)) ([d5768ab](https://github.com/apache/superset/commit/d5768ab649a70fd4f541ad4982498f622160b220))
### Features
- apply standardized form data to tier 2 charts ([#20530](https://github.com/apache/superset/issues/20530)) ([de524bc](https://github.com/apache/superset/commit/de524bc59f011fd361dcdb7d35c2cb51f7eba442))
- **explore:** improve UI in the control panel ([#19748](https://github.com/apache/superset/issues/19748)) ([e3a54aa](https://github.com/apache/superset/commit/e3a54aa3c15bdd0c970aa73f898288a408205c97))
- improve color consistency (save all labels) ([#19038](https://github.com/apache/superset/issues/19038)) ([dc57508](https://github.com/apache/superset/commit/dc575080d7e43d40b1734bb8f44fdc291cb95b11))
- update time comparison choices (again) ([#17968](https://github.com/apache/superset/issues/17968)) ([05d9cde](https://github.com/apache/superset/commit/05d9cde203b99f8c63106446f0be58668cc9f0c9))
- update time comparison choices (again) ([#22458](https://github.com/apache/superset/issues/22458)) ([9e81c3a](https://github.com/apache/superset/commit/9e81c3a1192a18226d505178d16e1e395917a719))
# [0.18.0](https://github.com/apache-superset/superset-ui/compare/v0.17.87...v0.18.0) (2021-08-30)
**Note:** Version bump only for package @superset-ui/legacy-plugin-chart-rose
## [0.17.61](https://github.com/apache-superset/superset-ui/compare/v0.17.60...v0.17.61) (2021-07-02)
**Note:** Version bump only for package @superset-ui/legacy-plugin-chart-rose
@@ -1,52 +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.
-->
## @superset-ui/legacy-plugin-chart-rose
[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-rose.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/legacy-plugin-chart-rose)
[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Flegacy-plugin-chart-rose?style=flat)](https://libraries.io/npm/@superset-ui%2Flegacy-plugin-chart-rose)
This plugin provides Nightingale Rose Diagram for Superset.
### Usage
Configure `key`, which can be any `string`, and register the plugin. This `key` will be used to
lookup this chart throughout the app.
```js
import RoseChartPlugin from '@superset-ui/legacy-plugin-chart-rose';
new RoseChartPlugin().configure({ key: 'rose' }).register();
```
Then use it via `SuperChart`. See
[storybook](https://apache-superset.github.io/superset-ui-plugins/?selectedKind=plugin-chart-rose)
for more details.
```js
<SuperChart
chartType="rose"
width={600}
height={600}
formData={...}
queriesData={[{
data: {...},
}]}
/>
```
@@ -1,40 +0,0 @@
{
"name": "@superset-ui/legacy-plugin-chart-rose",
"version": "0.20.3",
"description": "Superset Legacy Chart - Nightingale Rose Diagram",
"keywords": [
"superset"
],
"homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-plugin-chart-rose#readme",
"bugs": {
"url": "https://github.com/apache/superset/issues"
},
"license": "Apache-2.0",
"author": "Superset",
"repository": {
"type": "git",
"url": "https://github.com/apache/superset.git",
"directory": "superset-frontend/plugins/legacy-plugin-chart-rose"
},
"files": [
"esm",
"lib"
],
"main": "lib/index.js",
"module": "esm/index.js",
"publishConfig": {
"access": "public"
},
"dependencies": {
"d3": "^3.5.17",
"nvd3-fork": "^2.0.5",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@emotion/react": "^11.4.1",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.3.0"
}
}
@@ -1,87 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { reactify } from '@superset-ui/core';
import { styled, css } from '@apache-superset/core/theme';
import { Global } from '@emotion/react';
import Component from './Rose';
// Type-erase the render function to allow flexible prop spreading in the wrapper.
// The Rose render function has typed props, but the wrapper passes props via spread
// which TypeScript cannot verify at compile time. Props are validated at runtime.
const ReactComponent = reactify(
Component as unknown as (
container: HTMLDivElement,
props: Record<string, unknown>,
) => void,
);
interface RoseWrapperProps {
className?: string;
[key: string]: unknown;
}
const Rose = ({ className, ...otherProps }: RoseWrapperProps) => (
<div className={className}>
<Global
styles={theme => css`
.tooltip {
line-height: 1;
padding: ${theme.sizeUnit * 3}px;
background: ${theme.colorBgElevated};
color: ${theme.colorText};
border-radius: 4px;
pointer-events: none;
z-index: 1000;
font-size: ${theme.fontSizeSM}px;
}
`}
/>
<ReactComponent {...otherProps} />
</div>
);
export default styled(Rose)`
${({ theme }) => `
.superset-legacy-chart-rose path {
transition: fill-opacity 180ms linear;
stroke: ${theme.colorBorder};
stroke-width: 1px;
stroke-opacity: 1;
fill-opacity: 0.75;
}
.superset-legacy-chart-rose text {
font-size: ${theme.fontSizeSM}px;
font-family: ${theme.fontFamily};
pointer-events: none;
}
.superset-legacy-chart-rose .clickable path {
cursor: pointer;
}
.superset-legacy-chart-rose .hover path {
fill-opacity: 1;
}
.nv-legend .nv-series {
cursor: pointer;
}
`}
`;
@@ -1,697 +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.
*/
// @ts-nocheck
/* oxlint-disable no-use-before-define: ["error", { "functions": false }] */
/* eslint-disable no-restricted-syntax */
/* eslint-disable react/sort-prop-types */
import d3 from 'd3';
import PropTypes from 'prop-types';
import nv from 'nvd3-fork';
import {
getTimeFormatter,
getNumberFormatter,
CategoricalColorNamespace,
sanitizeHtml,
} from '@superset-ui/core';
interface RoseDataEntry {
key: string[];
name: string;
time: number;
value: number;
id: number;
}
interface RoseData {
[timestamp: string]: RoseDataEntry[];
}
interface ArcDatum {
startAngle: number;
endAngle: number;
innerRadius: number;
outerRadius: number;
name?: string;
arcId?: number;
val?: number;
time?: number;
percent?: number;
}
interface RoseProps {
data: RoseData;
width: number;
height: number;
colorScheme: string;
dateTimeFormat: string;
numberFormat: string;
useRichTooltip: boolean;
useAreaProportions: boolean;
sliceId: number;
}
const propTypes = {
// Data is an object hashed by numeric value, perhaps timestamp
data: PropTypes.objectOf(
PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.arrayOf(PropTypes.string),
name: PropTypes.arrayOf(PropTypes.string),
time: PropTypes.number,
value: PropTypes.number,
}),
),
),
width: PropTypes.number,
height: PropTypes.number,
dateTimeFormat: PropTypes.string,
numberFormat: PropTypes.string,
useRichTooltip: PropTypes.bool,
useAreaProportions: PropTypes.bool,
colorScheme: PropTypes.string,
};
function copyArc(d: ArcDatum): ArcDatum {
return {
startAngle: d.startAngle,
endAngle: d.endAngle,
innerRadius: d.innerRadius,
outerRadius: d.outerRadius,
};
}
function sortValues(a: RoseDataEntry, b: RoseDataEntry): number {
if (a.value === b.value) {
return a.name > b.name ? 1 : -1;
}
return b.value - a.value;
}
function Rose(element: HTMLElement, props: RoseProps): void {
const {
data,
width,
height,
colorScheme,
dateTimeFormat,
numberFormat,
useRichTooltip,
useAreaProportions,
sliceId,
} = props;
const div = d3.select(element);
div.classed('superset-legacy-chart-rose', true);
const datum = data;
const times = Object.keys(datum)
.map(t => parseInt(t, 10))
.sort((a, b) => a - b);
const numGrains = times.length;
const numGroups = datum[times[0]].length;
const format = getNumberFormatter(numberFormat);
const timeFormat = getTimeFormatter(dateTimeFormat);
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
d3.select('.nvtooltip').remove();
div.selectAll('*').remove();
const arc = d3.svg.arc();
const legend = nv.models.legend();
const tooltip = nv.models.tooltip();
const state = { disabled: datum[times[0]].map(() => false) };
const svg = div.append('svg').attr('width', width).attr('height', height);
const g = svg.append('g').attr('class', 'rose').append('g');
const legendWrap = g.append('g').attr('class', 'legendWrap');
function legendData(adatum: RoseData) {
return adatum[times[0]].map((v: RoseDataEntry, i: number) => ({
disabled: state.disabled[i],
// Keep the raw name as `key` so it matches the value used for arc
// fills (colorFn is called with d.name on arcs and d.key on the
// legend). nvd3-fork's legend renders `key` via .text(), so the
// raw value is escaped at the DOM sink.
key: v.name,
}));
}
function tooltipData(d: ArcDatum, i: number, adatum: RoseData) {
const timeIndex = Math.floor(d.arcId / numGroups);
// nvd3-fork's nv.models.tooltip renders the `key` strings via .html(),
// so any HTML in user-controlled column values would execute. Pass the
// keys through sanitizeHtml to strip dangerous markup while preserving
// legitimate text content.
const series = useRichTooltip
? adatum[times[timeIndex]]
.filter(v => !state.disabled[v.id % numGroups])
.map(v => ({
key: sanitizeHtml(v.name),
value: v.value,
color: colorFn(v.name, sliceId),
highlight: v.id === d.arcId,
}))
: [
{
key: sanitizeHtml(d.name),
value: d.val,
color: colorFn(d.name, sliceId),
},
];
return {
key: 'Date',
value: d.time,
series,
};
}
legend.width(width).color(d => colorFn(d.key, sliceId));
legendWrap.datum(legendData(datum)).call(legend);
tooltip.headerFormatter(timeFormat).valueFormatter(format);
tooltip.classes('tooltip');
// Compute max radius, which the largest value will occupy
const roseHeight = height - legend.height();
const margin = { top: legend.height() };
const edgeMargin = 35; // space between outermost radius and slice edge
const maxRadius = Math.min(width, roseHeight) / 2 - edgeMargin;
const labelThreshold = 0.05;
const gro = 8; // mouseover radius growth in pixels
const mini = 0.075;
const centerTranslate = `translate(${width / 2},${
roseHeight / 2 + margin.top
})`;
const roseWrap = g
.append('g')
.attr('transform', centerTranslate)
.attr('class', 'roseWrap');
const labelsWrap = g
.append('g')
.attr('transform', centerTranslate)
.attr('class', 'labelsWrap');
const groupLabelsWrap = g
.append('g')
.attr('transform', centerTranslate)
.attr('class', 'groupLabelsWrap');
// Compute inner and outer angles for each data point
function computeArcStates(adatum: RoseData) {
// Find the max sum of values across all time
let maxSum = 0;
let grain = 0;
const sums = [];
for (const t of times) {
const sum = datum[t].reduce(
(a, v, i) => a + (state.disabled[i] ? 0 : v.value),
0,
);
maxSum = sum > maxSum ? sum : maxSum;
sums[grain] = sum;
grain += 1;
}
// Compute angle occupied by each time grain
const dtheta = (Math.PI * 2) / numGrains;
const angles = [];
for (let i = 0; i <= numGrains; i += 1) {
angles.push(dtheta * i - Math.PI / 2);
}
// Compute proportion
const P = maxRadius / maxSum;
const Q = P * maxRadius;
const computeOuterRadius = (value: number, innerRadius: number): number =>
useAreaProportions
? Math.sqrt(Q * value + innerRadius * innerRadius)
: P * value + innerRadius;
const arcSt = {
data: [],
extend: {},
push: {},
pieStart: {},
pie: {},
pieOver: {},
mini: {},
labels: [],
groupLabels: [],
};
let arcId = 0;
for (let i = 0; i < numGrains; i += 1) {
const t = times[i];
const startAngle = angles[i];
const endAngle = angles[i + 1];
const G = (2 * Math.PI) / sums[i];
let innerRadius = 0;
let outerRadius;
let pieStartAngle = 0;
let pieEndAngle;
for (const v of adatum[t]) {
const val = state.disabled[arcId % numGroups] ? 0 : v.value;
const { name, time } = v;
v.id = arcId;
outerRadius = computeOuterRadius(val, innerRadius);
arcSt.data.push({
startAngle,
endAngle,
innerRadius,
outerRadius,
name,
arcId,
val,
time,
});
arcSt.extend[arcId] = {
startAngle,
endAngle,
innerRadius,
name,
outerRadius: outerRadius + gro,
};
arcSt.push[arcId] = {
startAngle,
endAngle,
innerRadius: innerRadius + gro,
outerRadius: outerRadius + gro,
};
arcSt.pieStart[arcId] = {
startAngle,
endAngle,
innerRadius: mini * maxRadius,
outerRadius: maxRadius,
};
arcSt.mini[arcId] = {
startAngle,
endAngle,
innerRadius: innerRadius * mini,
outerRadius: outerRadius * mini,
};
arcId += 1;
innerRadius = outerRadius;
}
const labelArc = { ...arcSt.data[i * numGroups] };
labelArc.outerRadius = maxRadius + 20;
labelArc.innerRadius = maxRadius + 15;
arcSt.labels.push(labelArc);
for (const v of adatum[t].concat().sort(sortValues)) {
const val = state.disabled[v.id % numGroups] ? 0 : v.value;
pieEndAngle = G * val + pieStartAngle;
arcSt.pie[v.id] = {
startAngle: pieStartAngle,
endAngle: pieEndAngle,
innerRadius: maxRadius * mini,
outerRadius: maxRadius,
percent: v.value / sums[i],
};
arcSt.pieOver[v.id] = {
startAngle: pieStartAngle,
endAngle: pieEndAngle,
innerRadius: maxRadius * mini,
outerRadius: maxRadius + gro,
};
pieStartAngle = pieEndAngle;
}
}
arcSt.groupLabels = arcSt.data.slice(0, numGroups);
return arcSt;
}
let arcSt = computeArcStates(datum);
function tween(target: ArcDatum, resFunc: (d: ArcDatum) => string) {
return function doTween(d: ArcDatum) {
const interpolate = d3.interpolate(copyArc(d), copyArc(target));
return (t: number) => resFunc(Object.assign(d, interpolate(t)));
};
}
function arcTween(target: ArcDatum) {
return tween(target, (d: ArcDatum) => arc(d));
}
function translateTween(target: ArcDatum) {
return tween(target, (d: ArcDatum) => `translate(${arc.centroid(d)})`);
}
// Grab the ID range of segments stand between
// this segment and the edge of the circle
const segmentsToEdgeCache: Record<number, [number, number]> = {};
function getSegmentsToEdge(arcId: number): [number, number] {
if (segmentsToEdgeCache[arcId]) {
return segmentsToEdgeCache[arcId];
}
const timeIndex = Math.floor(arcId / numGroups);
segmentsToEdgeCache[arcId] = [arcId + 1, numGroups * (timeIndex + 1) - 1];
return segmentsToEdgeCache[arcId];
}
// Get the IDs of all segments in a timeIndex
const segmentsInTimeCache: Record<number, [number, number]> = {};
function getSegmentsInTime(arcId: number): [number, number] {
if (segmentsInTimeCache[arcId]) {
return segmentsInTimeCache[arcId];
}
const timeIndex = Math.floor(arcId / numGroups);
segmentsInTimeCache[arcId] = [
timeIndex * numGroups,
(timeIndex + 1) * numGroups - 1,
];
return segmentsInTimeCache[arcId];
}
let clickId = -1;
let inTransition = false;
const ae = roseWrap
.selectAll('g')
.data(JSON.parse(JSON.stringify(arcSt.data))) // deep copy data state
.enter()
.append('g')
.attr('class', 'segment')
.classed('clickable', true);
const labels = labelsWrap
.selectAll('g')
.data(JSON.parse(JSON.stringify(arcSt.labels)))
.enter()
.append('g')
.attr('class', 'roseLabel')
.attr('transform', d => `translate(${arc.centroid(d)})`);
labels
.append('text')
.style('text-anchor', 'middle')
.style('fill', '#000')
.text(d => timeFormat(d.time));
const groupLabels = groupLabelsWrap
.selectAll('g')
.data(JSON.parse(JSON.stringify(arcSt.groupLabels)))
.enter()
.append('g');
groupLabels
.style('opacity', 0)
.attr('class', 'roseGroupLabels')
.append('text')
.style('text-anchor', 'middle')
.style('fill', '#000')
.text(d => d.name);
const arcs = ae
.append('path')
.attr('class', 'arc')
.attr('fill', d => colorFn(d.name, sliceId))
.attr('d', arc);
function mousemove(): void {
tooltip();
}
function mouseover(this: Element, b: ArcDatum, i: number): void {
tooltip.data(tooltipData(b, i, datum)).hidden(false);
const $this = d3.select(this);
$this.classed('hover', true);
if (clickId < 0 && !inTransition) {
$this
.select('path')
.interrupt()
.transition()
.duration(180)
.attrTween('d', arcTween(arcSt.extend[i]));
const edge = getSegmentsToEdge(i);
arcs
.filter(d => edge[0] <= d.arcId && d.arcId <= edge[1])
.interrupt()
.transition()
.duration(180)
.attrTween('d', d => arcTween(arcSt.push[d.arcId])(d));
} else if (!inTransition) {
const segments = getSegmentsInTime(clickId);
if (segments[0] <= b.arcId && b.arcId <= segments[1]) {
$this
.select('path')
.interrupt()
.transition()
.duration(180)
.attrTween('d', arcTween(arcSt.pieOver[i]));
}
}
}
function mouseout(this: Element, b: ArcDatum, i: number): void {
tooltip.hidden(true);
const $this = d3.select(this);
$this.classed('hover', false);
if (clickId < 0 && !inTransition) {
$this
.select('path')
.interrupt()
.transition()
.duration(180)
.attrTween('d', arcTween(arcSt.data[i]));
const edge = getSegmentsToEdge(i);
arcs
.filter(d => edge[0] <= d.arcId && d.arcId <= edge[1])
.interrupt()
.transition()
.duration(180)
.attrTween('d', d => arcTween(arcSt.data[d.arcId])(d));
} else if (!inTransition) {
const segments = getSegmentsInTime(clickId);
if (segments[0] <= b.arcId && b.arcId <= segments[1]) {
$this
.select('path')
.interrupt()
.transition()
.duration(180)
.attrTween('d', arcTween(arcSt.pie[i]));
}
}
}
function click(b: ArcDatum, i: number): void {
if (inTransition) {
return;
}
const delay = d3.event.altKey ? 3750 : 375;
const segments = getSegmentsInTime(i);
if (clickId < 0) {
inTransition = true;
clickId = i;
labels
.interrupt()
.transition()
.duration(delay)
.attrTween('transform', d =>
translateTween({
outerRadius: 0,
innerRadius: 0,
startAngle: d.startAngle,
endAngle: d.endAngle,
})(d),
)
.style('opacity', 0);
groupLabels
.attr(
'transform',
`translate(${arc.centroid({
outerRadius: maxRadius + 20,
innerRadius: maxRadius + 15,
startAngle: arcSt.data[i].startAngle,
endAngle: arcSt.data[i].endAngle,
})})`,
)
.interrupt()
.transition()
.delay(delay)
.duration(delay)
.attrTween('transform', d =>
translateTween({
outerRadius: maxRadius + 20,
innerRadius: maxRadius + 15,
startAngle: arcSt.pie[segments[0] + d.arcId].startAngle,
endAngle: arcSt.pie[segments[0] + d.arcId].endAngle,
})(d),
)
.style('opacity', d =>
state.disabled[d.arcId] ||
arcSt.pie[segments[0] + d.arcId].percent < labelThreshold
? 0
: 1,
);
ae.classed(
'clickable',
d => segments[0] > d.arcId || d.arcId > segments[1],
);
arcs
.filter(d => segments[0] <= d.arcId && d.arcId <= segments[1])
.interrupt()
.transition()
.duration(delay)
.attrTween('d', d => arcTween(arcSt.pieStart[d.arcId])(d))
.transition()
.duration(delay)
.attrTween('d', d => arcTween(arcSt.pie[d.arcId])(d))
.each('end', () => {
inTransition = false;
});
arcs
.filter(d => segments[0] > d.arcId || d.arcId > segments[1])
.interrupt()
.transition()
.duration(delay)
.attrTween('d', d => arcTween(arcSt.mini[d.arcId])(d));
} else if (clickId < segments[0] || segments[1] < clickId) {
inTransition = true;
const clickSegments = getSegmentsInTime(clickId);
labels
.interrupt()
.transition()
.delay(delay)
.duration(delay)
.attrTween('transform', d =>
translateTween(arcSt.labels[d.arcId / numGroups])(d),
)
.style('opacity', 1);
groupLabels
.interrupt()
.transition()
.duration(delay)
.attrTween(
'transform',
translateTween({
outerRadius: maxRadius + 20,
innerRadius: maxRadius + 15,
startAngle: arcSt.data[clickId].startAngle,
endAngle: arcSt.data[clickId].endAngle,
}),
)
.style('opacity', 0);
ae.classed('clickable', true);
arcs
.filter(d => clickSegments[0] <= d.arcId && d.arcId <= clickSegments[1])
.interrupt()
.transition()
.duration(delay)
.attrTween('d', d => arcTween(arcSt.pieStart[d.arcId])(d))
.transition()
.duration(delay)
.attrTween('d', d => arcTween(arcSt.data[d.arcId])(d))
.each('end', () => {
clickId = -1;
inTransition = false;
});
arcs
.filter(d => clickSegments[0] > d.arcId || d.arcId > clickSegments[1])
.interrupt()
.transition()
.delay(delay)
.duration(delay)
.attrTween('d', d => arcTween(arcSt.data[d.arcId])(d));
}
}
ae.on('mouseover', mouseover)
.on('mouseout', mouseout)
.on('mousemove', mousemove)
.on('click', click);
function updateActive(): void {
const delay = d3.event.altKey ? 3000 : 300;
legendWrap.datum(legendData(datum)).call(legend);
const nArcSt = computeArcStates(datum);
inTransition = true;
if (clickId < 0) {
arcs
.style('opacity', 1)
.interrupt()
.transition()
.duration(delay)
.attrTween('d', d => arcTween(nArcSt.data[d.arcId])(d))
.each('end', () => {
inTransition = false;
arcSt = nArcSt;
})
.transition()
.duration(0)
.style('opacity', d => (state.disabled[d.arcId % numGroups] ? 0 : 1));
} else {
const segments = getSegmentsInTime(clickId);
arcs
.style('opacity', 1)
.interrupt()
.transition()
.duration(delay)
.attrTween('d', d =>
segments[0] <= d.arcId && d.arcId <= segments[1]
? arcTween(nArcSt.pie[d.arcId])(d)
: arcTween(nArcSt.mini[d.arcId])(d),
)
.each('end', () => {
inTransition = false;
arcSt = nArcSt;
})
.transition()
.duration(0)
.style('opacity', d => (state.disabled[d.arcId % numGroups] ? 0 : 1));
groupLabels
.interrupt()
.transition()
.duration(delay)
.attrTween('transform', d =>
translateTween({
outerRadius: maxRadius + 20,
innerRadius: maxRadius + 15,
startAngle: nArcSt.pie[segments[0] + d.arcId].startAngle,
endAngle: nArcSt.pie[segments[0] + d.arcId].endAngle,
})(d),
)
.style('opacity', d =>
state.disabled[d.arcId] ||
arcSt.pie[segments[0] + d.arcId].percent < labelThreshold
? 0
: 1,
);
}
}
legend.dispatch.on('stateChange', newState => {
if (state.disabled !== newState.disabled) {
state.disabled = newState.disabled;
updateActive();
}
});
}
Rose.displayName = 'Rose';
Rose.propTypes = propTypes;
export default Rose;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

@@ -1,95 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable no-magic-numbers, sort-keys */
import { SuperChart, VizType } from '@superset-ui/core';
import RoseChartPlugin from '@superset-ui/legacy-plugin-chart-rose';
import data from './data';
import { withResizableChartDemo } from '@storybook-shared';
new RoseChartPlugin().configure({ key: VizType.Rose }).register();
export default {
title: 'Legacy Chart Plugins/legacy-plugin-chart-rose',
decorators: [withResizableChartDemo],
args: {
colorScheme: 'd3Category10',
numberFormat: '.3s',
dateTimeFormat: '%Y-%m-%d',
richTooltip: true,
roseAreaProportion: false,
},
argTypes: {
colorScheme: {
control: 'select',
options: [
'supersetColors',
'd3Category10',
'bnbColors',
'googleCategory20c',
],
},
numberFormat: {
control: 'select',
options: ['SMART_NUMBER', '.2f', '.0%', '$,.2f', '.3s', ',d'],
},
dateTimeFormat: {
control: 'select',
options: ['%Y-%m-%d', '%Y-%m-%d %H:%M', '%b %d, %Y', '%d/%m/%Y'],
},
richTooltip: { control: 'boolean' },
roseAreaProportion: {
control: 'boolean',
description:
'When true, area is proportional to value; when false, radius is proportional',
},
},
};
export const Basic = ({
width,
height,
colorScheme,
numberFormat,
dateTimeFormat,
richTooltip,
roseAreaProportion,
}: {
width: number;
height: number;
colorScheme: string;
numberFormat: string;
dateTimeFormat: string;
richTooltip: boolean;
roseAreaProportion: boolean;
}) => (
<SuperChart
chartType={VizType.Rose}
width={width}
height={height}
queriesData={[{ data }]}
formData={{
color_scheme: colorScheme,
date_time_format: dateTimeFormat,
number_format: numberFormat,
rich_tooltip: richTooltip,
rose_area_proportion: roseAreaProportion,
}}
/>
);
@@ -1,950 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable sort-keys, no-magic-numbers */
export default {
'-157766400000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: -157766400000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: -157766400000.0,
},
{
key: ['David'],
value: 6820.0,
name: ['David'],
time: -157766400000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: -157766400000.0,
},
{
key: ['Michael'],
value: 7835.0,
name: ['Michael'],
time: -157766400000.0,
},
],
'-126230400000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: -126230400000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: -126230400000.0,
},
{
key: ['David'],
value: 6757.0,
name: ['David'],
time: -126230400000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: -126230400000.0,
},
{
key: ['Michael'],
value: 7678.0,
name: ['Michael'],
time: -126230400000.0,
},
],
'-94694400000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: -94694400000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: -94694400000.0,
},
{
key: ['David'],
value: 6923.0,
name: ['David'],
time: -94694400000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: -94694400000.0,
},
{
key: ['Michael'],
value: 8132.0,
name: ['Michael'],
time: -94694400000.0,
},
],
'-63158400000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: -63158400000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: -63158400000.0,
},
{
key: ['David'],
value: 6490.0,
name: ['David'],
time: -63158400000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: -63158400000.0,
},
{
key: ['Michael'],
value: 8079.0,
name: ['Michael'],
time: -63158400000.0,
},
],
'-31536000000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: -31536000000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: -31536000000.0,
},
{
key: ['David'],
value: 6563.0,
name: ['David'],
time: -31536000000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: -31536000000.0,
},
{
key: ['Michael'],
value: 8245.0,
name: ['Michael'],
time: -31536000000.0,
},
],
'0': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: 0.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 0.0,
},
{
key: ['David'],
value: 6367.0,
name: ['David'],
time: 0.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 0.0,
},
{
key: ['Michael'],
value: 8186.0,
name: ['Michael'],
time: 0.0,
},
],
'31536000000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: 31536000000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 31536000000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 31536000000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 31536000000.0,
},
{
key: ['Michael'],
value: 6825.0,
name: ['Michael'],
time: 31536000000.0,
},
],
'63072000000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: 63072000000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 63072000000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 63072000000.0,
},
{
key: ['Jennifer'],
value: 6066.0,
name: ['Jennifer'],
time: 63072000000.0,
},
{
key: ['Michael'],
value: 6337.0,
name: ['Michael'],
time: 63072000000.0,
},
],
'94694400000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: 94694400000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 94694400000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 94694400000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 94694400000.0,
},
{
key: ['Michael'],
value: 5877.0,
name: ['Michael'],
time: 94694400000.0,
},
],
'126230400000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: 126230400000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 126230400000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 126230400000.0,
},
{
key: ['Jennifer'],
value: 5672.0,
name: ['Jennifer'],
time: 126230400000.0,
},
{
key: ['Michael'],
value: 6058.0,
name: ['Michael'],
time: 126230400000.0,
},
],
'157766400000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: 157766400000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 157766400000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 157766400000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 157766400000.0,
},
{
key: ['Michael'],
value: 6206.0,
name: ['Michael'],
time: 157766400000.0,
},
],
'189302400000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: 189302400000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 189302400000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 189302400000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 189302400000.0,
},
{
key: ['Michael'],
value: 6178.0,
name: ['Michael'],
time: 189302400000.0,
},
],
'220924800000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: 220924800000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 220924800000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 220924800000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 220924800000.0,
},
{
key: ['Michael'],
value: 6313.0,
name: ['Michael'],
time: 220924800000.0,
},
],
'252460800000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: 252460800000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 252460800000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 252460800000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 252460800000.0,
},
{
key: ['Michael'],
value: 6489.0,
name: ['Michael'],
time: 252460800000.0,
},
],
'283996800000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: 283996800000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 283996800000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 283996800000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 283996800000.0,
},
{
key: ['Michael'],
value: 6580.0,
name: ['Michael'],
time: 283996800000.0,
},
],
'315532800000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: 315532800000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 315532800000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 315532800000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 315532800000.0,
},
{
key: ['Michael'],
value: 6896.0,
name: ['Michael'],
time: 315532800000.0,
},
],
'347155200000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: 347155200000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 347155200000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 347155200000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 347155200000.0,
},
{
key: ['Michael'],
value: 7142.0,
name: ['Michael'],
time: 347155200000.0,
},
],
'378691200000000000': [
{
key: ['Christopher'],
value: 6093.0,
name: ['Christopher'],
time: 378691200000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 378691200000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 378691200000.0,
},
{
key: ['Jennifer'],
value: 5811.0,
name: ['Jennifer'],
time: 378691200000.0,
},
{
key: ['Michael'],
value: 7155.0,
name: ['Michael'],
time: 378691200000.0,
},
],
'410227200000000000': [
{
key: ['Christopher'],
value: 6375.0,
name: ['Christopher'],
time: 410227200000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 410227200000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 410227200000.0,
},
{
key: ['Jennifer'],
value: 5829.0,
name: ['Jennifer'],
time: 410227200000.0,
},
{
key: ['Michael'],
value: 7386.0,
name: ['Michael'],
time: 410227200000.0,
},
],
'441763200000000000': [
{
key: ['Christopher'],
value: 6509.0,
name: ['Christopher'],
time: 441763200000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 441763200000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 441763200000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 441763200000.0,
},
{
key: ['Michael'],
value: 7478.0,
name: ['Michael'],
time: 441763200000.0,
},
],
'473385600000000000': [
{
key: ['Christopher'],
value: 6744.0,
name: ['Christopher'],
time: 473385600000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 473385600000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 473385600000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 473385600000.0,
},
{
key: ['Michael'],
value: 7210.0,
name: ['Michael'],
time: 473385600000.0,
},
],
'504921600000000000': [
{
key: ['Christopher'],
value: 6497.0,
name: ['Christopher'],
time: 504921600000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 504921600000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 504921600000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 504921600000.0,
},
{
key: ['Michael'],
value: 7259.0,
name: ['Michael'],
time: 504921600000.0,
},
],
'536457600000000000': [
{
key: ['Christopher'],
value: 6549.0,
name: ['Christopher'],
time: 536457600000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 536457600000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 536457600000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 536457600000.0,
},
{
key: ['Michael'],
value: 7448.0,
name: ['Michael'],
time: 536457600000.0,
},
],
'567993600000000000': [
{
key: ['Christopher'],
value: 6496.0,
name: ['Christopher'],
time: 567993600000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 567993600000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 567993600000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 567993600000.0,
},
{
key: ['Michael'],
value: 7748.0,
name: ['Michael'],
time: 567993600000.0,
},
],
'599616000000000000': [
{
key: ['Christopher'],
value: 6526.0,
name: ['Christopher'],
time: 599616000000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 599616000000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 599616000000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 599616000000.0,
},
{
key: ['Michael'],
value: 7851.0,
name: ['Michael'],
time: 599616000000.0,
},
],
'631152000000000000': [
{
key: ['Christopher'],
value: 6641.0,
name: ['Christopher'],
time: 631152000000.0,
},
{
key: ['Daniel'],
value: 5760.0,
name: ['Daniel'],
time: 631152000000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 631152000000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 631152000000.0,
},
{
key: ['Michael'],
value: 8233.0,
name: ['Michael'],
time: 631152000000.0,
},
],
'662688000000000000': [
{
key: ['Christopher'],
value: 5784.0,
name: ['Christopher'],
time: 662688000000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 662688000000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 662688000000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 662688000000.0,
},
{
key: ['Michael'],
value: 7579.0,
name: ['Michael'],
time: 662688000000.0,
},
],
'694224000000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: 694224000000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 694224000000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 694224000000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 694224000000.0,
},
{
key: ['Michael'],
value: 6627.0,
name: ['Michael'],
time: 694224000000.0,
},
],
'725846400000000000': [
{
key: ['Christopher'],
value: 0,
name: ['Christopher'],
time: 725846400000.0,
},
{
key: ['Daniel'],
value: 0,
name: ['Daniel'],
time: 725846400000.0,
},
{
key: ['David'],
value: 0,
name: ['David'],
time: 725846400000.0,
},
{
key: ['Jennifer'],
value: 0,
name: ['Jennifer'],
time: 725846400000.0,
},
{
key: ['Michael'],
value: 5839.0,
name: ['Michael'],
time: 725846400000.0,
},
],
};
@@ -1,25 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
// Path Resolution: Override baseUrl to maintain correct path mappings from parent config
// (e.g., "@apache-superset/core" -> "./packages/superset-core/src")
"baseUrl": "../..",
// Directory Overrides: Parent config paths are relative to frontend root,
// but packages need paths relative to their own directory
"outDir": "lib",
"rootDir": "src",
"declarationDir": "lib"
},
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]
}
@@ -1,113 +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.
-->
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.20.0](https://github.com/apache/superset/compare/v2021.41.0...v0.20.0) (2024-09-09)
### Bug Fixes
- adding missing examples for bubble chart, bullet chart, calendar heatmap chart and country map chart in the gallery ([#22523](https://github.com/apache/superset/issues/22523)) ([839ec7c](https://github.com/apache/superset/commit/839ec7ceacc66c65928fd0ddead2b014db3d5563))
- Adds the Deprecated label to Time-series Percent Change chart ([#30148](https://github.com/apache/superset/issues/30148)) ([5e42d7a](https://github.com/apache/superset/commit/5e42d7aed0d11c7aac91ab19088d2632e49da614))
- **area chart legacy:** tool tip shows actual value rather than y axi… ([#23469](https://github.com/apache/superset/issues/23469)) ([db9ca20](https://github.com/apache/superset/commit/db9ca20737fecda8eee342b34d62d3b700ef3687))
- **Dashboard:** Color inconsistency on refreshes and conflicts ([#27439](https://github.com/apache/superset/issues/27439)) ([313ee59](https://github.com/apache/superset/commit/313ee596f5435894f857d72be7269d5070c8c964))
- **explore:** Fix chart standalone URL for report/thumbnail generation ([#20673](https://github.com/apache/superset/issues/20673)) ([84d4302](https://github.com/apache/superset/commit/84d4302628d18aa19c13cc5322e68abbc690ea4d))
- **explore:** make SORT-Descending visible if Sort-by has value ([#17726](https://github.com/apache/superset/issues/17726)) ([d5768ab](https://github.com/apache/superset/commit/d5768ab649a70fd4f541ad4982498f622160b220))
- **explore:** Prevent shared controls from checking feature flags outside React render ([#21315](https://github.com/apache/superset/issues/21315)) ([2285ebe](https://github.com/apache/superset/commit/2285ebe72ec4edded6d195052740b7f9f13d1f1b))
- **legacy-chart:** corrupted raw chart data ([#24850](https://github.com/apache/superset/issues/24850)) ([1c5971d](https://github.com/apache/superset/commit/1c5971d3afb70a338444c41943ff90c3a9c03ec3))
- Rename legacy line and area charts ([#28113](https://github.com/apache/superset/issues/28113)) ([b4c4ab7](https://github.com/apache/superset/commit/b4c4ab7790cbeb8d65ec7c1084482c21932e755b))
- Reset sorting bar issue in Barchart ([#19371](https://github.com/apache/superset/issues/19371)) ([94e06c2](https://github.com/apache/superset/commit/94e06c2b6a1f782133bb9ef85a1d46ce7eacf9ba))
- **storybook:** fix broken Storybook stories during development ([#29587](https://github.com/apache/superset/issues/29587)) ([462cda4](https://github.com/apache/superset/commit/462cda400baa00b3bcc4a7f8aded362ca55e18a5))
- Tooltip of area chart shows undefined total ([#24916](https://github.com/apache/superset/issues/24916)) ([ec9e9a4](https://github.com/apache/superset/commit/ec9e9a46f2f092ce56d3ed5a8a9a3ea0214db88a))
- warning of nth-child ([#23638](https://github.com/apache/superset/issues/23638)) ([16cc089](https://github.com/apache/superset/commit/16cc089b198dcdebc2422845aa08d18233c6b3a4))
- Zero values on Dual Line axis bounds ([#23649](https://github.com/apache/superset/issues/23649)) ([d66e6e6](https://github.com/apache/superset/commit/d66e6e6d400db0fee35d73cd43e610cd1c491f4b))
### Features
- Adds the ECharts Bubble chart ([#22107](https://github.com/apache/superset/issues/22107)) ([c81c60c](https://github.com/apache/superset/commit/c81c60c91fbcb09dd63c05f050e18ee09ceebfd6))
- apply standardized form data to tier 2 charts ([#20530](https://github.com/apache/superset/issues/20530)) ([de524bc](https://github.com/apache/superset/commit/de524bc59f011fd361dcdb7d35c2cb51f7eba442))
- **chart & legend:** make to enable show legend by default ([#19927](https://github.com/apache/superset/issues/19927)) ([7b3d0f0](https://github.com/apache/superset/commit/7b3d0f040b050905f7d0901d0227f1cd6b761b56))
- **explore:** Apply denormalization to tier 2 charts form data ([#20524](https://github.com/apache/superset/issues/20524)) ([e12ee59](https://github.com/apache/superset/commit/e12ee59b13822241dca8d8015f1222c477edd4f3))
- **explore:** Denormalize form data in echarts, world map and nvd3 bar and line charts ([#20313](https://github.com/apache/superset/issues/20313)) ([354a899](https://github.com/apache/superset/commit/354a89950c4d001da3e107f60788cea873bd6bf6))
- **explore:** improve UI in the control panel ([#19748](https://github.com/apache/superset/issues/19748)) ([e3a54aa](https://github.com/apache/superset/commit/e3a54aa3c15bdd0c970aa73f898288a408205c97))
- **explore:** standardized controls for time pivot chart ([#21321](https://github.com/apache/superset/issues/21321)) ([79525df](https://github.com/apache/superset/commit/79525dfaf29b810af668e3b6c5a56cd866370d92))
- **formatters:** Add custom d3-time-format locale ([#24263](https://github.com/apache/superset/issues/24263)) ([024cfd8](https://github.com/apache/superset/commit/024cfd86e408ec5f7ddf49a9e90908e2fb2e6b70))
- improve color consistency (save all labels) ([#19038](https://github.com/apache/superset/issues/19038)) ([dc57508](https://github.com/apache/superset/commit/dc575080d7e43d40b1734bb8f44fdc291cb95b11))
- **legacy-preset-chart-nvd3:** add richtooltip in nvd3 bar chart ([#17615](https://github.com/apache/superset/issues/17615)) ([72f3215](https://github.com/apache/superset/commit/72f3215ffc74ead33dba57196aeaf4e1db63fd6c))
- Migrates Dual Line Chart to Mixed Chart ([#23910](https://github.com/apache/superset/issues/23910)) ([f5148ef](https://github.com/apache/superset/commit/f5148ef728ce649697c10fb7aa65982d7dd05638))
- Removes the Multiple Line Charts ([#23933](https://github.com/apache/superset/issues/23933)) ([6ce8592](https://github.com/apache/superset/commit/6ce85921fc103ba0e93b437d473003e6f1b4a42b))
- update time comparison choices (again) ([#17968](https://github.com/apache/superset/issues/17968)) ([05d9cde](https://github.com/apache/superset/commit/05d9cde203b99f8c63106446f0be58668cc9f0c9))
- update time comparison choices (again) ([#22458](https://github.com/apache/superset/issues/22458)) ([9e81c3a](https://github.com/apache/superset/commit/9e81c3a1192a18226d505178d16e1e395917a719))
- **viz picker:** Remove some tags, refactor Recommended section ([#27708](https://github.com/apache/superset/issues/27708)) ([c314999](https://github.com/apache/superset/commit/c3149994ac0d4392e0462421b62cd0c034142082))
# [0.19.0](https://github.com/apache/superset/compare/v2021.41.0...v0.19.0) (2024-09-07)
### Bug Fixes
- adding missing examples for bubble chart, bullet chart, calendar heatmap chart and country map chart in the gallery ([#22523](https://github.com/apache/superset/issues/22523)) ([839ec7c](https://github.com/apache/superset/commit/839ec7ceacc66c65928fd0ddead2b014db3d5563))
- Adds the Deprecated label to Time-series Percent Change chart ([#30148](https://github.com/apache/superset/issues/30148)) ([5e42d7a](https://github.com/apache/superset/commit/5e42d7aed0d11c7aac91ab19088d2632e49da614))
- **area chart legacy:** tool tip shows actual value rather than y axi… ([#23469](https://github.com/apache/superset/issues/23469)) ([db9ca20](https://github.com/apache/superset/commit/db9ca20737fecda8eee342b34d62d3b700ef3687))
- **Dashboard:** Color inconsistency on refreshes and conflicts ([#27439](https://github.com/apache/superset/issues/27439)) ([313ee59](https://github.com/apache/superset/commit/313ee596f5435894f857d72be7269d5070c8c964))
- **explore:** Fix chart standalone URL for report/thumbnail generation ([#20673](https://github.com/apache/superset/issues/20673)) ([84d4302](https://github.com/apache/superset/commit/84d4302628d18aa19c13cc5322e68abbc690ea4d))
- **explore:** make SORT-Descending visible if Sort-by has value ([#17726](https://github.com/apache/superset/issues/17726)) ([d5768ab](https://github.com/apache/superset/commit/d5768ab649a70fd4f541ad4982498f622160b220))
- **explore:** Prevent shared controls from checking feature flags outside React render ([#21315](https://github.com/apache/superset/issues/21315)) ([2285ebe](https://github.com/apache/superset/commit/2285ebe72ec4edded6d195052740b7f9f13d1f1b))
- **legacy-chart:** corrupted raw chart data ([#24850](https://github.com/apache/superset/issues/24850)) ([1c5971d](https://github.com/apache/superset/commit/1c5971d3afb70a338444c41943ff90c3a9c03ec3))
- Rename legacy line and area charts ([#28113](https://github.com/apache/superset/issues/28113)) ([b4c4ab7](https://github.com/apache/superset/commit/b4c4ab7790cbeb8d65ec7c1084482c21932e755b))
- Reset sorting bar issue in Barchart ([#19371](https://github.com/apache/superset/issues/19371)) ([94e06c2](https://github.com/apache/superset/commit/94e06c2b6a1f782133bb9ef85a1d46ce7eacf9ba))
- **storybook:** fix broken Storybook stories during development ([#29587](https://github.com/apache/superset/issues/29587)) ([462cda4](https://github.com/apache/superset/commit/462cda400baa00b3bcc4a7f8aded362ca55e18a5))
- Tooltip of area chart shows undefined total ([#24916](https://github.com/apache/superset/issues/24916)) ([ec9e9a4](https://github.com/apache/superset/commit/ec9e9a46f2f092ce56d3ed5a8a9a3ea0214db88a))
- warning of nth-child ([#23638](https://github.com/apache/superset/issues/23638)) ([16cc089](https://github.com/apache/superset/commit/16cc089b198dcdebc2422845aa08d18233c6b3a4))
- Zero values on Dual Line axis bounds ([#23649](https://github.com/apache/superset/issues/23649)) ([d66e6e6](https://github.com/apache/superset/commit/d66e6e6d400db0fee35d73cd43e610cd1c491f4b))
### Features
- Adds the ECharts Bubble chart ([#22107](https://github.com/apache/superset/issues/22107)) ([c81c60c](https://github.com/apache/superset/commit/c81c60c91fbcb09dd63c05f050e18ee09ceebfd6))
- apply standardized form data to tier 2 charts ([#20530](https://github.com/apache/superset/issues/20530)) ([de524bc](https://github.com/apache/superset/commit/de524bc59f011fd361dcdb7d35c2cb51f7eba442))
- **chart & legend:** make to enable show legend by default ([#19927](https://github.com/apache/superset/issues/19927)) ([7b3d0f0](https://github.com/apache/superset/commit/7b3d0f040b050905f7d0901d0227f1cd6b761b56))
- **explore:** Apply denormalization to tier 2 charts form data ([#20524](https://github.com/apache/superset/issues/20524)) ([e12ee59](https://github.com/apache/superset/commit/e12ee59b13822241dca8d8015f1222c477edd4f3))
- **explore:** Denormalize form data in echarts, world map and nvd3 bar and line charts ([#20313](https://github.com/apache/superset/issues/20313)) ([354a899](https://github.com/apache/superset/commit/354a89950c4d001da3e107f60788cea873bd6bf6))
- **explore:** improve UI in the control panel ([#19748](https://github.com/apache/superset/issues/19748)) ([e3a54aa](https://github.com/apache/superset/commit/e3a54aa3c15bdd0c970aa73f898288a408205c97))
- **explore:** standardized controls for time pivot chart ([#21321](https://github.com/apache/superset/issues/21321)) ([79525df](https://github.com/apache/superset/commit/79525dfaf29b810af668e3b6c5a56cd866370d92))
- **formatters:** Add custom d3-time-format locale ([#24263](https://github.com/apache/superset/issues/24263)) ([024cfd8](https://github.com/apache/superset/commit/024cfd86e408ec5f7ddf49a9e90908e2fb2e6b70))
- improve color consistency (save all labels) ([#19038](https://github.com/apache/superset/issues/19038)) ([dc57508](https://github.com/apache/superset/commit/dc575080d7e43d40b1734bb8f44fdc291cb95b11))
- **legacy-preset-chart-nvd3:** add richtooltip in nvd3 bar chart ([#17615](https://github.com/apache/superset/issues/17615)) ([72f3215](https://github.com/apache/superset/commit/72f3215ffc74ead33dba57196aeaf4e1db63fd6c))
- Migrates Dual Line Chart to Mixed Chart ([#23910](https://github.com/apache/superset/issues/23910)) ([f5148ef](https://github.com/apache/superset/commit/f5148ef728ce649697c10fb7aa65982d7dd05638))
- Removes the Multiple Line Charts ([#23933](https://github.com/apache/superset/issues/23933)) ([6ce8592](https://github.com/apache/superset/commit/6ce85921fc103ba0e93b437d473003e6f1b4a42b))
- update time comparison choices (again) ([#17968](https://github.com/apache/superset/issues/17968)) ([05d9cde](https://github.com/apache/superset/commit/05d9cde203b99f8c63106446f0be58668cc9f0c9))
- update time comparison choices (again) ([#22458](https://github.com/apache/superset/issues/22458)) ([9e81c3a](https://github.com/apache/superset/commit/9e81c3a1192a18226d505178d16e1e395917a719))
- **viz picker:** Remove some tags, refactor Recommended section ([#27708](https://github.com/apache/superset/issues/27708)) ([c314999](https://github.com/apache/superset/commit/c3149994ac0d4392e0462421b62cd0c034142082))
# [0.18.0](https://github.com/apache-superset/superset-ui/compare/v0.17.87...v0.18.0) (2021-08-30)
**Note:** Version bump only for package @superset-ui/legacy-preset-chart-nvd3
## [0.17.63](https://github.com/apache-superset/superset-ui/compare/v0.17.62...v0.17.63) (2021-07-02)
**Note:** Version bump only for package @superset-ui/legacy-preset-chart-nvd3
## [0.17.62](https://github.com/apache-superset/superset-ui/compare/v0.17.61...v0.17.62) (2021-07-02)
**Note:** Version bump only for package @superset-ui/legacy-preset-chart-nvd3
## [0.17.61](https://github.com/apache-superset/superset-ui/compare/v0.17.60...v0.17.61) (2021-07-02)
**Note:** Version bump only for package @superset-ui/legacy-preset-chart-nvd3
@@ -1,64 +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.
-->
## @superset-ui/legacy-preset-chart-nvd3
[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-preset-chart-nvd3.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/legacy-preset-chart-nvd3)
[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Flegacy-preset-chart-nvd3?style=flat)](https://libraries.io/npm/@superset-ui%2Flegacy-preset-chart-nvd3)
This plugin provides Big Number for Superset.
### Usage
Import the preset and register. This will register all the chart plugins under nvd3.
```js
import { NVD3ChartPreset } from '@superset-ui/legacy-preset-chart-nvd3';
new NVD3ChartPreset().register();
```
or register charts one by one. Configure `key`, which can be any `string`, and register the plugin.
This `key` will be used to lookup this chart throughout the app.
```js
import {
AreaChartPlugin,
LineChartPlugin,
} from '@superset-ui/legacy-preset-chart-nvd3';
new AreaChartPlugin().configure({ key: 'area' }).register();
new LineChartPlugin().configure({ key: 'line' }).register();
```
Then use it via `SuperChart`. See
[storybook](https://apache-superset.github.io/superset-ui-plugins/?selectedKind=plugin-chart-nvd3)
for more details.
```js
<SuperChart
chartType="line"
width={600}
height={600}
formData={...}
queriesData={[{
data: {...},
}]}
/>
```
@@ -1,49 +0,0 @@
{
"name": "@superset-ui/legacy-preset-chart-nvd3",
"version": "0.20.3",
"description": "Superset Legacy Chart - NVD3",
"keywords": [
"superset"
],
"homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-preset-chart-nvd3#readme",
"bugs": {
"url": "https://github.com/apache/superset/issues"
},
"license": "Apache-2.0",
"author": "Superset",
"repository": {
"type": "git",
"url": "https://github.com/apache/superset.git",
"directory": "superset-frontend/packages/legacy-preset-chart-nvd3"
},
"files": [
"esm",
"lib"
],
"sideEffects": [
"*.css"
],
"main": "lib/index.js",
"module": "esm/index.js",
"publishConfig": {
"access": "public"
},
"dependencies": {
"d3": "^3.5.17",
"d3-tip": "^0.9.1",
"dompurify": "^3.4.12",
"fast-safe-stringify": "^2.1.1",
"lodash": "^4.18.1",
"lodash-es": "^4.18.1",
"nvd3-fork": "^2.0.5",
"prop-types": "^15.8.1",
"urijs": "^1.19.11"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"dayjs": "^1.11.21",
"react": "^18.3.0"
}
}
@@ -1,81 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SuperChart, VizType } from '@superset-ui/core';
import { EchartsBoxPlotChartPlugin } from '@superset-ui/plugin-chart-echarts';
import { dummyDatasource, withResizableChartDemo } from '@storybook-shared';
import data from './data';
new EchartsBoxPlotChartPlugin().configure({ key: 'box-plot' }).register();
export default {
title: 'Legacy Chart Plugins/legacy-preset-chart-nvd3/BoxPlot',
decorators: [withResizableChartDemo],
args: {
colorScheme: 'd3Category10',
whiskerOptions: 'Min/max (no outliers)',
},
argTypes: {
colorScheme: {
control: 'select',
options: [
'supersetColors',
'd3Category10',
'bnbColors',
'googleCategory20c',
],
},
whiskerOptions: {
control: 'select',
options: [
'Tukey',
'Min/max (no outliers)',
'2/98 percentiles',
'9/91 percentiles',
],
},
},
};
export const Basic = ({
colorScheme,
whiskerOptions,
width,
height,
}: {
colorScheme: string;
whiskerOptions: string;
width: number;
height: number;
}) => (
<SuperChart
chartType="box-plot"
width={width}
height={height}
datasource={dummyDatasource}
queriesData={[{ data }]}
formData={{
color_scheme: colorScheme,
viz_type: VizType.BoxPlot,
whisker_options: whiskerOptions,
groupby: ['region'],
metrics: ['sum__SP_POP_TOTL'],
}}
/>
);
@@ -1,78 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable sort-keys, no-magic-numbers */
// Data format for ECharts BoxPlot - requires metric__min, metric__q1, metric__median, etc.
export default [
{
region: 'East Asia & Pacific',
sum__SP_POP_TOTL__min: 1031863394.0,
sum__SP_POP_TOTL__q1: 1384725172.5,
sum__SP_POP_TOTL__median: 1717904169.0,
sum__SP_POP_TOTL__q3: 2032724922.5,
sum__SP_POP_TOTL__max: 2240687901.0,
sum__SP_POP_TOTL__mean: 1681000000.0,
sum__SP_POP_TOTL__count: 50,
sum__SP_POP_TOTL__outliers: [],
},
{
region: 'Europe & Central Asia',
sum__SP_POP_TOTL__min: 660881033.0,
sum__SP_POP_TOTL__q1: 751386460.5,
sum__SP_POP_TOTL__median: 820716895.0,
sum__SP_POP_TOTL__q3: 862814192.5,
sum__SP_POP_TOTL__max: 903095786.0,
sum__SP_POP_TOTL__mean: 799778873.0,
sum__SP_POP_TOTL__count: 50,
sum__SP_POP_TOTL__outliers: [],
},
{
region: 'Latin America & Caribbean',
sum__SP_POP_TOTL__min: 220564224.0,
sum__SP_POP_TOTL__q1: 313690832.5,
sum__SP_POP_TOTL__median: 421490233.0,
sum__SP_POP_TOTL__q3: 529668114.5,
sum__SP_POP_TOTL__max: 626270167.0,
sum__SP_POP_TOTL__mean: 422336714.0,
sum__SP_POP_TOTL__count: 50,
sum__SP_POP_TOTL__outliers: [],
},
{
region: 'South Asia',
sum__SP_POP_TOTL__min: 572036107.0,
sum__SP_POP_TOTL__q1: 772373036.5,
sum__SP_POP_TOTL__median: 1059570231.0,
sum__SP_POP_TOTL__q3: 1398841234.0,
sum__SP_POP_TOTL__max: 1720976995.0,
sum__SP_POP_TOTL__mean: 1104759521.0,
sum__SP_POP_TOTL__count: 50,
sum__SP_POP_TOTL__outliers: [],
},
{
region: 'Sub-Saharan Africa',
sum__SP_POP_TOTL__min: 228268752.0,
sum__SP_POP_TOTL__q1: 320037758.0,
sum__SP_POP_TOTL__median: 467337821.0,
sum__SP_POP_TOTL__q3: 676768689.0,
sum__SP_POP_TOTL__max: 974315323.0,
sum__SP_POP_TOTL__mean: 533345669.0,
sum__SP_POP_TOTL__count: 50,
sum__SP_POP_TOTL__outliers: [1100000000, 1200000000],
},
];
@@ -1,140 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { t } from '@apache-superset/core/translation';
import {
ControlPanelConfig,
formatSelectOptions,
D3_FORMAT_OPTIONS,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import {
showLegend,
xAxisLabel,
yAxisLabel,
bottomMargin,
xTicksLayout,
xAxisFormat,
yLogScale,
xAxisShowMinmax,
yAxisShowMinmax,
leftMargin,
yAxisBounds,
} from '../NVD3Controls';
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
['series'],
['entity'],
['x'],
['y'],
['adhoc_filters'],
['size'],
[
{
name: 'max_bubble_size',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Max Bubble Size'),
default: '25',
choices: formatSelectOptions([
'5',
'10',
'15',
'25',
'50',
'75',
'100',
]),
},
},
],
['limit', null],
],
},
{
label: t('Chart Options'),
expanded: true,
tabOverride: 'customize',
controlSetRows: [['color_scheme'], [showLegend, null]],
},
{
label: t('X Axis'),
expanded: true,
tabOverride: 'customize',
controlSetRows: [
[xAxisLabel, leftMargin],
[
{
name: xAxisFormat.name,
config: {
...xAxisFormat.config,
default: 'SMART_NUMBER',
choices: D3_FORMAT_OPTIONS,
},
},
xTicksLayout,
],
[
{
name: 'x_log_scale',
config: {
type: 'CheckboxControl',
label: t('X Log Scale'),
default: false,
renderTrigger: true,
description: t('Use a log scale for the X-axis'),
},
},
xAxisShowMinmax,
],
],
},
{
label: t('Y Axis'),
expanded: true,
tabOverride: 'customize',
controlSetRows: [
[yAxisLabel, bottomMargin],
['y_axis_format', null],
[yLogScale, yAxisShowMinmax],
[yAxisBounds],
],
},
],
controlOverrides: {
color_scheme: {
renderTrigger: false,
},
},
formDataOverrides: formData => ({
...formData,
series: getStandardizedControls().shiftColumn(),
entity: getStandardizedControls().shiftColumn(),
x: getStandardizedControls().shiftMetric(),
y: getStandardizedControls().shiftMetric(),
size: getStandardizedControls().shiftMetric(),
}),
};
export default config;

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