Compare commits

...
Author SHA1 Message Date
Bouke van der Bijl dcc556a9a7 chore(CHANGELOG): add link to 6.0.0 CHANGELOG (#36832) 2026-01-05 12:59:42 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 61986100bd chore(deps): bump hot-shots from 12.0.0 to 12.1.0 in /superset-websocket (#36899)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-05 09:22:09 -08:00
Mehmet Salih YavuzandClaude c76ddcbbec fix(deck.gl): Fix Scatterplot chart error when using fixed point size (#36890)
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-05 15:55:43 +03:00
Enzo Martellucci 740ddc03e2 fix(plugin-chart-table): remove column misalignment when no scrollbars are present (#36891) 2026-01-05 13:52:37 +01:00
Đỗ Trọng Hải 2080633e57 other: resolve frontend dep vulns (#36820)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-01-02 23:28:44 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ac27c0aa3c chore(deps-dev): bump @typescript-eslint/parser from 8.50.1 to 8.51.0 in /superset-websocket (#36861)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-02 23:21:59 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 53fa65fe67 chore(deps): bump caniuse-lite from 1.0.30001761 to 1.0.30001762 in /docs (#36865)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-02 23:19:04 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> fdef8fa50a chore(deps-dev): bump @typescript-eslint/parser from 8.50.1 to 8.51.0 in /docs (#36866)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-02 23:18:50 -08:00
İbrahim ErcanandIbrahim Ercan 1334040fd6 chore(docs): Include VLMedia logo to intothewild (#36868)
Co-authored-by: Ibrahim Ercan <ibrahim.ercan@vlmedia.com.tr>
2026-01-02 23:18:31 -08:00
nmdo 52af489d8f docs: Add Rogow to INTHEWILD.yaml (#36879) 2026-01-02 23:14:43 -08:00
innovark d07a452e9b fix(RightMenu): fix inconsistent icon alignment in RightMenu items (#36532) 2026-01-02 23:13:08 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> aed95453b3 chore(deps): bump qs from 6.14.0 to 6.14.1 in /superset-websocket/utils/client-ws-app (#36883)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-02 23:10:25 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 4451e8db05 chore(deps): bump qs from 6.14.0 to 6.14.1 in /docs (#36884)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-02 23:10:08 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dd2eb6293d chore(deps-dev): bump globals from 16.5.0 to 17.0.0 in /superset-websocket (#36885)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-02 23:09:39 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1b1be96274 chore(deps-dev): bump globals from 16.5.0 to 17.0.0 in /docs (#36886)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-02 23:09:22 -08:00
innovark e5489bd30f fix(SavedQueries): unify query card actions styling across all home page cards (#36671) 2026-01-02 14:41:47 +01:00
Levis Mbote 12aa425049 fix(logout): clicking logout displays an error notification "invalid username or password" (#36490) 2026-01-02 12:59:27 +01:00
Luis Sánchez c31224c891 fix(TableChart): render cell bars for columns with NULL values (#36819) 2025-12-31 17:51:35 +03:00
Mehmet Salih Yavuz 85e830de46 fix: Clear database form errors (#36854) 2025-12-31 15:25:23 +01:00
Geidō d4ba44fce2 fix: Query history view button in SqlLab (#36540) 2025-12-31 15:18:59 +01:00
Enzo MartellucciandDiego Pucci 7cd76e4647 fix: SqlLab error when collapsing the left panel preview (#36858)
Co-authored-by: Diego Pucci <diegopucci.me@gmail.com>
2025-12-31 14:11:59 +01:00
Enzo Martellucci e112d863bf fix(dashboard): resolve tab reorder state sync issues (#36855) 2025-12-31 13:50:58 +01:00
Enzo Martellucci fe5d5fdae6 fix(chart-creation): use exact match when loading dataset from URL parameter (#36831) 2025-12-31 13:50:03 +01:00
cr0fters 02411ffde0 docs: Add EnquiryLabs to INTHEWILD.yaml (#36843) 2025-12-27 20:44:36 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1697cf733b chore(deps): bump antd from 6.1.1 to 6.1.2 in /docs (#36823)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-27 20:42:16 -08:00
Mehmet Salih Yavuz 28c802fb6c fix(TableCollection): only apply highlight class when defined (#36809) 2025-12-24 16:26:01 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 362b5e3b89 chore(deps-dev): bump js-yaml from 3.14.1 to 3.14.2 in /superset-frontend (#36711)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-23 15:20:58 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> bf5070471d chore(deps): bump storybook from 8.6.14 to 8.6.15 in /docs (#36727)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-23 15:20:39 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 100789200a chore(deps): bump hot-shots from 11.4.0 to 12.0.0 in /superset-websocket (#36786)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-23 15:20:22 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f95f125c4c chore(deps-dev): bump html-webpack-plugin from 5.6.4 to 5.6.5 in /superset-frontend (#36787)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-23 15:20:05 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> fd67d3190a chore(deps-dev): bump typescript-eslint from 8.50.0 to 8.50.1 in /docs (#36801)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-23 15:19:30 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> bd8d4ddbee chore(deps-dev): bump typescript-eslint from 8.50.0 to 8.50.1 in /superset-websocket (#36802)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-23 15:19:13 -08:00
Beto DealmeidaandClaude Opus 4.5 ecb4e483df fix: apply EXCLUDE_USERS_FROM_LISTS to /api/v1/security/users/ (#36742)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 15:18:34 -08:00
Sebastian JingoiHriday AlghHriday Alghcodeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com>
f8cb935105 feat(reports): add webhook option for notifications (#36127)
Co-authored-by: Hriday Algh <hridayalgh@gmail.com>
Co-authored-by: Hriday Algh <53922405+AlwaysIngame@users.noreply.github.com>
Co-authored-by: codeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com>
2025-12-23 10:51:04 -08:00
Michael S. Molina ba8d6eb9ac docs: Adds a new "Dependencies" page to the Developer Portal (#36817) 2025-12-23 10:41:10 -08:00
Michael S. Molina c399fd2801 fix: Stale editor content in @apache-superset/core (#36813) 2025-12-23 10:40:26 -08:00
Michael S. Molina 9e04c3471d docs: Add SQL Lab Result Stats to community extensions registry (#36796) 2025-12-23 10:39:47 -08:00
Michael S. Molina 8f8fe19e3e docs: Add SQL Snippets to community extensions registry (#36797) 2025-12-23 14:52:09 -03:00
Michael S. Molina ff3dab9b3b docs: Add Query Estimator to community extensions registry (#36814) 2025-12-23 14:25:45 -03:00
Levis Mbote ff24e2f27d fix: fix error with dashboard filters when global async queries is enabled and user navigates quickly (#36639) 2025-12-23 15:53:01 +01:00
Alexandru Soare 54eb6317ef fix(margin): Fixing margin issues (#36479) 2025-12-22 15:57:43 +01:00
Enzo MartellucciandDiego Pucci 32c98d02d3 fix: UI cut off (#36531)
Co-authored-by: Diego Pucci <diegopucci.me@gmail.com>
2025-12-22 15:52:04 +01:00
Michael S. Molinaandcodeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com> 6b25d0663e refactor: Migrates the MCP execute_sql tool to use the SQL execution API (#36739)
Co-authored-by: codeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com>
2025-12-22 09:48:28 -03:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> c0bcf28947 chore(deps-dev): bump @playwright/test from 1.56.0 to 1.57.0 in /superset-frontend (#36571)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-20 20:56:15 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> e0ea807031 chore(deps-dev): bump baseline-browser-mapping from 2.9.8 to 2.9.9 in /superset-frontend (#36725)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-20 20:55:59 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8d070f5cb6 chore(deps-dev): bump @types/node from 25.0.2 to 25.0.3 in /superset-frontend (#36726)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-20 20:55:41 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 5cd8e1e736 chore(deps): bump caniuse-lite from 1.0.30001760 to 1.0.30001761 in /docs (#36757)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-20 20:55:21 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0ced20457b chore(deps-dev): bump webpack from 5.104.0 to 5.104.1 in /docs (#36758)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-20 20:55:01 -08:00
Amin Ghadersohi e3e6b0e18b fix(mcp): use SQLScript for all SQL parsing in execute_sql (#36599) 2025-12-20 20:52:56 -08:00
Joe LiandClaude Opus 4.5 c026ae2ce7 fix(dashboard): prevent table chart infinite reload loop (#36686)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 13:45:36 -08:00
Michael S. Molina ae491aee00 docs: Add SQL Lab Query Comparison to community extensions registry (#36769) 2025-12-19 16:55:16 -03:00
Evan RusackasandClaude Opus 4.5 3258082819 docs: add SQL Lab Export to Parquet to community extensions registry (#36755)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 09:16:43 -08:00
Michael S. Molina d36ddbbb33 feat(extensions-cli): Add .gitignore generation to init command (#36768) 2025-12-19 13:33:37 -03:00
Michael S. Molina 5920cb57ea refactor(extensions): add Pydantic validation for extension configuration (#36767) 2025-12-19 13:33:10 -03:00
Joe Li fb6f3fbb4d chore: update files for release 6.0.0 (#36745) 2025-12-18 21:19:12 -08:00
Evan RusackasandClaude Opus 4.5 91539f77aa feat(docker): support running multiple Superset instances simultaneously (#36751)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 17:04:58 -08:00
Evan RusackasandClaude Opus 4.5 b8f31124d0 chore(frontend): migrate 13 JS/JSX files to TypeScript (#36720)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 13:54:49 -08:00
Evan RusackasandClaude Opus 4.5 da8e077a44 chore(frontend): migrate utility JS files to TypeScript (#36721)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 13:54:40 -08:00
Evan RusackasandClaude Opus 4.5 32435bc3e9 feat(docs): enhance Matomo analytics tracking (#36743)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 13:52:08 -08:00
Evan RusackasandClaude Opus 4.5 2cf0d7936e chore(pre-commit): exclude logos from end-of-file-fixer (#36744)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 13:51:50 -08:00
Evan RusackasandClaude Opus 4.5 0830a57fa6 feat(docs): add llms.txt for LLM-friendly documentation index (#36730)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 13:48:28 -08:00
Brandon Sovran 0f56e3b9ae fix: Implement SIP-40 error styles for GAQ (#36596) 2025-12-18 12:22:17 -08:00
Evan RusackasandClaude Opus 4.5 ee45b26ad7 fix(tests): optimize DatasourceEditorCurrency tests for CI reliability (#36723)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 09:37:53 -08:00
Kamil Gabryjelski f3407d7a56 chore: Close playwright browser gracefully (#36537) 2025-12-18 17:30:22 +01:00
Joe LiandClaude f51f7f3307 fix(tests): resolve flakey selectOption helper race condition (#36719)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 21:27:17 -08:00
Evan RusackasandClaude Opus 4.5 2f4f64dfe8 chore(frontend): migrate easy JS/JSX files to TypeScript (#36713)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 17:12:02 -08:00
Evan RusackasandClaude Opus 4.5 ae584c8886 chore: remove INTHEWILD.md after migration to YAML (#36718)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 12:59:19 -08:00
Đỗ Trọng Hải b1e004e122 build(dev-deps): remove stub type definition packages (#36706)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2025-12-17 12:57:44 -08:00
Vitor Avila 737a5162e4 fix: Use is_active for guest users (#36716) 2025-12-17 17:23:22 -03:00
Evan RusackasandClaude Opus 4.5 b800412eda fix(docs): add retry logic and concurrency handling for badge downloads (#36715)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 12:19:39 -08:00
Michael S. Molina 24a4f8510d docs: Add SQL Lab Export to Google Sheets to community extensions registry (#36714) 2025-12-17 16:53:10 -03:00
Yousuf Ansari 33a425bbbc fix(echarts): use scroll legend for horizontal layouts to prevent overlap (#36306) 2025-12-17 11:16:35 -08:00
5ce4c52cfa feat(docs): In the Wild page with YAML data and AntD components (#36386)
Co-authored-by: Catherine Qu <catherine.qu@mail.utoronto.ca>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 09:42:29 -08:00
Luis Sánchez c9ec173647 fix(SearchFilter): prevent unintended autocomplete on search input (#36209) 2025-12-17 20:41:07 +03:00
Michael S. Molina 71f9dcff5a chore: Bump core packages (0.0.1rc3, 0.0.1-rc6) (#36707) 2025-12-17 09:12:26 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 479b7a3fba chore(deps-dev): bump @pmmmwh/react-refresh-webpack-plugin from 0.5.17 to 0.6.2 in /superset-frontend (#36691)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 09:01:02 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 594ea972ca chore(deps-dev): bump @types/node from 25.0.2 to 25.0.3 in /superset-websocket (#36692)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 08:46:34 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d77f7b6d20 chore(deps): bump nanoid from 5.0.9 to 5.1.6 in /superset-frontend (#36586)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 08:46:17 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f4ded02e0d chore(deps-dev): bump typescript-eslint from 8.49.0 to 8.50.0 in /docs (#36650)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 08:46:01 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f97fa08477 chore(deps-dev): bump baseline-browser-mapping from 2.9.7 to 2.9.8 in /superset-frontend (#36690)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 08:45:47 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 789be78166 chore(deps-dev): bump webpack from 5.103.0 to 5.104.0 in /docs (#36695)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 21:42:48 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ea3d247017 chore(deps-dev): bump webpack-bundle-analyzer from 4.10.2 to 5.1.0 in /superset-frontend (#36610)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 16:06:20 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 6456f4c516 chore(deps): bump googleapis from 168.0.0 to 169.0.0 in /superset-frontend (#36646)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 15:48:07 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> e9bbf06938 chore(deps): bump re-resizable from 6.10.3 to 6.11.2 in /superset-frontend (#36647)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 15:47:53 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ebee35ea5a chore(deps-dev): bump typescript-eslint from 8.49.0 to 8.50.0 in /superset-websocket (#36649)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 15:47:13 -08:00
SBIN2010 d0fb77cbc8 fix: removed dashboard from main page in "All" tab, refreshes dashboard list (#35945) 2025-12-16 15:45:58 -08:00
Evan RusackasandClaude Opus 4.5 46659c2bd1 fix(tests): resolve flaky ExploreChartHeader export menu tests (#36642)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 15:44:30 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8407e9cf3b chore(deps): bump antd from 6.1.0 to 6.1.1 in /docs (#36655)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 15:43:53 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 5eeba2e734 chore(deps-dev): bump @typescript-eslint/parser from 8.49.0 to 8.50.0 in /docs (#36656)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 15:43:33 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 4ca8c000d1 chore(deps): update classnames requirement from ^2.2.5 to ^2.5.1 in /superset-frontend/packages/superset-ui-core (#36660)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 15:43:15 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7108658de0 chore(deps-dev): bump @babel/runtime-corejs3 from 7.28.2 to 7.28.4 in /superset-frontend (#36664)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 15:42:58 -08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 42311f602e chore(deps-dev): bump npm from 11.5.2 to 11.7.0 in /superset-frontend (#36668)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 15:42:39 -08:00
201 changed files with 12305 additions and 5347 deletions
+41
View File
@@ -0,0 +1,41 @@
#
# 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.
#
# Auto-configure Docker Compose for multi-instance support
# Requires direnv: https://direnv.net/
#
# Install: brew install direnv (or apt install direnv)
# Setup: Add 'eval "$(direnv hook bash)"' to ~/.bashrc (or ~/.zshrc)
# Allow: Run 'direnv allow' in this directory once
# Generate unique project name from directory
export COMPOSE_PROJECT_NAME=$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g')
# Find available ports sequentially to avoid collisions
_is_free() { ! lsof -i ":$1" &>/dev/null 2>&1; }
_p=80; while ! _is_free $_p; do ((_p++)); done; export NGINX_PORT=$_p
_p=8088; while ! _is_free $_p; do ((_p++)); done; export SUPERSET_PORT=$_p
_p=9000; while ! _is_free $_p; do ((_p++)); done; export NODE_PORT=$_p
_p=8080; while ! _is_free $_p || [ $_p -eq $NGINX_PORT ]; do ((_p++)); done; export WEBSOCKET_PORT=$_p
_p=8081; while ! _is_free $_p || [ $_p -eq $WEBSOCKET_PORT ]; do ((_p++)); done; export CYPRESS_PORT=$_p
_p=5432; while ! _is_free $_p; do ((_p++)); done; export DATABASE_PORT=$_p
_p=6379; while ! _is_free $_p; do ((_p++)); done; export REDIS_PORT=$_p
unset _p _is_free
echo "🐳 Superset configured: http://localhost:$SUPERSET_PORT (dev: localhost:$NODE_PORT)"
+1 -1
View File
@@ -41,8 +41,8 @@ body:
label: Superset version
options:
- master / latest-dev
- "6.0.0"
- "5.0.0"
- "4.1.3"
validations:
required: true
- type: dropdown
+1 -1
View File
@@ -54,7 +54,7 @@ repos:
exclude: ^helm/superset/templates/
- id: debug-statements
- id: end-of-file-fixer
exclude: .*/lerna\.json$
exclude: .*/lerna\.json$|^docs/static/img/logos/
- id: trailing-whitespace
exclude: ^.*\.(snap)
args: ["--markdown-linebreak-ext=md"]
+4
View File
@@ -76,6 +76,9 @@ snowflake.svg
ydb.svg
loading.svg
# docs third-party logos, i.e. docs/static/img/logos/*
logos/*
# docs-related
erd.puml
erd.svg
@@ -83,6 +86,7 @@ intro_header.txt
# for LLMs
llm-context.md
llms.txt
AGENTS.md
LLMS.md
CLAUDE.md
+1
View File
@@ -49,3 +49,4 @@ under the License.
- [4.1.3](./CHANGELOG/4.1.3.md)
- [4.1.4](./CHANGELOG/4.1.4.md)
- [5.0.0](./CHANGELOG/5.0.0.md)
- [6.0.0](./CHANGELOG/6.0.0.md)
+1062
View File
File diff suppressed because it is too large Load Diff
+20 -1
View File
@@ -18,7 +18,7 @@
# Python version installed; we need 3.10-3.11
PYTHON=`command -v python3.11 || command -v python3.10`
.PHONY: install superset venv pre-commit
.PHONY: install superset venv pre-commit up down logs ps nuke
install: superset pre-commit
@@ -112,3 +112,22 @@ report-celery-beat:
admin-user:
superset fab create-admin
# Docker Compose with auto-assigned ports (for running multiple instances)
up:
./scripts/docker-compose-up.sh
up-detached:
./scripts/docker-compose-up.sh -d
down:
./scripts/docker-compose-up.sh down
logs:
./scripts/docker-compose-up.sh logs -f
ps:
./scripts/docker-compose-up.sh ps
nuke:
./scripts/docker-compose-up.sh nuke
+2 -2
View File
@@ -55,7 +55,7 @@ A modern, enterprise-ready business intelligence web application.
[**Get Involved**](#get-involved) |
[**Contributor Guide**](#contributor-guide) |
[**Resources**](#resources) |
[**Organizations Using Superset**](https://github.com/apache/superset/blob/master/RESOURCES/INTHEWILD.md)
[**Organizations Using Superset**](https://superset.apache.org/inTheWild)
## Why Superset?
@@ -171,7 +171,7 @@ how to set up a development environment.
## Resources
- [Superset "In the Wild"](https://github.com/apache/superset/blob/master/RESOURCES/INTHEWILD.md) - open a PR to add your org to the list!
- [Superset "In the Wild"](https://superset.apache.org/inTheWild) - see who's using Superset, and [add your organization](https://github.com/apache/superset/edit/master/RESOURCES/INTHEWILD.yaml) to the list!
- [Feature Flags](https://github.com/apache/superset/blob/master/RESOURCES/FEATURE_FLAGS.md) - the status of Superset's Feature Flags.
- [Standard Roles](https://github.com/apache/superset/blob/master/RESOURCES/STANDARD_ROLES.md) - How RBAC permissions map to roles.
- [Superset Wiki](https://github.com/apache/superset/wiki) - Tons of additional community resources: best practices, community content and other information.
-226
View File
@@ -1,226 +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 Users in the Wild
Here's a list of organizations, broken down into broad industry categories, that have taken the time to send a PR to let
the world know they are using Apache Superset. If you are a user and want to be recognized,
all you have to do is file a simple PR [like this one](https://github.com/apache/superset/pull/10122) — [just click here](https://github.com/apache/superset/edit/master/RESOURCES/INTHEWILD.md) to do so. If you think
the categorization is inaccurate, please file a PR with your correction as well.
Join our growing community!
### Sharing Economy
- [Airbnb](https://github.com/airbnb)
- [Faasos](https://faasos.com/) [@shashanksingh]
- [Free2Move](https://www.free2move.com/) [@PaoloTerzi]
- [Hostnfly](https://www.hostnfly.com/) [@alexisrosuel]
- [Lime](https://www.li.me/) [@cxmcc]
- [Lyft](https://www.lyft.com/)
- [Ontruck](https://www.ontruck.com/)
### Financial Services
- [Aktia Bank plc](https://www.aktia.com)
- [American Express](https://www.americanexpress.com) [@TheLastSultan]
- [bumper](https://www.bumper.co/) [@vasu-ram, @JamiePercival]
- [Cape Crypto](https://capecrypto.com)
- [Capital Service S.A.](https://capitalservice.pl) [@pkonarzewski]
- [Clark.de](https://clark.de/)
- [Europace](https://europace.de)
- [KarrotPay](https://www.daangnpay.com/)
- [Remita](https://remita.net) [@mujibishola]
- [Taveo](https://www.taveo.com) [@codek]
- [Unit](https://www.unit.co/about-us) [@amitmiran137]
- [Wise](https://wise.com) [@koszti]
- [Xendit](https://xendit.co/) [@LieAlbertTriAdrian]
- [Cover Genius](https://covergenius.com/)
### Gaming
- [Popoko VM Games Studio](https://popoko.live)
### E-Commerce
- [AiHello](https://www.aihello.com) [@ganeshkrishnan1]
- [Bazaar Technologies](https://www.bazaartech.com) [@umair-abro]
- [Dragonpass](https://www.dragonpass.com.cn/) [@zhxjdwh]
- [Dropit Shopping](https://www.dropit.shop/) [@dropit-dev]
- [Fanatics](https://www.fanatics.com/) [@coderfender]
- [Fordeal](https://www.fordeal.com) [@Renkai]
- [Fynd](https://www.fynd.com/) [@darpanjain07]
- [GFG - Global Fashion Group](https://global-fashion-group.com) [@ksaagariconic]
- [GoTo/Gojek](https://www.gojek.io/) [@gwthm-in]
- [HuiShouBao](https://www.huishoubao.com/) [@Yukinoshita-Yukino]
- [Now](https://www.now.vn/) [@davidkohcw]
- [Qunar](https://www.qunar.com/) [@flametest]
- [Rakuten Viki](https://www.viki.com)
- [Shopee](https://shopee.sg) [@xiaohanyu]
- [Shopkick](https://www.shopkick.com) [@LAlbertalli]
- [ShopUp](https://www.shopup.org/) [@gwthm-in]
- [Tails.com](https://tails.com/gb/) [@alanmcruickshank]
- [THE ICONIC](https://theiconic.com.au/) [@ksaagariconic]
- [Utair](https://www.utair.ru) [@utair-digital]
- [VkusVill](https://vkusvill.ru/) [@ETselikov]
- [Zalando](https://www.zalando.com) [@dmigo]
- [Zalora](https://www.zalora.com) [@ksaagariconic]
- [Zepto](https://www.zeptonow.com/) [@gwthm-in]
### Enterprise Technology
- [A3Data](https://a3data.com.br) [@neylsoncrepalde]
- [Analytics Aura](https://analyticsaura.com/) [@Analytics-Aura]
- [Apollo GraphQL](https://www.apollographql.com/) [@evans]
- [Astronomer](https://www.astronomer.io) [@ryw]
- [Avesta Technologies](https://avestatechnologies.com/) [@TheRum]
- [Caizin](https://caizin.com/) [@tejaskatariya]
- [Canonical](https://canonical.com)
- [Careem](https://www.careem.com/) [@samraHanif0340]
- [Cloudsmith](https://cloudsmith.io) [@alancarson]
- [Cyberhaven](https://www.cyberhaven.com/) [@toliver-ch]
- [Deepomatic](https://deepomatic.com/) [@Zanoellia]
- [Dial Once](https://www.dial-once.com/)
- [Dremio](https://dremio.com) [@narendrans]
- [EFinance](https://www.efinance.com.eg) [@habeeb556]
- [Elestio](https://elest.io/) [@kaiwalyakoparkar]
- [ELMO Cloud HR & Payroll](https://elmosoftware.com.au/)
- [Endress+Hauser](https://www.endress.com/) [@rumbin]
- [FBK - ICT center](https://ict.fbk.eu)
- [Formbricks](https://formbricks.com)
- [Gavagai](https://gavagai.io) [@gavagai-corp]
- [GfK Data Lab](https://www.gfk.com/home) [@mherr]
- [HPE](https://www.hpe.com/in/en/home.html) [@anmol-hpe]
- [Hydrolix](https://www.hydrolix.io/)
- [Intercom](https://www.intercom.com/) [@kate-gallo]
- [jampp](https://jampp.com/)
- [Konfío](https://konfio.mx) [@uis-rodriguez]
- [Mainstrat](https://mainstrat.com/)
- [mishmash io](https://mishmash.io/) [@mishmash-io]
- [Myra Labs](https://www.myralabs.com/) [@viksit]
- [Nielsen](https://www.nielsen.com/) [@amitNielsen]
- [Ona](https://ona.io) [@pld]
- [Orange](https://www.orange.com) [@icsu]
- [Oslandia](https://oslandia.com)
- [Oxylabs](https://oxylabs.io/) [@rytis-ulys]
- [Peak AI](https://www.peak.ai/) [@azhar22k]
- [PeopleDoc](https://www.people-doc.com) [@rodo]
- [PlaidCloud](https://www.plaidcloud.com)
- [Preset, Inc.](https://preset.io)
- [PubNub](https://pubnub.com) [@jzucker2]
- [ReadyTech](https://www.readytech.io)
- [Reward Gateway](https://www.rewardgateway.com)
- [RIADVICE](https://riadvice.tn) [@riadvice]
- [ScopeAI](https://www.getscopeai.com) [@iloveluce]
- [shipmnts](https://shipmnts.com)
- [Showmax](https://showmax.com) [@bobek]
- [SingleStore](https://www.singlestore.com/)
- [TechAudit](https://www.techaudit.info) [@ETselikov]
- [Tenable](https://www.tenable.com) [@dflionis]
- [Tentacle](https://www.linkedin.com/company/tentacle-cmi/) [@jdclarke5]
- [timbr.ai](https://timbr.ai/) [@semantiDan]
- [Tobii](https://www.tobii.com/) [@dwa]
- [Tooploox](https://www.tooploox.com/) [@jakubczaplicki]
- [Unvired](https://unvired.com) [@srinisubramanian]
- [Virtuoso QA](https://www.virtuosoqa.com)
- [Whale](https://whale.im)
- [Windsor.ai](https://www.windsor.ai/) [@octaviancorlade]
- [WinWin Network马上赢](https://brandct.cn/) [@wenbinye]
- [Zeta](https://www.zeta.tech/) [@shaikidris]
### Media & Entertainment
- [6play](https://www.6play.fr) [@CoryChaplin]
- [bilibili](https://www.bilibili.com) [@Moinheart]
- [BurdaForward](https://www.burda-forward.de/en/)
- [Douban](https://www.douban.com/) [@luchuan]
- [Kuaishou](https://www.kuaishou.com/) [@zhaoyu89730105]
- [Netflix](https://www.netflix.com/)
- [Prensa Iberica](https://www.prensaiberica.es/) [@zamar-roura]
- [TME QQMUSIC/WESING](https://www.tencentmusic.com/) [@shenyuanli,@marklaw]
- [Xite](https://xite.com/) [@shashankkoppar]
- [Zaihang](https://www.zaih.com/)
### Education
- [Aveti Learning](https://avetilearning.com/) [@TheShubhendra]
- [Brilliant.org](https://brilliant.org/)
- [Open edX](https://openedx.org/)
- [Platzi.com](https://platzi.com/)
- [Sunbird](https://www.sunbird.org/) [@eksteporg]
- [The GRAPH Network](https://thegraphnetwork.org/) [@fccoelho]
- [Udemy](https://www.udemy.com/) [@sungjuly]
- [VIPKID](https://www.vipkid.com.cn/) [@illpanda]
- [WikiMedia Foundation](https://wikimediafoundation.org) [@vg]
### Energy
- [Airboxlab](https://foobot.io) [@antoine-galataud]
- [DouroECI](https://www.douroeci.com/) [@nunohelibeires]
- [Safaricom](https://www.safaricom.co.ke/) [@mmutiso]
- [Scoot](https://scoot.co/) [@haaspt]
- [Wattbewerb](https://wattbewerb.de/) [@wattbewerb]
### Healthcare
- [Amino](https://amino.com) [@shkr]
- [Bluesquare](https://www.bluesquarehub.com/) [@madewulf]
- [Care](https://www.getcare.io/) [@alandao2021]
- [Living Goods](https://www.livinggoods.org) [@chelule]
- [Maieutical Labs](https://maieuticallabs.it) [@xrmx]
- [Medic](https://medic.org) [@1yuv]
- [REDCap Cloud](https://www.redcapcloud.com/)
- [TrustMedis](https://trustmedis.com/) [@famasya]
- [WeSure](https://www.wesure.cn/)
- [2070Health](https://2070health.com/)
### HR / Staffing
- [Swile](https://www.swile.co/) [@PaoloTerzi]
- [Symmetrics](https://www.symmetrics.fyi)
- [bluquist](https://bluquist.com/)
### Government
- [City of Ann Arbor, MI](https://www.a2gov.org/) [@sfirke]
- [RIS3 Strategy of CZ, MIT CR](https://www.ris3.cz/) [@RIS3CZ]
- [NRLM - Sarathi, India](https://pib.gov.in/PressReleasePage.aspx?PRID=1999586)
### Travel
- [Agoda](https://www.agoda.com/) [@lostseaway, @maiake, @obombayo]
- [HomeToGo](https://hometogo.com/) [@pedromartinsteenstrup]
- [Skyscanner](https://www.skyscanner.net/) [@cleslie, @stanhoucke]
### Others
- [10Web](https://10web.io/)
- [AI inside](https://inside.ai/en/)
- [Automattic](https://automattic.com/) [@Khrol, @Usiel]
- [Dropbox](https://www.dropbox.com/) [@bkyryliuk]
- [Flowbird](https://flowbird.com) [@EmmanuelCbd]
- [GEOTAB](https://www.geotab.com) [@JZ6]
- [Grassroot](https://www.grassrootinstitute.org/)
- [Increff](https://www.increff.com/) [@ishansinghania]
- [komoot](https://www.komoot.com/) [@christophlingg]
- [Let's Roam](https://www.letsroam.com/)
- [Machrent SA](https://www.machrent.com/)
- [Onebeat](https://1beat.com/) [@GuyAttia]
- [X](https://x.com/)
- [VLMedia](https://www.vlmedia.com.tr/) [@ibotheperfect]
- [Yahoo!](https://yahoo.com/)
+653
View File
@@ -0,0 +1,653 @@
# 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.
# Apache Superset Users in the Wild
#
# To add your organization:
# 1. Find the appropriate category (or add a new one)
# 2. Add an entry with your organization details
# 3. Optionally add a logo file to docs/static/img/logos/
#
# Required fields:
# - name: Your organization name
# - url: Link to your organization's website
#
# Optional fields:
# - logo: Filename of logo in docs/static/img/logos/ (e.g., "mycompany.svg")
# - contributors: List of GitHub usernames who contributed (e.g., ["@username"])
categories:
Sharing Economy:
- name: Airbnb
url: https://github.com/airbnb
- name: Faasos
url: https://faasos.com/
contributors: ["@shashanksingh"]
- name: Free2Move
url: https://www.free2move.com/
contributors: ["@PaoloTerzi"]
- name: Hostnfly
url: https://www.hostnfly.com/
contributors: ["@alexisrosuel"]
- name: Lime
url: https://www.li.me/
contributors: ["@cxmcc"]
- name: Lyft
url: https://www.lyft.com/
- name: Ontruck
url: https://www.ontruck.com/
Financial Services:
- name: Aktia Bank plc
url: https://www.aktia.com
- name: American Express
url: https://www.americanexpress.com
contributors: ["@TheLastSultan"]
- name: bumper
url: https://www.bumper.co/
contributors: ["@vasu-ram", "@JamiePercival"]
- name: Cape Crypto
url: https://capecrypto.com
- name: Capital Service S.A.
url: https://capitalservice.pl
contributors: ["@pkonarzewski"]
- name: Clark.de
url: https://clark.de/
- name: EnquiryLabs
url: https://www.enquirylabs.co.uk
- name: Europace
url: https://europace.de
- name: KarrotPay
url: https://www.daangnpay.com/
- name: Remita
url: https://remita.net
contributors: ["@mujibishola"]
- name: Taveo
url: https://www.taveo.com
contributors: ["@codek"]
- name: Unit
url: https://www.unit.co/about-us
contributors: ["@amitmiran137"]
- name: Wise
url: https://wise.com
contributors: ["@koszti"]
- name: Xendit
url: https://xendit.co/
contributors: ["@LieAlbertTriAdrian"]
- name: Cover Genius
url: https://covergenius.com/
Gaming:
- name: Popoko VM Games Studio
url: https://popoko.live
E-Commerce:
- name: AiHello
url: https://www.aihello.com
contributors: ["@ganeshkrishnan1"]
- name: Bazaar Technologies
url: https://www.bazaartech.com
contributors: ["@umair-abro"]
- name: Dragonpass
url: https://www.dragonpass.com.cn/
contributors: ["@zhxjdwh"]
- name: Dropit Shopping
url: https://www.dropit.shop/
contributors: ["@dropit-dev"]
- name: Fanatics
url: https://www.fanatics.com/
contributors: ["@coderfender"]
- name: Fordeal
url: https://www.fordeal.com
contributors: ["@Renkai"]
- name: Fynd
url: https://www.fynd.com/
contributors: ["@darpanjain07"]
- name: GFG - Global Fashion Group
url: https://global-fashion-group.com
contributors: ["@ksaagariconic"]
- name: GoTo/Gojek
url: https://www.gojek.io/
contributors: ["@gwthm-in"]
- name: HuiShouBao
url: https://www.huishoubao.com/
contributors: ["@Yukinoshita-Yukino"]
- name: Now
url: https://www.now.vn/
contributors: ["@davidkohcw"]
- name: Qunar
url: https://www.qunar.com/
contributors: ["@flametest"]
- name: Rakuten Viki
url: https://www.viki.com
- name: Shopee
url: https://shopee.sg
contributors: ["@xiaohanyu"]
- name: Shopkick
url: https://www.shopkick.com
contributors: ["@LAlbertalli"]
- name: ShopUp
url: https://www.shopup.org/
contributors: ["@gwthm-in"]
- name: Tails.com
url: https://tails.com/gb/
contributors: ["@alanmcruickshank"]
- name: THE ICONIC
url: https://theiconic.com.au/
contributors: ["@ksaagariconic"]
- name: Utair
url: https://www.utair.ru
contributors: ["@utair-digital"]
- name: VkusVill
url: https://vkusvill.ru/
contributors: ["@ETselikov"]
- name: Zalando
url: https://www.zalando.com
contributors: ["@dmigo"]
- name: Zalora
url: https://www.zalora.com
contributors: ["@ksaagariconic"]
- name: Zepto
url: https://www.zeptonow.com/
contributors: ["@gwthm-in"]
Enterprise Technology:
- name: A3Data
url: https://a3data.com.br
contributors: ["@neylsoncrepalde"]
- name: Analytics Aura
url: https://analyticsaura.com/
contributors: ["@Analytics-Aura"]
- name: Apollo GraphQL
url: https://www.apollographql.com/
contributors: ["@evans"]
- name: Astronomer
url: https://www.astronomer.io
contributors: ["@ryw"]
- name: Avesta Technologies
url: https://avestatechnologies.com/
contributors: ["@TheRum"]
- name: Caizin
url: https://caizin.com/
contributors: ["@tejaskatariya"]
- name: Canonical
url: https://canonical.com
- name: Careem
url: https://www.careem.com/
contributors: ["@samraHanif0340"]
- name: Cloudsmith
url: https://cloudsmith.io
contributors: ["@alancarson"]
- name: Cyberhaven
url: https://www.cyberhaven.com/
contributors: ["@toliver-ch"]
- name: Deepomatic
url: https://deepomatic.com/
contributors: ["@Zanoellia"]
- name: Dial Once
url: https://www.dial-once.com/
- name: Dremio
url: https://dremio.com
contributors: ["@narendrans"]
- name: EFinance
url: https://www.efinance.com.eg
contributors: ["@habeeb556"]
- name: Elestio
url: https://elest.io/
contributors: ["@kaiwalyakoparkar"]
- name: ELMO Cloud HR & Payroll
url: https://elmosoftware.com.au/
- name: Endress+Hauser
url: https://www.endress.com/
contributors: ["@rumbin"]
- name: FBK - ICT center
url: https://ict.fbk.eu
- name: Formbricks
url: https://formbricks.com
- name: Gavagai
url: https://gavagai.io
contributors: ["@gavagai-corp"]
- name: GfK Data Lab
url: https://www.gfk.com/home
contributors: ["@mherr"]
- name: HPE
url: https://www.hpe.com/in/en/home.html
contributors: ["@anmol-hpe"]
- name: Hydrolix
url: https://www.hydrolix.io/
- name: Intercom
url: https://www.intercom.com/
contributors: ["@kate-gallo"]
- name: jampp
url: https://jampp.com/
- name: Konfío
url: https://konfio.mx
contributors: ["@uis-rodriguez"]
- name: Mainstrat
url: https://mainstrat.com/
- name: mishmash io
url: https://mishmash.io/
contributors: ["@mishmash-io"]
- name: Myra Labs
url: https://www.myralabs.com/
contributors: ["@viksit"]
- name: Nielsen
url: https://www.nielsen.com/
contributors: ["@amitNielsen"]
- name: Ona
url: https://ona.io
contributors: ["@pld"]
- name: Orange
url: https://www.orange.com
contributors: ["@icsu"]
- name: Oslandia
url: https://oslandia.com
- name: Oxylabs
url: https://oxylabs.io/
contributors: ["@rytis-ulys"]
- name: Peak AI
url: https://www.peak.ai/
contributors: ["@azhar22k"]
- name: PeopleDoc
url: https://www.people-doc.com
contributors: ["@rodo"]
- name: PlaidCloud
url: https://www.plaidcloud.com
- name: Preset, Inc.
url: https://preset.io
logo: preset.svg
contributors: ["@mistercrunch", "@betodealmeida", "@dpgaspar", "@rusackas", "@sadpandajoe", "@Vitor-Avila", "@kgabryje", "@geido", "@eschutho", "@Antonio-RiveroMartnez", "@yousoph"]
- name: PubNub
url: https://pubnub.com
contributors: ["@jzucker2"]
- name: ReadyTech
url: https://www.readytech.io
- name: Reward Gateway
url: https://www.rewardgateway.com
- name: RIADVICE
url: https://riadvice.tn
contributors: ["@riadvice"]
- name: ScopeAI
url: https://www.getscopeai.com
contributors: ["@iloveluce"]
- name: shipmnts
url: https://shipmnts.com
- name: Showmax
url: https://showmax.com
contributors: ["@bobek"]
- name: SingleStore
url: https://www.singlestore.com/
- name: TechAudit
url: https://www.techaudit.info
contributors: ["@ETselikov"]
- name: Tenable
url: https://www.tenable.com
contributors: ["@dflionis"]
- name: Tentacle
url: https://www.linkedin.com/company/tentacle-cmi/
contributors: ["@jdclarke5"]
- name: timbr.ai
url: https://timbr.ai/
contributors: ["@semantiDan"]
- name: Tobii
url: https://www.tobii.com/
contributors: ["@dwa"]
- name: Tooploox
url: https://www.tooploox.com/
contributors: ["@jakubczaplicki"]
- name: Unvired
url: https://unvired.com
contributors: ["@srinisubramanian"]
- name: Virtuoso QA
url: https://www.virtuosoqa.com
- name: Whale
url: https://whale.im
- name: Windsor.ai
url: https://www.windsor.ai/
contributors: ["@octaviancorlade"]
- name: WinWin Network马上赢
url: https://brandct.cn/
contributors: ["@wenbinye"]
- name: Zeta
url: https://www.zeta.tech/
contributors: ["@shaikidris"]
Media & Entertainment:
- name: 6play
url: https://www.6play.fr
contributors: ["@CoryChaplin"]
- name: bilibili
url: https://www.bilibili.com
contributors: ["@Moinheart"]
- name: BurdaForward
url: https://www.burda-forward.de/en/
- name: Douban
url: https://www.douban.com/
contributors: ["@luchuan"]
- name: Kuaishou
url: https://www.kuaishou.com/
contributors: ["@zhaoyu89730105"]
- name: Netflix
url: https://www.netflix.com/
- name: Prensa Iberica
url: https://www.prensaiberica.es/
contributors: ["@zamar-roura"]
- name: TME QQMUSIC/WESING
url: https://www.tencentmusic.com/
contributors: ["@shenyuanli", "@marklaw"]
- name: Xite
url: https://xite.com/
contributors: ["@shashankkoppar"]
- name: Zaihang
url: https://www.zaih.com/
Education:
- name: Aveti Learning
url: https://avetilearning.com/
contributors: ["@TheShubhendra"]
- name: Brilliant.org
url: https://brilliant.org/
- name: Open edX
url: https://openedx.org/
- name: Platzi.com
url: https://platzi.com/
- name: Sunbird
url: https://www.sunbird.org/
contributors: ["@eksteporg"]
- name: The GRAPH Network
url: https://thegraphnetwork.org/
contributors: ["@fccoelho"]
- name: Udemy
url: https://www.udemy.com/
contributors: ["@sungjuly"]
- name: VIPKID
url: https://www.vipkid.com.cn/
contributors: ["@illpanda"]
- name: WikiMedia Foundation
url: https://wikimediafoundation.org
contributors: ["@vg"]
Energy:
- name: Airboxlab
url: https://foobot.io
contributors: ["@antoine-galataud"]
- name: DouroECI
url: https://www.douroeci.com/
contributors: ["@nunohelibeires"]
- name: Safaricom
url: https://www.safaricom.co.ke/
contributors: ["@mmutiso"]
- name: Scoot
url: https://scoot.co/
contributors: ["@haaspt"]
- name: Wattbewerb
url: https://wattbewerb.de/
contributors: ["@wattbewerb"]
- name: Rogow
url: https://rogow.com.br/
contributors: ["@nilmonto"]
Healthcare:
- name: Amino
url: https://amino.com
contributors: ["@shkr"]
- name: Bluesquare
url: https://www.bluesquarehub.com/
contributors: ["@madewulf"]
- name: Care
url: https://www.getcare.io/
contributors: ["@alandao2021"]
- name: Living Goods
url: https://www.livinggoods.org
contributors: ["@chelule"]
- name: Maieutical Labs
url: https://maieuticallabs.it
contributors: ["@xrmx"]
- name: Medic
url: https://medic.org
contributors: ["@1yuv"]
- name: REDCap Cloud
url: https://www.redcapcloud.com/
- name: TrustMedis
url: https://trustmedis.com/
contributors: ["@famasya"]
- name: WeSure
url: https://www.wesure.cn/
- name: 2070Health
url: https://2070health.com/
HR / Staffing:
- name: Swile
url: https://www.swile.co/
contributors: ["@PaoloTerzi"]
- name: Symmetrics
url: https://www.symmetrics.fyi
- name: bluquist
url: https://bluquist.com/
Government:
- name: City of Ann Arbor, MI
url: https://www.a2gov.org/
contributors: ["@sfirke"]
- name: RIS3 Strategy of CZ, MIT CR
url: https://www.ris3.cz/
contributors: ["@RIS3CZ"]
- name: NRLM - Sarathi, India
url: https://pib.gov.in/PressReleasePage.aspx?PRID=1999586
Mobile Software:
- name: VLMedia
url: https://www.vlmedia.com.tr
logo: vlmedia.svg
contributors: ["@iercan"]
Travel:
- name: Agoda
url: https://www.agoda.com/
contributors: ["@lostseaway", "@maiake", "@obombayo"]
- name: HomeToGo
url: https://hometogo.com/
contributors: ["@pedromartinsteenstrup"]
- name: Skyscanner
url: https://www.skyscanner.net/
contributors: ["@cleslie", "@stanhoucke"]
Others:
- name: 10Web
url: https://10web.io/
- name: AI inside
url: https://inside.ai/en/
- name: Automattic
url: https://automattic.com/
contributors: ["@Khrol", "@Usiel"]
- name: Dropbox
url: https://www.dropbox.com/
contributors: ["@bkyryliuk"]
- name: Flowbird
url: https://flowbird.com
contributors: ["@EmmanuelCbd"]
- name: GEOTAB
url: https://www.geotab.com
contributors: ["@JZ6"]
- name: Grassroot
url: https://www.grassrootinstitute.org/
- name: Increff
url: https://www.increff.com/
contributors: ["@ishansinghania"]
- name: komoot
url: https://www.komoot.com/
contributors: ["@christophlingg"]
- name: Let's Roam
url: https://www.letsroam.com/
- name: Machrent SA
url: https://www.machrent.com/
- name: Onebeat
url: https://1beat.com/
contributors: ["@GuyAttia"]
- name: X
url: https://x.com/
- name: Yahoo!
url: https://yahoo.com/
+24 -27
View File
@@ -125,8 +125,30 @@ See `superset/mcp_service/PRODUCTION.md` for deployment guides.
---
- [35621](https://github.com/apache/superset/pull/35621): The default hash algorithm has changed from MD5 to SHA-256 for improved security and FedRAMP compliance. This affects cache keys for thumbnails, dashboard digests, chart digests, and filter option names. Existing cached data will be invalidated upon upgrade. To opt out of this change and maintain backward compatibility, set `HASH_ALGORITHM = "md5"` in your `superset_config.py`.
- [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.
- [35062](https://github.com/apache/superset/pull/35062): Changed the function signature of `setupExtensions` to `setupCodeOverrides` with options as arguments.
### Breaking Changes
- [36317](https://github.com/apache/superset/pull/36317): The `CUSTOM_FONT_URLS` configuration option has been removed. Use the new per-theme `fontUrls` token in `THEME_DEFAULT` or database-managed themes instead.
- **Before:**
```python
CUSTOM_FONT_URLS = [
"https://fonts.example.com/myfont.css",
]
```
- **After:**
```python
THEME_DEFAULT = {
"token": {
"fontUrls": [
"https://fonts.example.com/myfont.css",
],
# ... other tokens
}
}
```
## 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).
- [34536](https://github.com/apache/superset/pull/34536): The `ENVIRONMENT_TAG_CONFIG` color values have changed to support only Ant Design semantic colors. Update your `superset_config.py`:
@@ -143,35 +165,10 @@ Note: Pillow is now a required dependency (previously optional) to support image
- [33116](https://github.com/apache/superset/pull/33116) In Echarts Series charts (e.g. Line, Area, Bar, etc.) charts, the `x_axis_sort_series` and `x_axis_sort_series_ascending` form data items have been renamed with `x_axis_sort` and `x_axis_sort_asc`.
There's a migration added that can potentially affect a significant number of existing charts.
- [32317](https://github.com/apache/superset/pull/32317) The horizontal filter bar feature is now out of testing/beta development and its feature flag `HORIZONTAL_FILTER_BAR` has been removed.
- [31590](https://github.com/apache/superset/pull/31590) Marks the beginning of intricate work around supporting dynamic Theming, and breaks support for [THEME_OVERRIDES](https://github.com/apache/superset/blob/732de4ac7fae88e29b7f123b6cbb2d7cd411b0e4/superset/config.py#L671) in favor of a new theming system based on AntD V5. Likely this will be in disrepair until settling over the 5.x lifecycle.
- [31590](https://github.com/apache/superset/pull/31590) Marks the begining of intricate work around supporting dynamic Theming, and breaks support for [THEME_OVERRIDES](https://github.com/apache/superset/blob/732de4ac7fae88e29b7f123b6cbb2d7cd411b0e4/superset/config.py#L671) in favor of a new theming system based on AntD V5. Likely this will be in disrepair until settling over the 5.x lifecycle.
- [32432](https://github.com/apache/superset/pull/31260) Moves the List Roles FAB view to the frontend and requires `FAB_ADD_SECURITY_API` to be enabled in the configuration and `superset init` to be executed.
- [34319](https://github.com/apache/superset/pull/34319) Drill to Detail and Drill By is now supported in Embedded mode, and also with the `DASHBOARD_RBAC` FF. If you don't want to expose these features in Embedded / `DASHBOARD_RBAC`, make sure the roles used for Embedded / `DASHBOARD_RBAC`don't have the required permissions to perform D2D actions.
### Breaking Changes
#### CUSTOM_FONT_URLS removed
The `CUSTOM_FONT_URLS` configuration option has been removed. Use the new per-theme `fontUrls` token in `THEME_DEFAULT` or database-managed themes instead.
**Before (5.x):**
```python
CUSTOM_FONT_URLS = [
"https://fonts.example.com/myfont.css",
]
```
**After (6.0):**
```python
THEME_DEFAULT = {
"token": {
"fontUrls": [
"https://fonts.example.com/myfont.css",
],
# ... other tokens
}
}
```
## 5.0.0
- [31976](https://github.com/apache/superset/pull/31976) Removed the `DISABLE_LEGACY_DATASOURCE_EDITOR` feature flag. The previous value of the feature flag was `True` and now the feature is permanently removed.
+9 -17
View File
@@ -54,10 +54,9 @@ services:
- path: docker/.env-local # optional override
required: false
image: nginx:latest
container_name: superset_nginx
restart: unless-stopped
ports:
- "80:80"
- "${NGINX_PORT:-80}:80"
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
@@ -66,10 +65,9 @@ services:
redis:
image: redis:7
container_name: superset_cache
restart: unless-stopped
ports:
- "127.0.0.1:6379:6379"
- "127.0.0.1:${REDIS_PORT:-6379}:6379"
volumes:
- redis:/data
@@ -80,10 +78,9 @@ services:
- path: docker/.env-local # optional override
required: false
image: postgres:16
container_name: superset_db
restart: unless-stopped
ports:
- "127.0.0.1:5432:5432"
- "127.0.0.1:${DATABASE_PORT:-5432}:5432"
volumes:
- db_home:/var/lib/postgresql/data
- ./docker/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
@@ -96,13 +93,12 @@ services:
required: false
build:
<<: *common-build
container_name: superset_app
command: ["/app/docker/docker-bootstrap.sh", "app"]
restart: unless-stopped
ports:
- 8088:8088
- ${SUPERSET_PORT:-8088}:8088
# When in cypress-mode ->
- 8081:8081
- ${CYPRESS_PORT:-8081}:8081
extra_hosts:
- "host.docker.internal:host-gateway"
user: *superset-user
@@ -114,10 +110,9 @@ services:
SUPERSET__SQLALCHEMY_EXAMPLES_URI: "duckdb:////app/data/examples.duckdb"
superset-websocket:
container_name: superset_websocket
build: ./superset-websocket
ports:
- 8080:8080
- ${WEBSOCKET_PORT:-8080}:8080
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
@@ -149,7 +144,6 @@ services:
superset-init:
build:
<<: *common-build
container_name: superset_init
command: ["/app/docker/docker-init.sh"]
env_file:
- path: docker/.env # default
@@ -186,9 +180,10 @@ services:
SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}"
# configuring the dev-server to use the host.docker.internal to connect to the backend
superset: "http://superset:8088"
# Bind to all interfaces so Docker port mapping works
WEBPACK_DEVSERVER_HOST: "0.0.0.0"
ports:
- "127.0.0.1:9000:9000" # exposing the dynamic webpack dev server
container_name: superset_node
- "127.0.0.1:${NODE_PORT:-9000}:9000" # exposing the dynamic webpack dev server
command: ["/app/docker/docker-frontend.sh"]
env_file:
- path: docker/.env # default
@@ -200,7 +195,6 @@ services:
superset-worker:
build:
<<: *common-build
container_name: superset_worker
command: ["/app/docker/docker-bootstrap.sh", "worker"]
env_file:
- path: docker/.env # default
@@ -226,7 +220,6 @@ services:
superset-worker-beat:
build:
<<: *common-build
container_name: superset_worker_beat
command: ["/app/docker/docker-bootstrap.sh", "beat"]
env_file:
- path: docker/.env # default
@@ -244,7 +237,6 @@ services:
superset-tests-worker:
build:
<<: *common-build
container_name: superset_tests_worker
command: ["/app/docker/docker-bootstrap.sh", "worker"]
env_file:
- path: docker/.env # default
+9
View File
@@ -21,6 +21,15 @@ PYTHONUNBUFFERED=1
COMPOSE_PROJECT_NAME=superset
DEV_MODE=true
# Port configuration (override in .env-local for multiple instances)
# NGINX_PORT=80
# SUPERSET_PORT=8088
# NODE_PORT=9000
# WEBSOCKET_PORT=8080
# CYPRESS_PORT=8081
# DATABASE_PORT=5432
# REDIS_PORT=6379
# database configurations (do not modify)
DATABASE_DB=superset
DATABASE_HOST=db
+39
View File
@@ -0,0 +1,39 @@
#
# 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.
#
# -----------------------------------------------------------------------
# Example .env-local file for running multiple Superset instances
# Copy this file to .env-local and customize for your setup
# -----------------------------------------------------------------------
# Unique project name prevents container/volume conflicts between clones
# Each clone should have a different name (e.g., superset-pr123, superset-feature-x)
COMPOSE_PROJECT_NAME=superset-dev2
# Port offsets for running multiple instances simultaneously
# Instance 1 (default): 80, 8088, 9000, 8080, 8081, 5432, 6379
# Instance 2 example: 81, 8089, 9001, 8082, 8083, 5433, 6380
NGINX_PORT=81
SUPERSET_PORT=8089
NODE_PORT=9001
WEBSOCKET_PORT=8082
CYPRESS_PORT=8083
DATABASE_PORT=5433
REDIS_PORT=6380
# For verbose logging during development:
# SUPERSET_LOG_LEVEL=debug
+28
View File
@@ -77,6 +77,34 @@ To run the container, simply run: `docker compose up`
After waiting several minutes for Superset initialization to finish, you can open a browser and view [`http://localhost:8088`](http://localhost:8088)
to start your journey.
### Running Multiple Instances
If you need to run multiple Superset instances simultaneously (e.g., different branches or clones), use the make targets which automatically find available ports:
```bash
make up
```
This automatically:
- Generates a unique project name from your directory
- Finds available ports (incrementing from defaults if in use)
- Displays the assigned URLs before starting
Available commands (run from repo root):
| Command | Description |
|---------|-------------|
| `make up` | Start services (foreground) |
| `make up-detached` | Start services (background) |
| `make down` | Stop all services |
| `make ps` | Show running containers |
| `make logs` | Follow container logs |
| `make nuke` | Stop, remove volumes & local images |
From a subdirectory, use: `make -C $(git rev-parse --show-toplevel) up`
**Important**: Always use these commands instead of plain `docker compose down`, which won't know the correct project name.
## Developing
While running, the container server will reload on modification of the Superset Python and JavaScript source code.
@@ -139,6 +139,39 @@ docker volume rm superset_db_home
docker-compose up
```
### Running multiple instances
If you need to run multiple Superset clones simultaneously (e.g., testing different branches),
use `make up` instead of `docker compose up`:
```bash
make up
```
This automatically:
- Generates a unique project name from your directory name
- Finds available ports (incrementing from 8088, 9000, etc. if already in use)
- Displays the assigned URLs before starting
Each clone gets isolated containers and volumes, so you can run them side-by-side without conflicts.
Available commands (run from repo root):
| Command | Description |
|---------|-------------|
| `make up` | Start services (foreground) |
| `make up-detached` | Start services (background) |
| `make down` | Stop all services |
| `make ps` | Show running containers |
| `make logs` | Follow container logs |
| `make nuke` | Stop, remove volumes & local images |
From a subdirectory, use: `make -C $(git rev-parse --show-toplevel) up`
:::warning
Always use these commands instead of plain `docker compose down`, which won't know the correct project name for your instance.
:::
## GitHub Codespaces (Cloud Development)
GitHub Codespaces provides a complete, pre-configured development environment in the cloud. This is ideal for:
@@ -138,19 +138,6 @@ The diagram shows:
3. **The host application** implements the APIs and manages extensions
4. **Extensions** integrate seamlessly with the host through well-defined interfaces
### Extension Dependencies
Extensions can depend on any combination of packages based on their needs. For example:
**Frontend-only extension** (e.g., a custom chart type):
- Depends on `@apache-superset/core` for UI components and React APIs
**Full-stack extension** (e.g., a custom SQL editor with new API endpoints):
- Depends on `@apache-superset/core` for frontend components
- Depends on `apache-superset-core` for backend APIs and models
This modular approach allows extension authors to choose exactly what they need while promoting consistency and reusability.
## Dynamic Module Loading
One of the most sophisticated aspects of the extension architecture is how frontend code is dynamically loaded at runtime using Webpack's Module Federation.
@@ -248,6 +235,7 @@ This architecture provides several key benefits:
Now that you understand the architecture, explore:
- **[Dependencies](./dependencies)** - Managing dependencies and understanding API stability
- **[Quick Start](./quick-start)** - Build your first extension
- **[Contribution Types](./contribution-types)** - What kinds of extensions you can build
- **[Development](./development)** - Project structure, APIs, and development workflow
@@ -1,6 +1,6 @@
---
title: Contribution Types
sidebar_position: 4
sidebar_position: 5
---
<!--
@@ -0,0 +1,166 @@
---
title: Dependencies
sidebar_position: 4
---
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Dependencies
This guide explains how to manage dependencies in your Superset extensions, including the difference between public APIs and internal code, and best practices for maintaining stable extensions.
## Core Packages vs Internal Code
Extensions run in the same context as Superset during runtime. This means extension developers can technically import any module from the Superset codebase, not just the public APIs. Understanding the distinction between public and internal code is critical for building maintainable extensions.
### Public APIs (Stable)
The core packages follow [semantic versioning](https://semver.org/) and provide stable, documented APIs:
| Package | Language | Description |
|---------|----------|-------------|
| `@apache-superset/core` | JavaScript/TypeScript | Frontend APIs, UI components, hooks, and utilities |
| `apache-superset-core` | Python | Backend APIs, models, DAOs, and utilities |
**Benefits of using core packages:**
- **Semantic versioning**: Breaking changes are communicated through version numbers
- **Documentation**: APIs are documented with clear usage examples
- **Stability commitment**: We strive to maintain backward compatibility
- **Type safety**: Full TypeScript and Python type definitions
### Internal Code (Unstable)
Any code that is not exported through the core packages is considered internal. This includes:
- Direct imports from `superset-frontend/src/` modules
- Direct imports from `superset/` Python modules (outside of `superset_core`)
- Undocumented functions, classes, or utilities
:::warning Use at Your Own Risk
Internal code can change at any time without notice. If you depend on internal modules, your extension may break when Superset is upgraded. There is no guarantee of backward compatibility for internal code.
:::
**Example of internal vs public imports:**
```typescript
// ✅ Public API - stable
import { Button, sqlLab } from '@apache-superset/core';
// ❌ Internal code - may break without notice
import { someInternalFunction } from 'src/explore/components/SomeComponent';
```
```python
# ✅ Public API - stable
from superset_core.api.models import Database
from superset_core.api.daos import DatabaseDAO
# ❌ Internal code - may break without notice
from superset.views.core import SomeInternalClass
```
## API Evolution
The core packages are still evolving. While we follow semantic versioning, the APIs may change as we add new extension points and refine existing ones based on community feedback.
**What this means for extension developers:**
- Check the release notes when upgrading Superset
- Test your extensions against new Superset versions before deploying
- Participate in discussions about API changes to influence the direction
- In some cases, using internal dependencies may be acceptable while the public API is being developed for your use case
### When Internal Dependencies May Be Acceptable
While public APIs are always preferred, there are situations where using internal code may be reasonable:
1. **Missing functionality**: The public API doesn't yet expose what you need
2. **Prototype/experimental extensions**: You're exploring capabilities before committing to a stable implementation
3. **Bridge period**: You need functionality that's planned for the public API but not yet released
In these cases, document your internal dependencies clearly and plan to migrate to public APIs when they become available.
## Core Library Dependencies
An important architectural principle of the Superset extension system is that **we do not provide abstractions on top of core dependencies** like React (frontend) or SQLAlchemy (backend).
### Why We Don't Abstract Core Libraries
Abstracting libraries like React or SQLAlchemy would:
- Create maintenance overhead keeping abstractions in sync with upstream
- Limit access to the full power of these libraries
- Add unnecessary abstraction layers
- Fragment the ecosystem with Superset-specific variants
### Depending on Core Libraries Directly
Extension developers should depend on and use core libraries directly:
**Frontend (examples):**
- [React](https://react.dev/) - UI framework
- [Ant Design](https://ant.design/) - UI component library (prefer Superset components from `@apache-superset/core/ui` when available to preserve visual consistency)
- [Emotion](https://emotion.sh/) - CSS-in-JS styling
- ...
**Backend (examples):**
- [SQLAlchemy](https://www.sqlalchemy.org/) - Database toolkit
- [Flask](https://flask.palletsprojects.com/) - Web framework
- [Flask-AppBuilder](https://flask-appbuilder.readthedocs.io/) - Application framework
- ...
:::info Version Compatibility
When Superset upgrades its core dependencies (e.g., a new major version of Ant Design or SQLAlchemy), extension developers should upgrade their extensions accordingly. This ensures compatibility and access to the latest features and security fixes.
:::
## API Versioning and Changelog
Once the extensions API reaches **v1**, we will maintain a dedicated `CHANGELOG.md` file to track all changes to the public APIs. This will include:
- New APIs and features
- Deprecation notices
- Breaking changes with migration guides
- Bug fixes affecting API behavior
Until then, monitor the Superset release notes and test your extensions with each new release.
## Best Practices
### Do
- **Prefer public APIs**: Always check if functionality exists in `@apache-superset/core` or `apache-superset-core` before using internal code
- **Pin versions**: Specify compatible Superset versions in your extension metadata
- **Test upgrades**: Verify your extension works with new Superset releases before deploying
- **Report missing APIs**: If you need functionality not in the public API, open a GitHub issue to request it
- **Use core libraries directly**: Leverage Ant Design, SQLAlchemy, and other core libraries directly
### Don't
- **Assume stability of internal code**: Internal modules can change or be removed in any release
- **Depend on implementation details**: Even if something works, it may not be supported
- **Skip upgrade testing**: Always test your extension against new Superset versions
- **Expect abstractions**: Use core dependencies directly rather than expecting Superset-specific abstractions
## Next Steps
- **[Architecture](./architecture)** - Understand the extension system design
- **[Development](./development)** - Learn about APIs and development workflow
- **[Quick Start](./quick-start)** - Build your first extension
@@ -1,6 +1,6 @@
---
title: Deployment
sidebar_position: 6
sidebar_position: 7
---
<!--
@@ -1,6 +1,6 @@
---
title: Development
sidebar_position: 5
sidebar_position: 6
---
<!--
+1 -1
View File
@@ -1,7 +1,7 @@
---
title: MCP Integration
hide_title: true
sidebar_position: 7
sidebar_position: 8
version: 1
---
@@ -45,6 +45,7 @@ Extensions can provide:
- **[Quick Start](./quick-start)** - Build your first extension with a complete walkthrough
- **[Architecture](./architecture)** - Design principles and system overview
- **[Dependencies](./dependencies)** - Managing dependencies and understanding API stability
- **[Contribution Types](./contribution-types)** - Available extension points
- **[Development](./development)** - Project structure, APIs, and development workflow
- **[Deployment](./deployment)** - Packaging and deploying extensions
+12 -5
View File
@@ -1,6 +1,6 @@
---
title: Community Extensions
sidebar_position: 9
sidebar_position: 10
---
<!--
@@ -28,10 +28,17 @@ This page serves as a registry of community-created Superset extensions. These e
## Extensions
| Name | Description | Author | Preview |
| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Extensions API Explorer](https://github.com/michael-s-molina/superset-extensions/tree/main/api_explorer) | A SQL Lab panel that demonstrates the Extensions API by providing an interactive explorer for testing commands like getTabs, getCurrentTab, and getDatabases. Useful for extension developers to understand and experiment with the available APIs. | Michael S. Molina | <a href="/img/extensions/api_explorer.png" target="_blank"><img src="/img/extensions/api_explorer.png" alt="Extensions API Explorer" width="120" /></a> |
| [SQL Query Flow Visualizer](https://github.com/msyavuz/superset-sql-visualizer) | A SQL Lab panel that transforms SQL queries into interactive flow diagrams, helping developers and analysts understand query execution paths and data relationships.| Mehmet Salih Yavuz | <a href="/img/extensions/sql_flow_visualizer.png" target="_blank"><img src="/img/extensions/sql_flow_visualizer.png" alt="SQL Flow Visualizer" width="120" /></a> |
| Name | Description | Author | Preview |
| ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Extensions API Explorer](https://github.com/michael-s-molina/superset-extensions/tree/main/api_explorer) | A SQL Lab panel that demonstrates the Extensions API by providing an interactive explorer for testing commands like getTabs, getCurrentTab, and getDatabases. Useful for extension developers to understand and experiment with the available APIs. | Michael S. Molina | <a href="/img/extensions/api-explorer.png" target="_blank"><img src="/img/extensions/api-explorer.png" alt="Extensions API Explorer" width="120" /></a> |
| [SQL Query Flow Visualizer](https://github.com/msyavuz/superset-sql-visualizer) | A SQL Lab panel that transforms SQL queries into interactive flow diagrams, helping developers and analysts understand query execution paths and data relationships. | Mehmet Salih Yavuz | <a href="/img/extensions/sql-flow-visualizer.png" target="_blank"><img src="/img/extensions/sql-flow-visualizer.png" alt="SQL Flow Visualizer" width="120" /></a> |
| [SQL Lab Export to Google Sheets](https://github.com/michael-s-molina/superset-extensions/tree/main/sqllab_gsheets) | A Superset extension that allows users to export SQL Lab query results directly to Google Sheets. | Michael S. Molina | <a href="/img/extensions/gsheets-export.png" target="_blank"><img src="/img/extensions/gsheets-export.png" alt="SQL Lab Export to Google Sheets" width="120" /></a> |
| [SQL Lab Export to Parquet](https://github.com/rusackas/superset-extensions/tree/main/sqllab_parquet) | Export SQL Lab query results directly to Apache Parquet format with Snappy compression. | Evan Rusackas | <a href="/img/extensions/parquet-export.png" target="_blank"><img src="/img/extensions/parquet-export.png" alt="SQL Lab Export to Parquet" width="120" /></a> |
| [SQL Lab Query Comparison](https://github.com/michael-s-molina/superset-extensions/tree/main/query_comparison) | A SQL Lab extension that enables side-by-side comparison of query results across different tabs, with GitHub-style diff visualization showing added/removed rows and columns. | Michael S. Molina | <a href="/img/extensions/query-comparison.png" target="_blank"><img src="/img/extensions/query-comparison.png" alt="Query Comparison" width="120" /></a> |
| [SQL Lab Result Stats](https://github.com/michael-s-molina/superset-extensions/tree/main/result_stats) | A SQL Lab extension that automatically computes statistics for query results, providing type-aware analysis including numeric metrics (min, max, mean, median, std dev), string analysis (length, empty counts), and date range information. | Michael S. Molina | <a href="/img/extensions/result-stats.png" target="_blank"><img src="/img/extensions/result-stats.png" alt="Result Stats" width="120" /></a> |
| [SQL Snippets](https://github.com/michael-s-molina/superset-extensions/tree/main/sql_snippets) | A SQL Lab extension that provides reusable SQL code snippets, enabling quick insertion of commonly used code blocks such as license headers, author information, and frequently used SQL patterns. | Michael S. Molina | <a href="/img/extensions/sql-snippets.png" target="_blank"><img src="/img/extensions/sql-snippets.png" alt="SQL Snippets" width="120" /></a> |
| [SQL Lab Query Estimator](https://github.com/michael-s-molina/superset-extensions/tree/main/query_estimator) | A SQL Lab panel that analyzes query execution plans to estimate resource impact, detect performance issues like Cartesian products and high-cost operations, and visualize the query plan tree. | Michael S. Molina | <a href="/img/extensions/query-estimator.png" target="_blank"><img src="/img/extensions/query-estimator.png" alt="Query Estimator" width="120" /></a> |
## How to Add Your Extension
To add your extension to this registry, submit a pull request to the [Apache Superset repository](https://github.com/apache/superset) with the following changes:
+1 -1
View File
@@ -1,6 +1,6 @@
---
title: Security
sidebar_position: 8
sidebar_position: 9
---
<!--
+1
View File
@@ -36,6 +36,7 @@ module.exports = {
'extensions/overview',
'extensions/quick-start',
'extensions/architecture',
'extensions/dependencies',
'extensions/contribution-types',
{
type: 'category',
+8
View File
@@ -429,6 +429,14 @@ const config: Config = {
label: 'Stack Overflow',
href: 'https://stackoverflow.com/questions/tagged/apache-superset',
},
{
label: 'Community Calendar',
href: '/community#superset-community-calendar',
},
{
label: 'In the Wild',
href: '/inTheWild',
},
],
},
...dynamicNavbarItems,
+10 -7
View File
@@ -51,9 +51,11 @@
"@storybook/preview-api": "^8.6.11",
"@storybook/theming": "^8.6.11",
"@superset-ui/core": "^0.20.4",
"antd": "^6.1.0",
"caniuse-lite": "^1.0.30001760",
"antd": "^6.1.2",
"caniuse-lite": "^1.0.30001762",
"docusaurus-plugin-less": "^2.0.2",
"js-yaml": "^4.1.1",
"js-yaml-loader": "^1.2.2",
"json-bigint": "^1.0.0",
"less": "^4.5.1",
"less-loader": "^12.3.0",
@@ -64,7 +66,7 @@
"react-svg-pan-zoom": "^3.13.1",
"remark-import-partial": "^0.0.2",
"reselect": "^5.1.1",
"storybook": "^8.6.11",
"storybook": "^8.6.15",
"swagger-ui-react": "^5.31.0",
"tinycolor2": "^1.4.2",
"ts-loader": "^9.5.4",
@@ -74,18 +76,19 @@
"@docusaurus/module-type-aliases": "^3.9.1",
"@docusaurus/tsconfig": "^3.9.2",
"@eslint/js": "^9.39.2",
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^8.37.0",
"@typescript-eslint/parser": "^8.49.0",
"@typescript-eslint/parser": "^8.51.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.3",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.5.0",
"globals": "^17.0.0",
"prettier": "^3.7.4",
"typescript": "~5.9.3",
"typescript-eslint": "^8.49.0",
"webpack": "^5.103.0"
"typescript-eslint": "^8.50.1",
"webpack": "^5.104.1"
},
"browserslist": {
"production": [
+119 -57
View File
@@ -49,9 +49,23 @@ const BADGE_PATH_PATTERNS = [
// Cache for downloaded badges (persists across files in a single build)
const badgeCache = new Map();
// Track in-flight downloads to prevent duplicate concurrent requests
const inFlightDownloads = new Map();
// Track if we've already ensured the badges directory exists
let badgesDirCreated = false;
// Retry configuration
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 1000;
/**
* Sleep for a given number of milliseconds
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Generate a stable filename for a badge URL
*/
@@ -74,21 +88,61 @@ function isBadgeUrl(url) {
try {
const parsed = new URL(url);
// Check if it's from a known badge domain
if (BADGE_DOMAINS.some((domain) => parsed.hostname.includes(domain))) {
if (BADGE_DOMAINS.some(domain => parsed.hostname.includes(domain))) {
return true;
}
// Check if it matches a badge path pattern
return BADGE_PATH_PATTERNS.some((pattern) => pattern.test(url));
return BADGE_PATH_PATTERNS.some(pattern => pattern.test(url));
} catch {
return false;
}
}
/**
* Fetch a badge with retry logic
*/
async function fetchWithRetry(url, retries = MAX_RETRIES) {
let lastError;
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch(url, {
headers: {
// Some services need a user agent
'User-Agent': 'Mozilla/5.0 (compatible; DocusaurusBuild/1.0)',
Accept: 'image/svg+xml,image/*,*/*',
},
// Follow redirects
redirect: 'follow',
// Add timeout to prevent hanging
signal: AbortSignal.timeout(30000),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response;
} catch (error) {
lastError = error;
if (attempt < retries) {
const delay = RETRY_DELAY_MS * attempt; // Exponential backoff
console.log(
`[remark-localize-badges] Retry ${attempt}/${retries} for ${url} after ${delay}ms...`,
);
await sleep(delay);
}
}
}
throw lastError;
}
/**
* Download a badge and return the local path
*/
async function downloadBadge(url, staticDir) {
// Check cache first
// Check memory cache first
if (badgeCache.has(url)) {
return badgeCache.get(url);
}
@@ -105,58 +159,67 @@ async function downloadBadge(url, staticDir) {
const localPath = path.join(badgesDir, filename);
const webPath = `/badges/${filename}`;
// Check if already downloaded in a previous build
// Check if already downloaded in a previous build or by another concurrent request
if (fs.existsSync(localPath)) {
badgeCache.set(url, webPath);
return webPath;
}
console.log(`[remark-localize-badges] Downloading: ${url}`);
try {
const response = await fetch(url, {
headers: {
// Some services need a user agent
'User-Agent': 'Mozilla/5.0 (compatible; DocusaurusBuild/1.0)',
Accept: 'image/svg+xml,image/*,*/*',
},
// Follow redirects
redirect: 'follow',
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get('content-type') || '';
const content = await response.text();
// Validate it's actually an SVG or image
if (
!contentType.includes('svg') &&
!contentType.includes('image') &&
!content.trim().startsWith('<svg') &&
!content.trim().startsWith('<?xml')
) {
throw new Error(
`Invalid content type: ${contentType}. Expected SVG image.`,
);
}
// Write the badge to disk
fs.writeFileSync(localPath, content, 'utf8');
console.log(`[remark-localize-badges] Saved: ${filename}`);
badgeCache.set(url, webPath);
return webPath;
} catch (error) {
// Fail the build on badge download failure
throw new Error(
`[remark-localize-badges] Failed to download badge: ${url}\n` +
`Error: ${error.message}\n` +
`Build cannot continue with broken badges. Please fix the badge URL or remove it.`,
);
// Check if there's already an in-flight download for this URL
// This prevents duplicate concurrent downloads of the same badge
if (inFlightDownloads.has(url)) {
return inFlightDownloads.get(url);
}
// Create the download promise and store it
const downloadPromise = (async () => {
// Double-check file existence after acquiring the "lock"
if (fs.existsSync(localPath)) {
badgeCache.set(url, webPath);
return webPath;
}
console.log(`[remark-localize-badges] Downloading: ${url}`);
try {
const response = await fetchWithRetry(url);
const contentType = response.headers.get('content-type') || '';
const content = await response.text();
// Validate it's actually an SVG or image
if (
!contentType.includes('svg') &&
!contentType.includes('image') &&
!content.trim().startsWith('<svg') &&
!content.trim().startsWith('<?xml')
) {
throw new Error(
`Invalid content type: ${contentType}. Expected SVG image.`,
);
}
// Write the badge to disk
fs.writeFileSync(localPath, content, 'utf8');
console.log(`[remark-localize-badges] Saved: ${filename}`);
badgeCache.set(url, webPath);
return webPath;
} catch (error) {
// Fail the build on badge download failure
throw new Error(
`[remark-localize-badges] Failed to download badge: ${url}\n` +
`Error: ${error.message}\n` +
`Build cannot continue with broken badges. Please fix the badge URL or remove it.`,
);
} finally {
// Clean up the in-flight tracker
inFlightDownloads.delete(url);
}
})();
inFlightDownloads.set(url, downloadPromise);
return downloadPromise;
}
/**
@@ -168,15 +231,14 @@ export default function remarkLocalizeBadges(options = {}) {
const docsRoot = path.resolve(currentDir, '..');
const staticDir = options.staticDir || path.join(docsRoot, 'static');
return async function transformer(tree) {
const promises = [];
// Find all image nodes
visit(tree, 'image', (node) => {
visit(tree, 'image', node => {
if (isBadgeUrl(node.url)) {
promises.push(
downloadBadge(node.url, staticDir).then((localPath) => {
downloadBadge(node.url, staticDir).then(localPath => {
node.url = localPath;
}),
);
@@ -184,7 +246,7 @@ export default function remarkLocalizeBadges(options = {}) {
});
// Also handle HTML img tags in raw HTML or JSX
visit(tree, ['html', 'jsx'], (node) => {
visit(tree, ['html', 'jsx'], node => {
if (!node.value) return;
// Find img src attributes pointing to badge URLs
@@ -195,7 +257,7 @@ export default function remarkLocalizeBadges(options = {}) {
const url = match[1];
if (isBadgeUrl(url)) {
promises.push(
downloadBadge(url, staticDir).then((localPath) => {
downloadBadge(url, staticDir).then(localPath => {
node.value = node.value.replace(url, localPath);
}),
);
@@ -204,12 +266,12 @@ export default function remarkLocalizeBadges(options = {}) {
});
// Also handle markdown link images: [![alt](img-url)](link-url)
visit(tree, 'link', (node) => {
visit(tree, 'link', node => {
if (node.children) {
node.children.forEach((child) => {
node.children.forEach(child => {
if (child.type === 'image' && isBadgeUrl(child.url)) {
promises.push(
downloadBadge(child.url, staticDir).then((localPath) => {
downloadBadge(child.url, staticDir).then(localPath => {
child.url = localPath;
}),
);
+1
View File
@@ -72,6 +72,7 @@ const sidebars = {
'extensions/overview',
'extensions/quick-start',
'extensions/architecture',
'extensions/dependencies',
'extensions/contribution-types',
{
type: 'category',
+165
View File
@@ -0,0 +1,165 @@
/**
* 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 Layout from '@theme/Layout';
import { Avatar, Card, Col, Collapse, Row, Typography } from 'antd';
import BlurredSection from '../components/BlurredSection';
import SectionHeader from '../components/SectionHeader';
import DataSet from '../../../RESOURCES/INTHEWILD.yaml';
const { Text, Link } = Typography;
interface Organization {
name: string;
url: string;
logo?: string;
contributors?: string[];
}
interface DataSetType {
categories: Record<string, Organization[]>;
}
const typedDataSet = DataSet as DataSetType;
const ContributorAvatars = ({ contributors }: { contributors?: string[] }) => {
if (!contributors?.length) return null;
return (
<Avatar.Group size="small" max={{ count: 3 }}>
{contributors.map((handle) => {
const username = handle.replace('@', '');
return (
<a
key={username}
href={`https://github.com/${username}`}
target="_blank"
rel="noreferrer"
onClick={(e) => e.stopPropagation()}
>
<Avatar
src={`https://github.com/${username}.png?size=40`}
alt={username}
style={{ cursor: 'pointer' }}
>
{username.charAt(0).toUpperCase()}
</Avatar>
</a>
);
})}
</Avatar.Group>
);
};
export default function InTheWild() {
return (
<Layout title="In the Wild" description="Organizations using Apache Superset">
<main>
<BlurredSection>
<SectionHeader
level="h2"
title="In the Wild"
subtitle="See who's using Superset and join our growing community"
/>
<div style={{ textAlign: 'center', marginTop: 10 }}>
<Link
href="https://github.com/apache/superset/edit/master/RESOURCES/INTHEWILD.yaml"
target="_blank"
>
Add your name/org!
</Link>
</div>
</BlurredSection>
<div style={{ maxWidth: 850, margin: '70px auto 60px', padding: '0 20px' }}>
<Collapse
bordered={false}
defaultActiveKey={Object.keys(typedDataSet.categories)}
style={{
background: 'var(--ifm-background-color)',
border: '1px solid var(--ifm-border-color)',
borderRadius: 10,
}}
items={Object.entries(typedDataSet.categories).map(([category, items]) => {
const logoItems = items.filter(({ logo }) => logo?.trim());
const textItems = items.filter(({ logo }) => !logo?.trim());
return {
key: category,
label: (
<Text strong style={{ fontSize: 16, lineHeight: '22px' }}>
{category} ({items.length})
</Text>
),
children: (
<>
{logoItems.length > 0 && (
<Row gutter={[16, 16]} style={{ marginBottom: textItems.length > 0 ? 24 : 0 }}>
{logoItems.map(({ name, url, logo, contributors }) => (
<Col xs={24} sm={12} md={8} key={name}>
<a href={url} target="_blank" rel="noreferrer">
<Card
hoverable
style={{ height: 150, position: 'relative' }}
styles={{ body: { padding: 16, height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' } }}
>
<img
src={`/img/logos/${logo}`}
alt={name}
style={{ maxHeight: 80, maxWidth: '100%', objectFit: 'contain' }}
/>
{contributors?.length && (
<div style={{ position: 'absolute', bottom: 8, right: 8 }}>
<ContributorAvatars contributors={contributors} />
</div>
)}
</Card>
</a>
</Col>
))}
</Row>
)}
{textItems.length > 0 && (
<Row gutter={[8, 8]}>
{textItems.map(({ name, url, contributors }) => (
<Col xs={24} sm={12} md={8} key={name}>
<a href={url} target="_blank" rel="noreferrer">
<Card
size="small"
hoverable
styles={{ body: { padding: '8px 12px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } }}
>
<Text ellipsis style={{ flex: 1 }}>{name}</Text>
<ContributorAvatars contributors={contributors} />
</Card>
</a>
</Col>
))}
</Row>
)}
</>
),
};
})}
/>
</div>
</main>
</Layout>
);
}
+103 -1
View File
@@ -19,15 +19,43 @@
import { useRef, useState, useEffect, JSX } from 'react';
import Layout from '@theme/Layout';
import Link from '@docusaurus/Link';
import { Carousel } from 'antd';
import { Card, Carousel, Flex } from 'antd';
import styled from '@emotion/styled';
import GitHubButton from 'react-github-btn';
import { mq } from '../utils';
import { Databases } from '../resources/data';
import SectionHeader from '../components/SectionHeader';
import BlurredSection from '../components/BlurredSection';
import DataSet from '../../../RESOURCES/INTHEWILD.yaml';
import '../styles/main.less';
interface Organization {
name: string;
url: string;
logo?: string;
}
interface DataSetType {
categories: Record<string, Organization[]>;
}
const typedDataSet = DataSet as DataSetType;
// Extract all organizations with logos for the carousel
const companiesWithLogos = Object.values(typedDataSet.categories)
.flat()
.filter((org) => org.logo?.trim());
// Fisher-Yates shuffle for fair randomization
function shuffleArray<T>(array: T[]): T[] {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
const features = [
{
image: 'powerful-yet-easy.jpg',
@@ -452,6 +480,7 @@ export default function Home(): JSX.Element {
const slider = useRef(null);
const [slideIndex, setSlideIndex] = useState(0);
const [shuffledCompanies, setShuffledCompanies] = useState(companiesWithLogos);
const onChange = (current, next) => {
setSlideIndex(next);
@@ -479,6 +508,11 @@ export default function Home(): JSX.Element {
}
};
// Shuffle companies on mount for fair rotation
useEffect(() => {
setShuffledCompanies(shuffleArray(companiesWithLogos));
}, []);
// Set up dark <-> light navbar change
useEffect(() => {
changeToDark();
@@ -747,6 +781,74 @@ export default function Home(): JSX.Element {
</span>
</StyledIntegrations>
</BlurredSection>
{/* Only show carousel when we have enough logos (>10) for a good display */}
{companiesWithLogos.length > 10 && (
<BlurredSection>
<div style={{ padding: '0 20px' }}>
<SectionHeader
level="h2"
title="Trusted by teams everywhere"
subtitle="Join thousands of companies using Superset to explore and visualize their data"
/>
<div style={{ maxWidth: 1160, margin: '25px auto 0' }}>
<Carousel
autoplay
autoplaySpeed={2000}
slidesToShow={6}
slidesToScroll={1}
dots={false}
responsive={[
{ breakpoint: 1024, settings: { slidesToShow: 4 } },
{ breakpoint: 768, settings: { slidesToShow: 3 } },
{ breakpoint: 480, settings: { slidesToShow: 2 } },
]}
>
{shuffledCompanies.map(({ name, url, logo }) => (
<div key={name}>
<a
href={url}
target="_blank"
rel="noreferrer"
aria-label={`Visit ${name}`}
>
<Card
style={{ margin: '0 8px' }}
styles={{
body: {
height: 80,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 16,
},
}}
>
<img
src={`/img/logos/${logo}`}
alt={name}
title={name}
style={{ maxHeight: 48, maxWidth: '100%', objectFit: 'contain' }}
/>
</Card>
</a>
</div>
))}
</Carousel>
</div>
<Flex justify="center" style={{ marginTop: 30, fontSize: 17 }}>
<Link to="/inTheWild">See all companies</Link>
<span style={{ margin: '0 8px' }}>·</span>
<a
href="https://github.com/apache/superset/edit/master/RESOURCES/INTHEWILD.yaml"
target="_blank"
rel="noreferrer"
>
Add yours to the list!
</a>
</Flex>
</div>
</BlurredSection>
)}
</StyledMain>
</Layout>
);
+218 -20
View File
@@ -19,6 +19,17 @@
import { useEffect } from 'react';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
// File extensions to track as downloads
const DOWNLOAD_EXTENSIONS = [
'pdf', 'zip', 'tar', 'gz', 'tgz', 'bz2',
'exe', 'dmg', 'pkg', 'deb', 'rpm',
'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
'csv', 'json', 'yaml', 'yml',
];
// Scroll depth milestones to track
const SCROLL_MILESTONES = [25, 50, 75, 100];
export default function Root({ children }) {
const { siteConfig } = useDocusaurusContext();
const { customFields } = siteConfig;
@@ -27,10 +38,9 @@ export default function Root({ children }) {
const { matomoUrl, matomoSiteId } = customFields;
if (typeof window !== 'undefined') {
// Making testing easier, logging debug junk if we're in development
const devMode = window.location.hostname === 'localhost' ? true : false;
const devMode = ['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(window.location.hostname);
// Initialize the _paq array first
// Initialize the _paq array
window._paq = window._paq || [];
// Configure the tracker before loading matomo.js
@@ -39,7 +49,8 @@ export default function Root({ children }) {
window._paq.push(['setTrackerUrl', `${matomoUrl}/matomo.php`]);
window._paq.push(['setSiteId', matomoSiteId]);
// Initial page view is handled by handleRouteChange
// Track downloads with custom extensions
window._paq.push(['setDownloadExtensions', DOWNLOAD_EXTENSIONS.join('|')]);
// Now load the matomo.js script
const script = document.createElement('script');
@@ -47,19 +58,168 @@ export default function Root({ children }) {
script.src = `${matomoUrl}/matomo.js`;
document.head.appendChild(script);
// Helper to track events
const trackEvent = (category, action, name, value) => {
if (devMode) {
console.log('Matomo trackEvent:', { category, action, name, value });
}
window._paq.push(['trackEvent', category, action, name, value]);
};
// Helper to track site search
const trackSiteSearch = (keyword, category, resultsCount) => {
if (devMode) {
console.log('Matomo trackSiteSearch:', { keyword, category, resultsCount });
}
window._paq.push(['trackSiteSearch', keyword, category, resultsCount]);
};
// Track external link clicks using domain as category (vendor-agnostic)
const handleLinkClick = (event) => {
const link = event.target.closest('a');
if (!link) return;
const href = link.getAttribute('href');
if (!href) return;
try {
const url = new URL(href, window.location.origin);
// Skip internal links
if (url.hostname === window.location.hostname) return;
// Use hostname as category for vendor-agnostic tracking
trackEvent('Outbound Link', url.hostname, href);
} catch {
// Invalid URL, skip tracking
}
};
// Track Algolia search queries
const setupAlgoliaTracking = () => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const searchInput = node.querySelector?.('.DocSearch-Input') ||
(node.classList?.contains('DocSearch-Input') ? node : null);
if (searchInput) {
let debounceTimer;
searchInput.addEventListener('input', (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const query = e.target.value.trim();
if (query.length >= 3) {
const results = document.querySelectorAll('.DocSearch-Hit');
trackSiteSearch(query, 'Documentation', results.length);
}
}, 1000);
});
}
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
return observer;
};
// Track video plays
const handleVideoPlay = (event) => {
if (event.target.tagName === 'VIDEO') {
const videoSrc = event.target.currentSrc || event.target.src || 'unknown';
trackEvent('Video', 'Play', videoSrc);
}
};
// Track CTA button clicks
const handleCTAClick = (event) => {
const button = event.target.closest('.get-started-button, .default-button-theme');
if (button) {
const buttonText = button.textContent?.trim() || 'Unknown';
const href = button.getAttribute('href') || '';
trackEvent('CTA', 'Click', `${buttonText} - ${href}`);
}
};
// Track scroll depth
let scrollMilestonesReached = new Set();
const handleScroll = () => {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
if (docHeight <= 0) return;
const scrollPercent = Math.round((scrollTop / docHeight) * 100);
SCROLL_MILESTONES.forEach(milestone => {
if (scrollPercent >= milestone && !scrollMilestonesReached.has(milestone)) {
scrollMilestonesReached.add(milestone);
trackEvent('Scroll Depth', `${milestone}%`, window.location.pathname);
}
});
};
// Reset scroll tracking on route change
const resetScrollTracking = () => {
scrollMilestonesReached = new Set();
};
// Track 404 pages
const track404 = () => {
const is404 = document.querySelector('.theme-doc-404') ||
document.title.toLowerCase().includes('not found') ||
document.querySelector('h1')?.textContent?.toLowerCase().includes('not found');
if (is404) {
trackEvent('Error', '404', window.location.pathname);
if (devMode) {
console.log('Matomo: 404 page detected', window.location.pathname);
}
}
};
// Track copy-to-clipboard events on code blocks
const handleCopy = (event) => {
const codeBlock = event.target.closest('pre, code, .prism-code');
if (codeBlock) {
const codeText = window.getSelection()?.toString() || '';
const codeSnippet = codeText.substring(0, 100) + (codeText.length > 100 ? '...' : '');
trackEvent('Code', 'Copy', `${window.location.pathname}: ${codeSnippet}`);
}
};
// Track color mode preference (as event, no admin config needed)
const trackColorMode = () => {
const colorMode = document.documentElement.getAttribute('data-theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
trackEvent('User Preference', 'Color Mode', colorMode);
};
// Track docs version from URL (as event, no admin config needed)
const trackDocsVersion = () => {
const pathMatch = window.location.pathname.match(/\/docs\/([\d.]+)\//);
const version = pathMatch ? pathMatch[1] : 'latest';
trackEvent('User Preference', 'Docs Version', version);
};
// Handle route changes for SPA
const handleRouteChange = () => {
if (devMode) {
console.log('Route changed to:', window.location.pathname);
}
// Short timeout to ensure the page has fully rendered
// Reset scroll tracking for new page
resetScrollTracking();
setTimeout(() => {
// Get the current page title from the document
const currentTitle = document.title;
const currentPath = window.location.pathname;
// For testing: impersonate real domain - ONLY FOR DEVELOPMENT
// Set custom dimensions before tracking page view
trackColorMode();
trackDocsVersion();
if (devMode) {
console.log('Tracking page view:', currentPath, currentTitle);
window._paq.push(['setDomains', ['superset.apache.org']]);
@@ -74,10 +234,13 @@ export default function Root({ children }) {
window._paq.push(['setReferrerUrl', window.location.href]);
window._paq.push(['setDocumentTitle', currentTitle]);
window._paq.push(['trackPageView']);
}, 100); // Increased delay to ensure page has fully rendered
// Check for 404 after page renders
setTimeout(track404, 500);
}, 100);
};
// Try all possible Docusaurus events - they've changed between versions
// Set up Docusaurus route listeners
const possibleEvents = [
'docusaurus.routeDidUpdate',
'docusaurusRouteDidUpdate',
@@ -85,21 +248,22 @@ export default function Root({ children }) {
];
if (devMode) {
console.log('Setting up Docusaurus route listeners');
console.log('Setting up Matomo tracking with enhanced features');
}
possibleEvents.forEach(eventName => {
document.addEventListener(eventName, () => {
// Store handler references for proper cleanup
const routeHandlers = possibleEvents.map(eventName => {
const handler = () => {
if (devMode) {
console.log(`Docusaurus route update detected via ${eventName}`);
}
handleRouteChange();
});
};
document.addEventListener(eventName, handler);
return { eventName, handler };
});
// Also set up manual history tracking as fallback
if (devMode) {
console.log('Setting up manual history tracking as fallback');
}
// Manual history tracking as fallback
const originalPushState = window.history.pushState;
window.history.pushState = function () {
originalPushState.apply(this, arguments);
@@ -108,19 +272,53 @@ export default function Root({ children }) {
window.addEventListener('popstate', handleRouteChange);
// Set up event listeners
document.addEventListener('click', handleLinkClick);
document.addEventListener('click', handleCTAClick);
document.addEventListener('play', handleVideoPlay, true);
document.addEventListener('copy', handleCopy);
window.addEventListener('scroll', handleScroll, { passive: true });
// Watch for color mode changes
const colorModeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
trackEvent('User Preference', 'Color Mode Change',
document.documentElement.getAttribute('data-theme'));
}
});
});
colorModeObserver.observe(document.documentElement, { attributes: true });
// Set up Algolia tracking
const algoliaObserver = setupAlgoliaTracking();
// Initial page tracking
handleRouteChange();
// Cleanup
return () => {
// Cleanup listeners
possibleEvents.forEach(eventName => {
document.removeEventListener(eventName, handleRouteChange);
routeHandlers.forEach(({ eventName, handler }) => {
document.removeEventListener(eventName, handler);
});
if (originalPushState) {
window.history.pushState = originalPushState;
window.removeEventListener('popstate', handleRouteChange);
}
document.removeEventListener('click', handleLinkClick);
document.removeEventListener('click', handleCTAClick);
document.removeEventListener('play', handleVideoPlay, true);
document.removeEventListener('copy', handleCopy);
window.removeEventListener('scroll', handleScroll);
if (algoliaObserver) {
algoliaObserver.disconnect();
}
if (colorModeObserver) {
colorModeObserver.disconnect();
}
};
}
}, []);
+28
View File
@@ -0,0 +1,28 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
declare module '*.yaml' {
const content: unknown;
export default content;
}
declare module '*.yml' {
const content: unknown;
export default content;
}
+7
View File
@@ -25,6 +25,13 @@ export default function webpackExtendPlugin(): Plugin<void> {
name: 'custom-webpack-plugin',
configureWebpack(config) {
const isDev = process.env.NODE_ENV === 'development';
// Add YAML loader rule directly to existing rules
config.module?.rules?.push({
test: /\.ya?ml$/,
use: 'js-yaml-loader',
});
return {
devtool: isDev ? 'eval-source-map' : config.devtool,
...(isDev && {

Before

Width:  |  Height:  |  Size: 476 KiB

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

+19
View File
@@ -0,0 +1,19 @@
<!--
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.
-->
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 494.72 148.76"><defs><style>.cls-1{fill:#2fc096;}</style></defs><title>preset-logo</title><path class="cls-1" d="M91,51.77H43.11L0,145H27.81L59.3,77.14H91a12.69,12.69,0,0,0,0-25.37ZM91,0H61.28L49.51,25.37H91a39.08,39.08,0,0,1,0,78.16H74.8L63,128.84a12.1,12.1,0,0,0,1.21.06H91A64.45,64.45,0,0,0,91,0Z"/><path d="M205,38.46c-14.41,0-24.47,10.31-24.47,25.07V146a2.66,2.66,0,0,0,2.75,2.75h5.62a2.66,2.66,0,0,0,2.75-2.75V114.25a20.32,20.32,0,0,0,14.72,5.65c13.17,0,23.1-10.78,23.1-25.08V63.53C229.5,48.77,219.44,38.46,205,38.46ZM191.68,63.53c0-8.66,5.24-14.26,13.35-14.26s13.35,5.6,13.35,14.26V94.82c0,8.66-5.24,14.26-13.35,14.26s-13.35-5.6-13.35-14.26Z"/><path d="M272.15,39.52h-6.68c-14.53,0-25.08,10.35-25.08,24.62v51.49a2.66,2.66,0,0,0,2.75,2.75h5.62a2.67,2.67,0,0,0,2.76-2.75V64.14c0-8.51,5.34-13.8,14-13.8h6.68a2.67,2.67,0,0,0,2.76-2.76V42.27A2.66,2.66,0,0,0,272.15,39.52Z"/><path d="M304.2,86.64c14.17,0,24.47-9.85,24.47-23.41,0-14.5-10.07-24.63-24.47-24.63-14.64,0-24.47,10.14-24.47,25.24V94.67c0,15,9.83,25.08,24.47,25.08,14.06,0,23.84-10.06,24.32-25.08a2.66,2.66,0,0,0-2.76-2.75h-5.62a2.57,2.57,0,0,0-2.75,2.71c-.28,8.69-5.46,14.3-13.19,14.3-8.11,0-13.35-5.6-13.35-14.26v-8Zm-13.35-22.8c0-9,5-14.41,13.35-14.41,8.1,0,13.34,5.41,13.34,13.8,0,7.64-5.24,12.59-13.34,12.59H290.85Z"/><path d="M363.68,73.69c-8.11-3.42-15.12-6.37-15.12-13.5,0-6.53,5-10.92,12.44-10.92S373.13,53.94,373.58,62a2.59,2.59,0,0,0,2.76,2.75H382A2.66,2.66,0,0,0,384.7,62c-.67-14.94-9.47-23.5-24.16-23.5-13.17,0-23.11,9.34-23.11,21.73,0,14.52,11.48,19.4,21.6,23.7,8.38,3.56,15.62,6.64,15.62,14.27,0,6.94-4.54,10.92-12.44,10.92-9.28,0-14-4.68-14.41-14.26a2.58,2.58,0,0,0-2.75-2.75h-5.62a2.66,2.66,0,0,0-2.75,2.75v.05c.65,15.44,10.43,25,25.53,25,13.43,0,23.56-9.35,23.56-21.74C385.77,83,374,78.05,363.68,73.69Z"/><path d="M418.55,86.64c14.18,0,24.47-9.85,24.47-23.41C443,48.73,433,38.6,418.55,38.6c-14.64,0-24.47,10.14-24.47,25.24V94.67c0,15,9.83,25.08,24.47,25.08,14.07,0,23.84-10.06,24.32-25.08a2.66,2.66,0,0,0-2.75-2.75H434.5a2.57,2.57,0,0,0-2.75,2.71c-.29,8.69-5.47,14.3-13.2,14.3-8.11,0-13.35-5.6-13.35-14.26v-8ZM405.2,63.84c0-9,5-14.41,13.35-14.41,8.11,0,13.35,5.41,13.35,13.8,0,7.64-5.24,12.59-13.35,12.59H405.2Z"/><path d="M478.14,108.05h-2c-7.65,0-11.53-5.36-11.53-15.93V50.83h11.68a2.67,2.67,0,0,0,2.75-2.76V42.91a2.66,2.66,0,0,0-2.75-2.75H464.64V21.65a2.67,2.67,0,0,0-2.75-2.76h-5.62a2.67,2.67,0,0,0-2.75,2.76V92.12c0,16.75,8.46,26.75,22.65,26.75h2a2.65,2.65,0,0,0,2.75-2.75V110.8A2.58,2.58,0,0,0,478.14,108.05Z"/><path d="M484,27h-1.91V25.85h5.18V27h-1.91v5.17H484Z"/><path d="M493.41,29.77c0-1.07,0-2.27,0-3h0c-.3,1.28-.93,3.37-1.53,5.34h-1.16c-.46-1.72-1.11-4.1-1.38-5.36h0c.06.74.08,2,.08,3.11v2.25h-1.24V25.85h2c.49,1.64,1,3.7,1.23,4.63h0c.15-.82.84-3,1.37-4.63h2v6.28h-1.31Z"/></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

+64
View File
@@ -0,0 +1,64 @@
# Apache Superset
> Apache Superset is a modern, enterprise-ready business intelligence web application. It provides a no-code interface for building charts, a powerful SQL editor, support for nearly any SQL database, and beautiful visualizations. Superset is open source under the Apache 2.0 license.
Superset is designed for data exploration and visualization at scale. It features a lightweight semantic layer, configurable caching, extensible security with RBAC, a REST API for programmatic access, and a cloud-native architecture.
## Getting Started
- [Introduction](https://superset.apache.org/docs/intro): Overview of Superset features, supported databases, and resources
- [Quickstart](https://superset.apache.org/docs/quickstart): Get Superset running quickly with Docker Compose
## Installation
- [Architecture](https://superset.apache.org/docs/installation/architecture): Production deployment architecture and components
- [Docker Compose](https://superset.apache.org/docs/installation/docker-compose): Install Superset using Docker Compose
- [Docker Builds](https://superset.apache.org/docs/installation/docker-builds): Building and customizing Docker images
- [Kubernetes](https://superset.apache.org/docs/installation/kubernetes): Deploy Superset on Kubernetes with Helm
- [PyPI](https://superset.apache.org/docs/installation/pypi): Install from PyPI using pip
- [Upgrading Superset](https://superset.apache.org/docs/installation/upgrading-superset): Upgrade between Superset versions
## Configuration
- [Configuring Superset](https://superset.apache.org/docs/configuration/configuring-superset): Main configuration options and superset_config.py
- [Databases](https://superset.apache.org/docs/configuration/databases): Connect to databases with SQLAlchemy connection strings
- [Caching](https://superset.apache.org/docs/configuration/cache): Configure caching with Redis or other backends
- [Async Queries & Celery](https://superset.apache.org/docs/configuration/async-queries-celery): Set up async query execution with Celery
- [Alerts & Reports](https://superset.apache.org/docs/configuration/alerts-reports): Configure email/Slack alerts and scheduled reports
- [SQL Templating](https://superset.apache.org/docs/configuration/sql-templating): Use Jinja templates in SQL queries
- [Theming](https://superset.apache.org/docs/configuration/theming): Customize Superset's appearance
- [Networking Settings](https://superset.apache.org/docs/configuration/networking-settings): CORS, proxy, and network configuration
- [Timezones](https://superset.apache.org/docs/configuration/timezones): Timezone handling configuration
- [Event Logging](https://superset.apache.org/docs/configuration/event-logging): Configure event and analytics logging
- [Importing/Exporting Datasources](https://superset.apache.org/docs/configuration/importing-exporting-datasources): Import and export datasource definitions
## Using Superset
- [Creating Your First Dashboard](https://superset.apache.org/docs/using-superset/creating-your-first-dashboard): Step-by-step tutorial for building dashboards
- [Exploring Data](https://superset.apache.org/docs/using-superset/exploring-data): Guide to data exploration features
- [Issue Codes](https://superset.apache.org/docs/using-superset/issue-codes): Reference for Superset error codes
## Security
- [Security Overview](https://superset.apache.org/docs/security/security): RBAC, row-level security, and authentication options
- [Securing Superset](https://superset.apache.org/docs/security/securing_superset): Production security hardening guide
- [CVEs](https://superset.apache.org/docs/security/cves): Security vulnerabilities and advisories
## Contributing
- [Contributing Guide](https://superset.apache.org/docs/contributing/contributing): How to contribute to Apache Superset
- [Development Environment](https://superset.apache.org/docs/contributing/development): Set up a local development environment
- [Guidelines](https://superset.apache.org/docs/contributing/guidelines): Code style, testing, and PR guidelines
- [How-Tos](https://superset.apache.org/docs/contributing/howtos): Common development tasks and recipes
- [Resources](https://superset.apache.org/docs/contributing/resources): Additional contributor resources
## Reference
- [FAQ](https://superset.apache.org/docs/faq): Frequently asked questions
- [REST API](https://superset.apache.org/docs/api): Superset REST API documentation
## Optional
- [Country Map Tools](https://superset.apache.org/docs/configuration/country-map-tools): Custom country map visualizations
- [Map Tiles](https://superset.apache.org/docs/configuration/map-tiles): Configure map tile providers for deck.gl charts
- [Miscellaneous](https://superset.apache.org/docs/contributing/misc): Additional contributor information
+289 -183
View File
@@ -2762,19 +2762,19 @@
"@rc-component/util" "^1.2.1"
clsx "^2.1.1"
"@rc-component/form@~1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@rc-component/form/-/form-1.4.0.tgz#bee504c182bbb768b5fb68809e82b69deef9aec0"
integrity sha512-C8MN/2wIaW9hSrCCtJmcgCkWTQNIspN7ARXLFA4F8PGr8Qxk39U5pS3kRK51/bUJNhb/fEtdFnaViLlISGKI2A==
"@rc-component/form@~1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@rc-component/form/-/form-1.6.0.tgz#d2f9f6be1063886b9a0ccc5168a0411da6e0da15"
integrity sha512-A7vrN8kExtw4sW06mrsgCb1rowhvBFFvQU6Bk/NL0Fj6Wet/5GF0QnGCxBu/sG3JI9FEhsJWES0D44BW2d0hzg==
dependencies:
"@rc-component/async-validator" "^5.0.3"
"@rc-component/util" "^1.3.0"
"@rc-component/util" "^1.5.0"
clsx "^2.1.1"
"@rc-component/image@~1.5.2":
version "1.5.2"
resolved "https://registry.yarnpkg.com/@rc-component/image/-/image-1.5.2.tgz#46cd467466f8b5c9a682bbc96a04f15ad3688af6"
integrity sha512-SIbYLy0IrXqyhccpKktQEvpbBti/KwgG8V/E8GJa8ycwOQmuZaCP7b/C+eQlivn4KDWpfKfoOrLKHXmVlljDgg==
"@rc-component/image@~1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@rc-component/image/-/image-1.5.3.tgz#ea163e5b55303d548e3b2946e99bdcd9e7586299"
integrity sha512-/NR7QW9uCN8Ugar+xsHZOPvzPySfEhcW2/vLcr7VPRM+THZMrllMRv7LAUgW7ikR+Z67Ab67cgPp5K5YftpJsQ==
dependencies:
"@rc-component/motion" "^1.0.0"
"@rc-component/portal" "^2.0.0"
@@ -2870,10 +2870,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/picker@~1.8.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@rc-component/picker/-/picker-1.8.0.tgz#4fe2965acd804995123d7bd81d184c2c81f92f8b"
integrity sha512-ek4efrIy+peC8WFJg6Lg7c+WNkykr+wUGQGBNoKmlF0K752aIJuaPcBj6p8CceT9vSJ9gOeeclQCBQIFWVDk1A==
"@rc-component/picker@~1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@rc-component/picker/-/picker-1.9.0.tgz#5ecb5595d2fcf0b4ec4edc9202628f42a314c4b0"
integrity sha512-OLisdk8AWVCG9goBU1dWzuH5QlBQk8jktmQ6p0/IyBFwdKGwyIZOSjnBYo8hooHiTdl0lU+wGf/OfMtVBw02KQ==
dependencies:
"@rc-component/overflow" "^1.0.0"
"@rc-component/resize-observer" "^1.0.0"
@@ -2919,20 +2919,20 @@
dependencies:
"@rc-component/util" "^1.2.0"
"@rc-component/segmented@~1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@rc-component/segmented/-/segmented-1.2.3.tgz#3a1b8d5daa2ecba6876062abb01424bbd512ef79"
integrity sha512-L7G4S6zUpqHclOXK0wKKN2/VyqHa9tfDNxkoFjWOTPtQ0ROFaBwZhbf1+9sdZfIFkxJkpcShAmDOMEIBaFFqkw==
"@rc-component/segmented@~1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@rc-component/segmented/-/segmented-1.3.0.tgz#46f5e3f34630a246e02fe20fd501ed448eb64c17"
integrity sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg==
dependencies:
"@babel/runtime" "^7.11.1"
"@rc-component/motion" "^1.1.4"
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/select@~1.3.0", "@rc-component/select@~1.3.2":
version "1.3.4"
resolved "https://registry.yarnpkg.com/@rc-component/select/-/select-1.3.4.tgz#e1c0756290ae4ed3d6823b36de536222752b1193"
integrity sha512-NKhzahL/lXk3aKtmeH5W/jIqaPKcx9QiFXOvJxKe8eiuusIcSCW+XvJdjY3nRvCpTZCZDp7e1RaCU95gohx6Ow==
"@rc-component/select@~1.3.0", "@rc-component/select@~1.3.6":
version "1.3.6"
resolved "https://registry.yarnpkg.com/@rc-component/select/-/select-1.3.6.tgz#3272fb12382d14e8d51f20de26b3cf39feb163bc"
integrity sha512-CzbJ9TwmWcF5asvTMZ9BMiTE9CkkrigeOGRPpzCNmeZP7KBwwmYrmOIiKh9tMG7d6DyGAEAQ75LBxzPx+pGTHA==
dependencies:
"@rc-component/overflow" "^1.0.0"
"@rc-component/trigger" "^3.0.0"
@@ -3036,10 +3036,10 @@
"@rc-component/virtual-list" "^1.0.1"
clsx "^2.1.1"
"@rc-component/trigger@^3.0.0", "@rc-component/trigger@^3.6.15", "@rc-component/trigger@^3.7.1":
version "3.7.1"
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-3.7.1.tgz#3b84eb77a6ea99f240b5fa4c06a2dea34b65d3d5"
integrity sha512-+YNP8FywxKJpdqzlAp6TN8UbSK6YsQtIs3kI13mHfm87qi3qUd5Q9AGW8Unfv76kXFUSu7U7D0FygRsGH+6MiA==
"@rc-component/trigger@^3.0.0", "@rc-component/trigger@^3.6.15", "@rc-component/trigger@^3.7.1", "@rc-component/trigger@^3.7.2":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-3.7.2.tgz#9ae9192c024c35432106904785359f8a46fa1412"
integrity sha512-25x+D2k9SAkaK/MNMNmv2Nlv8FH1D9RtmjoMoLEw1Cid+sMV4pAAT5k49ku59UeXaOA1qwLUVrBUMq4A6gUSsQ==
dependencies:
"@rc-component/motion" "^1.1.4"
"@rc-component/portal" "^2.0.0"
@@ -3055,7 +3055,7 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/util@^1.0.1", "@rc-component/util@^1.1.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0":
"@rc-component/util@^1.0.1", "@rc-component/util@^1.1.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.5.0", "@rc-component/util@^1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.6.0.tgz#4f700da5417eb5fd5f9491f08edcba6d075d9454"
integrity sha512-YbjuIVAm8InCnXVoA4n6G+uh31yESTxQ6fSY2frZ2/oMSvktoB+bumFUfNN7RKh7YeOkZgOvN2suGtEDhJSX0A==
@@ -3170,12 +3170,12 @@
resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-8.6.14.tgz#ba2be7b0644655d17db143b2be5f45199e617af4"
integrity sha512-RrJ95u3HuIE4Nk8VmZP0tc/u0vYoE2v9fYlMw6K2GUSExzKDITs3voy6WMIY7Q3qbQun8XUXVlmqkuFzTEy/pA==
"@storybook/core@8.6.14", "@storybook/core@^8.6.11":
version "8.6.14"
resolved "https://registry.yarnpkg.com/@storybook/core/-/core-8.6.14.tgz#335b067709fd649512b6553b31ad48c8c56f7ed9"
integrity sha512-1P/w4FSNRqP8j3JQBOi3yGt8PVOgSRbP66Ok520T78eJBeqx9ukCfl912PQZ7SPbW3TIunBwLXMZOjZwBB/JmA==
"@storybook/core@8.6.15", "@storybook/core@^8.6.11":
version "8.6.15"
resolved "https://registry.yarnpkg.com/@storybook/core/-/core-8.6.15.tgz#ba3aa59b02981136fa671d545b64d380c1188c8b"
integrity sha512-VFpKcphNurJpSC4fpUfKL3GTXVoL53oytghGR30QIw5jKWwaT50HVbTyb41BLOUuZjmMhUQA8weiQEew6RX0gw==
dependencies:
"@storybook/theming" "8.6.14"
"@storybook/theming" "8.6.15"
better-opn "^3.0.2"
browser-assert "^1.2.1"
esbuild "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0"
@@ -3221,7 +3221,12 @@
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-8.6.14.tgz#02fc8aeab701040744d93b6ef46b9e5727123370"
integrity sha512-0hixr3dOy3f3M+HBofp3jtMQMS+sqzjKNgl7Arfuj3fvjmyXOks/yGjDImySR4imPtEllvPZfhiQNlejheaInw==
"@storybook/theming@8.6.14", "@storybook/theming@^8.6.11":
"@storybook/theming@8.6.15":
version "8.6.15"
resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-8.6.15.tgz#781e6b36f113a10e76379956b9451276e8d5bda2"
integrity sha512-dAbL0XOekyT6XsF49R6Etj3WxQ/LpdJDIswUUeHgVJ6/yd2opZOGbPxnwA3zlmAh1c0tvpPyhSDXxSG79u8e4Q==
"@storybook/theming@^8.6.11":
version "8.6.14"
resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-8.6.14.tgz#78c6dc878f705de70c67f2b2d08b8313b985d81a"
integrity sha512-r4y+LsiB37V5hzpQo+BM10PaCsp7YlZ0YcZzQP1OCkPlYXmUAFy2VvDKaFRpD8IeNPKug2u4iFm/laDEbs03dg==
@@ -4211,6 +4216,11 @@
dependencies:
"@types/istanbul-lib-report" "*"
"@types/js-yaml@^4.0.9":
version "4.0.9"
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2"
integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==
"@types/json-bigint@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@types/json-bigint/-/json-bigint-1.0.4.tgz#250d29e593375499d8ba6efaab22d094c3199ef3"
@@ -4430,100 +4440,161 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.49.0", "@typescript-eslint/eslint-plugin@^8.37.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz#8ed8736b8415a9193989220eadb6031dbcd2260a"
integrity sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==
"@typescript-eslint/eslint-plugin@8.50.1", "@typescript-eslint/eslint-plugin@^8.37.0":
version "8.50.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.1.tgz#b56e422fb82eb40fae04905f1444aef0298b634b"
integrity sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==
dependencies:
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "8.49.0"
"@typescript-eslint/type-utils" "8.49.0"
"@typescript-eslint/utils" "8.49.0"
"@typescript-eslint/visitor-keys" "8.49.0"
"@typescript-eslint/scope-manager" "8.50.1"
"@typescript-eslint/type-utils" "8.50.1"
"@typescript-eslint/utils" "8.50.1"
"@typescript-eslint/visitor-keys" "8.50.1"
ignore "^7.0.0"
natural-compare "^1.4.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/parser@8.49.0", "@typescript-eslint/parser@^8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.49.0.tgz#0ede412d59e99239b770f0f08c76c42fba717fa2"
integrity sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==
"@typescript-eslint/parser@8.50.1":
version "8.50.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.50.1.tgz#9772760c0c4090ba3e8b43c796128ff88aff345c"
integrity sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==
dependencies:
"@typescript-eslint/scope-manager" "8.49.0"
"@typescript-eslint/types" "8.49.0"
"@typescript-eslint/typescript-estree" "8.49.0"
"@typescript-eslint/visitor-keys" "8.49.0"
"@typescript-eslint/scope-manager" "8.50.1"
"@typescript-eslint/types" "8.50.1"
"@typescript-eslint/typescript-estree" "8.50.1"
"@typescript-eslint/visitor-keys" "8.50.1"
debug "^4.3.4"
"@typescript-eslint/project-service@8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.49.0.tgz#ce220525c88cb2d23792b391c07e14cb9697651a"
integrity sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==
"@typescript-eslint/parser@^8.51.0":
version "8.51.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.51.0.tgz#584fb8be3a867cbf980917aabed5f7528f615d6b"
integrity sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.49.0"
"@typescript-eslint/types" "^8.49.0"
"@typescript-eslint/scope-manager" "8.51.0"
"@typescript-eslint/types" "8.51.0"
"@typescript-eslint/typescript-estree" "8.51.0"
"@typescript-eslint/visitor-keys" "8.51.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz#a3496765b57fb48035d671174552e462e5bffa63"
integrity sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==
"@typescript-eslint/project-service@8.50.1":
version "8.50.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.50.1.tgz#3176e55ac2907638f4b8d43da486c864934adc8d"
integrity sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==
dependencies:
"@typescript-eslint/types" "8.49.0"
"@typescript-eslint/visitor-keys" "8.49.0"
"@typescript-eslint/tsconfig-utils" "^8.50.1"
"@typescript-eslint/types" "^8.50.1"
debug "^4.3.4"
"@typescript-eslint/tsconfig-utils@8.49.0", "@typescript-eslint/tsconfig-utils@^8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz#857777c8e35dd1e564505833d8043f544442fbf4"
integrity sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==
"@typescript-eslint/type-utils@8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz#d8118a0c1896a78a22f01d3c176e9945409b085b"
integrity sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==
"@typescript-eslint/project-service@8.51.0":
version "8.51.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.51.0.tgz#3cfef313d8bebbf4b2442675a4dd463cef4c8369"
integrity sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==
dependencies:
"@typescript-eslint/types" "8.49.0"
"@typescript-eslint/typescript-estree" "8.49.0"
"@typescript-eslint/utils" "8.49.0"
"@typescript-eslint/tsconfig-utils" "^8.51.0"
"@typescript-eslint/types" "^8.51.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@8.50.1":
version "8.50.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.50.1.tgz#4a7cd64bcd45990865bdb2bedcacbfeccbd08193"
integrity sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==
dependencies:
"@typescript-eslint/types" "8.50.1"
"@typescript-eslint/visitor-keys" "8.50.1"
"@typescript-eslint/scope-manager@8.51.0":
version "8.51.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz#19b42f65680c21f7b6f40fe9024327f6bb1893c1"
integrity sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==
dependencies:
"@typescript-eslint/types" "8.51.0"
"@typescript-eslint/visitor-keys" "8.51.0"
"@typescript-eslint/tsconfig-utils@8.50.1", "@typescript-eslint/tsconfig-utils@^8.50.1":
version "8.50.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.1.tgz#ee4894bec14ef13db305d0323b14b109d996f116"
integrity sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==
"@typescript-eslint/tsconfig-utils@8.51.0", "@typescript-eslint/tsconfig-utils@^8.51.0":
version "8.51.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz#a575e9885e62dbd260fb64474eff1dae6e317515"
integrity sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==
"@typescript-eslint/type-utils@8.50.1":
version "8.50.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.50.1.tgz#7bbc79baa03aee6e3b3faf14bb0b8a78badb2370"
integrity sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==
dependencies:
"@typescript-eslint/types" "8.50.1"
"@typescript-eslint/typescript-estree" "8.50.1"
"@typescript-eslint/utils" "8.50.1"
debug "^4.3.4"
ts-api-utils "^2.1.0"
"@typescript-eslint/types@8.49.0", "@typescript-eslint/types@^8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.49.0.tgz#c1bd3ebf956d9e5216396349ca23c58d74f06aee"
integrity sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==
"@typescript-eslint/types@8.50.1":
version "8.50.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.50.1.tgz#43d19e99613788e0715f799a29f139981bcd8385"
integrity sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==
"@typescript-eslint/typescript-estree@8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz#99c5a53275197ccb4e849786dad68344e9924135"
integrity sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==
"@typescript-eslint/types@8.51.0", "@typescript-eslint/types@^8.50.1", "@typescript-eslint/types@^8.51.0":
version "8.51.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.51.0.tgz#6996e59d49e92fb893531bdc249f0d92a7bebdbb"
integrity sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==
"@typescript-eslint/typescript-estree@8.50.1":
version "8.50.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.1.tgz#ce273e584694fa5bd34514fcfbea51fe1d79e271"
integrity sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==
dependencies:
"@typescript-eslint/project-service" "8.49.0"
"@typescript-eslint/tsconfig-utils" "8.49.0"
"@typescript-eslint/types" "8.49.0"
"@typescript-eslint/visitor-keys" "8.49.0"
"@typescript-eslint/project-service" "8.50.1"
"@typescript-eslint/tsconfig-utils" "8.50.1"
"@typescript-eslint/types" "8.50.1"
"@typescript-eslint/visitor-keys" "8.50.1"
debug "^4.3.4"
minimatch "^9.0.4"
semver "^7.6.0"
tinyglobby "^0.2.15"
ts-api-utils "^2.1.0"
"@typescript-eslint/utils@8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.49.0.tgz#43b3b91d30afd6f6114532cf0b228f1790f43aff"
integrity sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==
"@typescript-eslint/typescript-estree@8.51.0":
version "8.51.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz#b57f5157d1ac2127bd7c2c9ad8060fa017df4a1a"
integrity sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==
dependencies:
"@typescript-eslint/project-service" "8.51.0"
"@typescript-eslint/tsconfig-utils" "8.51.0"
"@typescript-eslint/types" "8.51.0"
"@typescript-eslint/visitor-keys" "8.51.0"
debug "^4.3.4"
minimatch "^9.0.4"
semver "^7.6.0"
tinyglobby "^0.2.15"
ts-api-utils "^2.2.0"
"@typescript-eslint/utils@8.50.1":
version "8.50.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.50.1.tgz#054db870952e7526c3cf2162a2ff6e9434e544d0"
integrity sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==
dependencies:
"@eslint-community/eslint-utils" "^4.7.0"
"@typescript-eslint/scope-manager" "8.49.0"
"@typescript-eslint/types" "8.49.0"
"@typescript-eslint/typescript-estree" "8.49.0"
"@typescript-eslint/scope-manager" "8.50.1"
"@typescript-eslint/types" "8.50.1"
"@typescript-eslint/typescript-estree" "8.50.1"
"@typescript-eslint/visitor-keys@8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz#8e450cc502c0d285cad9e84d400cf349a85ced6c"
integrity sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==
"@typescript-eslint/visitor-keys@8.50.1":
version "8.50.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.1.tgz#13b9d43b7567862faca69527580b9adda1a5c9fd"
integrity sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==
dependencies:
"@typescript-eslint/types" "8.49.0"
"@typescript-eslint/types" "8.50.1"
eslint-visitor-keys "^4.2.1"
"@typescript-eslint/visitor-keys@8.51.0":
version "8.51.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz#d37f5c82b9bece2c8aeb3ba7bb836bbba0f92bb8"
integrity sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==
dependencies:
"@typescript-eslint/types" "8.51.0"
eslint-visitor-keys "^4.2.1"
"@ungap/structured-clone@^1.0.0":
@@ -4860,10 +4931,10 @@ ansi-styles@^6.1.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
antd@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.1.0.tgz#2924f50e37bf1fe9b4c494057eca1c1b7ccfe47a"
integrity sha512-RIe4W5saaL9SWgvqCcvz6LZta/KwT50B0YF7xYiWVZh0Gqfw2rJAsOMcp202Hxgm+YiyoSp4QqqvexKhuGGarw==
antd@^6.1.2:
version "6.1.2"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.1.2.tgz#99d76a950840c0ca51042507771013463db95a44"
integrity sha512-pqYaZECL/7TBiNxxz+LieLiPCem6DaEzudqN44EZ3SvJjixLP7K41n6clo0zxe/2HiOUe9KxTMxGN+icOkL6Tw==
dependencies:
"@ant-design/colors" "^8.0.0"
"@ant-design/cssinjs" "^2.0.1"
@@ -4879,8 +4950,8 @@ antd@^6.1.0:
"@rc-component/dialog" "~1.5.1"
"@rc-component/drawer" "~1.3.0"
"@rc-component/dropdown" "~1.0.2"
"@rc-component/form" "~1.4.0"
"@rc-component/image" "~1.5.2"
"@rc-component/form" "~1.6.0"
"@rc-component/image" "~1.5.3"
"@rc-component/input" "~1.1.2"
"@rc-component/input-number" "~1.6.2"
"@rc-component/mentions" "~1.6.0"
@@ -4889,13 +4960,13 @@ antd@^6.1.0:
"@rc-component/mutate-observer" "^2.0.1"
"@rc-component/notification" "~1.2.0"
"@rc-component/pagination" "~1.2.0"
"@rc-component/picker" "~1.8.0"
"@rc-component/picker" "~1.9.0"
"@rc-component/progress" "~1.0.2"
"@rc-component/qrcode" "~1.1.1"
"@rc-component/rate" "~1.0.1"
"@rc-component/resize-observer" "^1.0.1"
"@rc-component/segmented" "~1.2.3"
"@rc-component/select" "~1.3.2"
"@rc-component/segmented" "~1.3.0"
"@rc-component/select" "~1.3.6"
"@rc-component/slider" "~1.0.1"
"@rc-component/steps" "~1.2.2"
"@rc-component/switch" "~1.0.3"
@@ -4906,9 +4977,9 @@ antd@^6.1.0:
"@rc-component/tour" "~2.2.1"
"@rc-component/tree" "~1.1.0"
"@rc-component/tree-select" "~1.4.0"
"@rc-component/trigger" "^3.7.1"
"@rc-component/trigger" "^3.7.2"
"@rc-component/upload" "~1.1.0"
"@rc-component/util" "^1.4.0"
"@rc-component/util" "^1.6.0"
clsx "^2.1.1"
dayjs "^1.11.11"
scroll-into-view-if-needed "^3.1.0"
@@ -5157,10 +5228,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.8.9:
version "2.8.10"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.10.tgz#32eb5e253d633fa3fa3ffb1685fabf41680d9e8a"
integrity sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==
baseline-browser-mapping@^2.9.0:
version "2.9.8"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.8.tgz#04fb5c10ff9c7a1b04ac08cfdfc3b10942a8ac72"
integrity sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA==
batch@0.6.1:
version "0.6.1"
@@ -5275,16 +5346,16 @@ browser-assert@^1.2.1:
resolved "https://registry.yarnpkg.com/browser-assert/-/browser-assert-1.2.1.tgz#9aaa5a2a8c74685c2ae05bfe46efd606f068c200"
integrity sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.25.0, browserslist@^4.25.3, browserslist@^4.26.3:
version "4.26.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.26.3.tgz#40fbfe2d1cd420281ce5b1caa8840049c79afb56"
integrity sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.25.0, browserslist@^4.25.3, browserslist@^4.28.1:
version "4.28.1"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95"
integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==
dependencies:
baseline-browser-mapping "^2.8.9"
caniuse-lite "^1.0.30001746"
electron-to-chromium "^1.5.227"
node-releases "^2.0.21"
update-browserslist-db "^1.1.3"
baseline-browser-mapping "^2.9.0"
caniuse-lite "^1.0.30001759"
electron-to-chromium "^1.5.263"
node-releases "^2.0.27"
update-browserslist-db "^1.2.0"
buffer-from@^1.0.0:
version "1.1.2"
@@ -5393,10 +5464,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001746, caniuse-lite@^1.0.30001760:
version "1.0.30001760"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz#bdd1960fafedf8d5f04ff16e81460506ff9b798f"
integrity sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001762:
version "1.0.30001762"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz#e4dbfeda63d33258cdde93e53af2023a13ba27d4"
integrity sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==
ccount@^2.0.0:
version "2.0.1"
@@ -6716,10 +6787,10 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.5.227:
version "1.5.228"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.228.tgz#38b849bc8714bd21fb64f5ad56bf8cfd8638e1e9"
integrity sha512-nxkiyuqAn4MJ1QbobwqJILiDtu/jk14hEAWaMiJmNPh1Z+jqoFlBFZjdXwLWGeVSeu9hGLg6+2G9yJaW8rBIFA==
electron-to-chromium@^1.5.263:
version "1.5.267"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz#5d84f2df8cdb6bfe7e873706bb21bd4bfb574dc7"
integrity sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==
emoji-regex@^8.0.0:
version "8.0.0"
@@ -6756,10 +6827,10 @@ encodeurl@~2.0.0:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.3:
version "5.18.3"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz#9b5f4c5c076b8787c78fe540392ce76a88855b44"
integrity sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==
enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.4:
version "5.18.4"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz#c22d33055f3952035ce6a144ce092447c525f828"
integrity sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
@@ -6885,10 +6956,10 @@ es-iterator-helpers@^1.2.1:
iterator.prototype "^1.1.4"
safe-array-concat "^1.1.3"
es-module-lexer@^1.2.1:
version "1.7.0"
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a"
integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==
es-module-lexer@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.0.0.tgz#f657cd7a9448dcdda9c070a3cb75e5dc1e85f5b1"
integrity sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
version "1.1.1"
@@ -7679,10 +7750,10 @@ globals@^15.14.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-15.15.0.tgz#7c4761299d41c32b075715a4ce1ede7897ff72a8"
integrity sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==
globals@^16.5.0:
version "16.5.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-16.5.0.tgz#ccf1594a437b97653b2be13ed4d8f5c9f850cac1"
integrity sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==
globals@^17.0.0:
version "17.0.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-17.0.0.tgz#a4196d9cfeb4d627ba165b4647b1f5853bf90a30"
integrity sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw==
globalthis@^1.0.4:
version "1.0.4"
@@ -8756,7 +8827,16 @@ js-file-download@^0.4.12:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@=4.1.1, js-yaml@^4.1.0:
js-yaml-loader@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/js-yaml-loader/-/js-yaml-loader-1.2.2.tgz#2c15f93915617acd19676d648945fa3003f8629b"
integrity sha512-H+NeuNrG6uOs/WMjna2SjkaCw13rMWiT/D7l9+9x5n8aq88BDsh2sRmdfxckWPIHtViYHWRG6XiCKYvS1dfyLg==
dependencies:
js-yaml "^3.13.1"
loader-utils "^1.2.3"
un-eval "^1.2.0"
js-yaml@=4.1.1, js-yaml@^4.1.0, js-yaml@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
@@ -8764,9 +8844,9 @@ js-yaml@=4.1.1, js-yaml@^4.1.0:
argparse "^2.0.1"
js-yaml@^3.13.1:
version "3.14.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
version "3.14.2"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.2.tgz#77485ce1dd7f33c061fd1b16ecea23b55fcb04b0"
integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
@@ -8835,6 +8915,13 @@ json2mq@^0.2.0:
dependencies:
string-convert "^0.2.0"
json5@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
dependencies:
minimist "^1.2.0"
json5@^2.1.2, json5@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
@@ -8984,6 +9071,15 @@ loader-runner@^4.3.1:
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.1.tgz#6c76ed29b0ccce9af379208299f07f876de737e3"
integrity sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==
loader-utils@^1.2.3:
version "1.4.2"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3"
integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==
dependencies:
big.js "^5.2.2"
emojis-list "^3.0.0"
json5 "^1.0.1"
loader-utils@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c"
@@ -9360,9 +9456,9 @@ mdast-util-to-hast@^12.1.0:
unist-util-visit "^4.0.0"
mdast-util-to-hast@^13.0.0:
version "13.2.0"
resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz#5ca58e5b921cc0a3ded1bc02eed79a4fe4fe41f4"
integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==
version "13.2.1"
resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz#d7ff84ca499a57e2c060ae67548ad950e689a053"
integrity sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==
dependencies:
"@types/hast" "^3.0.0"
"@types/mdast" "^4.0.0"
@@ -10306,10 +10402,10 @@ node-gyp-build@^4.8.0, node-gyp-build@^4.8.2, node-gyp-build@^4.8.4:
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8"
integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==
node-releases@^2.0.21:
version "2.0.21"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.21.tgz#f59b018bc0048044be2d4c4c04e4c8b18160894c"
integrity sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==
node-releases@^2.0.27:
version "2.0.27"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e"
integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
@@ -11468,9 +11564,9 @@ pupa@^3.1.0:
escape-goat "^4.0.0"
qs@~6.14.0:
version "6.14.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930"
integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==
version "6.14.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.1.tgz#a41d85b9d3902f31d27861790506294881871159"
integrity sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==
dependencies:
side-channel "^1.1.0"
@@ -12785,12 +12881,12 @@ stop-iteration-iterator@^1.1.0:
es-errors "^1.3.0"
internal-slot "^1.1.0"
storybook@^8.6.11:
version "8.6.14"
resolved "https://registry.yarnpkg.com/storybook/-/storybook-8.6.14.tgz#d205e73b6427eebf321bcfbe63bfbec3ade4d9db"
integrity sha512-sVKbCj/OTx67jhmauhxc2dcr1P+yOgz/x3h0krwjyMgdc5Oubvxyg4NYDZmzAw+ym36g/lzH8N0Ccp4dwtdfxw==
storybook@^8.6.15:
version "8.6.15"
resolved "https://registry.yarnpkg.com/storybook/-/storybook-8.6.15.tgz#eb4a3bae1ac0fe875bb322a412fac9709d9b5124"
integrity sha512-Ob7DMlwWx8s7dMvcQ3xPc02TvUeralb+xX3oaPRk9wY9Hc6M1IBC/7cEoITkSmRS2v38DHubC+mtEKNc1u2gQg==
dependencies:
"@storybook/core" "8.6.14"
"@storybook/core" "8.6.15"
string-convert@^0.2.0:
version "0.2.1"
@@ -13119,10 +13215,10 @@ tapable@^2.0.0, tapable@^2.2.0, tapable@^2.2.1, tapable@^2.3.0:
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6"
integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==
terser-webpack-plugin@^5.3.11, terser-webpack-plugin@^5.3.9:
version "5.3.14"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz#9031d48e57ab27567f02ace85c7d690db66c3e06"
integrity sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==
terser-webpack-plugin@^5.3.16, terser-webpack-plugin@^5.3.9:
version "5.3.16"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz#741e448cc3f93d8026ebe4f7ef9e4afacfd56330"
integrity sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==
dependencies:
"@jridgewell/trace-mapping" "^0.3.25"
jest-worker "^27.4.5"
@@ -13287,6 +13383,11 @@ ts-api-utils@^2.1.0:
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91"
integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
ts-api-utils@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.3.0.tgz#9f397ac9d88ac76e8dd6e8bc4af0dbf98af99f73"
integrity sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==
ts-dedent@^2.0.0, ts-dedent@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"
@@ -13417,15 +13518,15 @@ types-ramda@^0.30.1:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.49.0:
version "8.49.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.49.0.tgz#4a8b608ae48c0db876c8fb2a2724839fc5a7147c"
integrity sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==
typescript-eslint@^8.50.1:
version "8.50.1"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.50.1.tgz#047df900e568757bc791b6b1ab6fa5fbed9b2393"
integrity sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==
dependencies:
"@typescript-eslint/eslint-plugin" "8.49.0"
"@typescript-eslint/parser" "8.49.0"
"@typescript-eslint/typescript-estree" "8.49.0"
"@typescript-eslint/utils" "8.49.0"
"@typescript-eslint/eslint-plugin" "8.50.1"
"@typescript-eslint/parser" "8.50.1"
"@typescript-eslint/typescript-estree" "8.50.1"
"@typescript-eslint/utils" "8.50.1"
typescript@~5.9.3:
version "5.9.3"
@@ -13437,6 +13538,11 @@ ufo@^1.5.4:
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b"
integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==
un-eval@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/un-eval/-/un-eval-1.2.0.tgz#22a95c650334d59d21697efae32612218ecad65f"
integrity sha512-Wlj/pum6dQtGTPD/lclDtoVPkSfpjPfy1dwnnKw/sZP5DpBH9fLhBgQfsqNhe5/gS1D+vkZUuB771NRMUPA5CA==
unbox-primitive@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2"
@@ -13651,10 +13757,10 @@ unraw@^3.0.0:
resolved "https://registry.yarnpkg.com/unraw/-/unraw-3.0.0.tgz#73443ed70d2ab09ccbac2b00525602d5991fbbe3"
integrity sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==
update-browserslist-db@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420"
integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==
update-browserslist-db@^1.2.0:
version "1.2.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d"
integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==
dependencies:
escalade "^3.2.0"
picocolors "^1.1.1"
@@ -13973,10 +14079,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.103.0, webpack@^5.88.1, webpack@^5.95.0:
version "5.103.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.103.0.tgz#17a7c5a5020d5a3a37c118d002eade5ee2c6f3da"
integrity sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==
webpack@^5.104.1, webpack@^5.88.1, webpack@^5.95.0:
version "5.104.1"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.104.1.tgz#94bd41eb5dbf06e93be165ba8be41b8260d4fb1a"
integrity sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==
dependencies:
"@types/eslint-scope" "^3.7.7"
"@types/estree" "^1.0.8"
@@ -13986,10 +14092,10 @@ webpack@^5.103.0, webpack@^5.88.1, webpack@^5.95.0:
"@webassemblyjs/wasm-parser" "^1.14.1"
acorn "^8.15.0"
acorn-import-phases "^1.0.3"
browserslist "^4.26.3"
browserslist "^4.28.1"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.17.3"
es-module-lexer "^1.2.1"
enhanced-resolve "^5.17.4"
es-module-lexer "^2.0.0"
eslint-scope "5.1.1"
events "^3.2.0"
glob-to-regexp "^0.4.1"
@@ -14000,7 +14106,7 @@ webpack@^5.103.0, webpack@^5.88.1, webpack@^5.95.0:
neo-async "^2.6.2"
schema-utils "^4.3.3"
tapable "^2.3.0"
terser-webpack-plugin "^5.3.11"
terser-webpack-plugin "^5.3.16"
watchpack "^2.4.4"
webpack-sources "^3.3.3"
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env bash
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# -----------------------------------------------------------------------
# Smart docker-compose wrapper for running multiple Superset instances
#
# Features:
# - Auto-generates unique project name from directory
# - Finds available ports automatically
# - No manual .env-local editing needed
#
# Usage:
# ./scripts/docker-compose-up.sh [docker-compose args...]
#
# Examples:
# ./scripts/docker-compose-up.sh # Start all services
# ./scripts/docker-compose-up.sh -d # Start detached
# ./scripts/docker-compose-up.sh down # Stop services
# -----------------------------------------------------------------------
set -e
# Get the repo root directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# Generate project name from directory name (sanitized for Docker)
DIR_NAME=$(basename "$REPO_ROOT")
PROJECT_NAME=$(echo "$DIR_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//')
# Function to check if a port is available
is_port_available() {
local port=$1
if command -v lsof &> /dev/null; then
! lsof -i ":$port" &> /dev/null
elif command -v netstat &> /dev/null; then
! netstat -tuln 2>/dev/null | grep -q ":$port "
elif command -v ss &> /dev/null; then
! ss -tuln 2>/dev/null | grep -q ":$port "
else
# If no tool available, assume port is available
return 0
fi
}
# Track ports we've already claimed in this session
CLAIMED_PORTS=""
# Function to check if port is available and not already claimed
is_port_free() {
local port=$1
# Check not already claimed by us
if [[ " $CLAIMED_PORTS " =~ " $port " ]]; then
return 1
fi
# Check not in use on system
is_port_available $port
}
# Function to find and claim next available port starting from base
# Sets the result in the variable named by $2
find_and_claim_port() {
local base_port=$1
local var_name=$2
local max_attempts=100
local port=$base_port
for ((i=0; i<max_attempts; i++)); do
if is_port_free $port; then
CLAIMED_PORTS="$CLAIMED_PORTS $port"
eval "$var_name=$port"
return 0
fi
((port++))
done
echo "ERROR: Could not find available port starting from $base_port" >&2
return 1
}
# Base ports (defaults from docker-compose.yml)
BASE_NGINX=80
BASE_SUPERSET=8088
BASE_NODE=9000
BASE_WEBSOCKET=8080
BASE_CYPRESS=8081
BASE_DATABASE=5432
BASE_REDIS=6379
# Find available ports (no subshells - claims persist correctly)
echo "🔍 Finding available ports..."
find_and_claim_port $BASE_NGINX NGINX_PORT
find_and_claim_port $BASE_SUPERSET SUPERSET_PORT
find_and_claim_port $BASE_NODE NODE_PORT
find_and_claim_port $BASE_WEBSOCKET WEBSOCKET_PORT
find_and_claim_port $BASE_CYPRESS CYPRESS_PORT
find_and_claim_port $BASE_DATABASE DATABASE_PORT
find_and_claim_port $BASE_REDIS REDIS_PORT
# Export for docker-compose
export COMPOSE_PROJECT_NAME="$PROJECT_NAME"
export NGINX_PORT
export SUPERSET_PORT
export NODE_PORT
export WEBSOCKET_PORT
export CYPRESS_PORT
export DATABASE_PORT
export REDIS_PORT
echo ""
echo "🐳 Starting Superset with:"
echo " Project: $PROJECT_NAME"
echo " Superset: http://localhost:$SUPERSET_PORT"
echo " Dev Server: http://localhost:$NODE_PORT"
echo " Nginx: http://localhost:$NGINX_PORT"
echo " WebSocket: localhost:$WEBSOCKET_PORT"
echo " Database: localhost:$DATABASE_PORT"
echo " Redis: localhost:$REDIS_PORT"
echo ""
# Change to repo root
cd "$REPO_ROOT"
# Handle special commands
case "${1:-}" in
--dry-run)
echo "✅ Dry run complete. To start, run without --dry-run"
exit 0
;;
--env)
# Output as sourceable environment variables
echo "export COMPOSE_PROJECT_NAME='$PROJECT_NAME'"
echo "export NGINX_PORT=$NGINX_PORT"
echo "export SUPERSET_PORT=$SUPERSET_PORT"
echo "export NODE_PORT=$NODE_PORT"
echo "export WEBSOCKET_PORT=$WEBSOCKET_PORT"
echo "export CYPRESS_PORT=$CYPRESS_PORT"
echo "export DATABASE_PORT=$DATABASE_PORT"
echo "export REDIS_PORT=$REDIS_PORT"
exit 0
;;
down|stop|logs|ps|exec|restart)
# Pass through to docker compose
docker compose "$@"
;;
nuke)
# Nuclear option: remove everything (containers, volumes, local images)
echo "💥 Nuking all containers, volumes, and locally-built images for $PROJECT_NAME..."
docker compose down -v --rmi local
echo "✅ Done. Run 'make up' or './scripts/docker-compose-up.sh' to start fresh."
;;
*)
# Default: start services
docker compose up "$@"
;;
esac
+1 -1
View File
@@ -18,7 +18,7 @@
[project]
name = "apache-superset-core"
version = "0.0.1rc2"
version = "0.0.1rc3"
description = "Core Python package for building Apache Superset backend extensions and integrations"
readme = "README.md"
authors = [
+10 -2
View File
@@ -92,7 +92,11 @@ class Database(CoreModel):
"""
Execute SQL synchronously.
:param sql: SQL query to execute
The SQL must be written in the dialect of the target database (e.g.,
PostgreSQL syntax for PostgreSQL databases, Snowflake syntax for
Snowflake, etc.). No automatic cross-dialect translation is performed.
:param sql: SQL query to execute (in the target database's dialect)
:param options: Query execution options (see `QueryOptions`).
If not provided, defaults are used.
:returns: QueryResult with status, data (DataFrame), and metadata
@@ -139,7 +143,11 @@ class Database(CoreModel):
Returns immediately with a handle for tracking progress and retrieving
results from the background worker.
:param sql: SQL query to execute
The SQL must be written in the dialect of the target database (e.g.,
PostgreSQL syntax for PostgreSQL databases, Snowflake syntax for
Snowflake, etc.). No automatic cross-dialect translation is performed.
:param sql: SQL query to execute (in the target database's dialect)
:param options: Query execution options (see `QueryOptions`).
If not provided, defaults are used.
:returns: AsyncQueryHandle for tracking the query
@@ -15,49 +15,190 @@
# specific language governing permissions and limitations
# under the License.
from typing import TypedDict
"""
Pydantic models for extension configuration and manifest validation.
Two distinct schemas:
- ExtensionConfig: Source configuration (extension.json) authored by developers
- Manifest: Built output (manifest.json) generated by the build tool
"""
from __future__ import annotations
from typing import Any
from pydantic import BaseModel, Field # noqa: I001
# =============================================================================
# Shared components
# =============================================================================
class ModuleFederationConfig(TypedDict):
exposes: dict[str, str]
filename: str
shared: dict[str, str]
remotes: dict[str, str]
class ModuleFederationConfig(BaseModel):
"""Configuration for Webpack Module Federation."""
exposes: list[str] = Field(
default_factory=list,
description="Modules exposed by this extension",
)
filename: str = Field(
default="remoteEntry.js",
description="Remote entry filename",
)
shared: dict[str, Any] = Field(
default_factory=dict,
description="Shared dependencies configuration",
)
remotes: dict[str, str] = Field(
default_factory=dict,
description="Remote module references",
)
class FrontendContributionConfig(TypedDict):
commands: dict[str, list[dict[str, str]]]
views: dict[str, list[dict[str, str]]]
menus: dict[str, list[dict[str, str]]]
class ContributionConfig(BaseModel):
"""Configuration for frontend UI contributions."""
commands: list[dict[str, Any]] = Field(
default_factory=list,
description="Command contributions",
)
views: dict[str, list[dict[str, Any]]] = Field(
default_factory=dict,
description="View contributions by location",
)
menus: dict[str, Any] = Field(
default_factory=dict,
description="Menu contributions",
)
class FrontendManifest(TypedDict):
contributions: FrontendContributionConfig
moduleFederation: ModuleFederationConfig
remoteEntry: str
class BaseExtension(BaseModel):
"""Base fields shared by ExtensionConfig and Manifest."""
id: str = Field(
...,
description="Unique extension identifier",
min_length=1,
)
name: str = Field(
...,
description="Human-readable extension name",
min_length=1,
)
version: str = Field(
default="0.0.0",
description="Semantic version string",
pattern=r"^\d+\.\d+\.\d+$",
)
license: str | None = Field(
default=None,
description="License identifier (e.g., 'Apache-2.0')",
)
description: str | None = Field(
default=None,
description="Extension description",
)
dependencies: list[str] = Field(
default_factory=list,
description="List of extension IDs this extension depends on",
)
permissions: list[str] = Field(
default_factory=list,
description="Permissions required by this extension",
)
class BackendManifest(TypedDict):
entryPoints: list[str]
# =============================================================================
# ExtensionConfig (extension.json)
# =============================================================================
class SharedBase(TypedDict, total=False):
id: str
name: str
dependencies: list[str]
description: str
version: str
frontend: FrontendManifest
permissions: list[str]
class ExtensionConfigFrontend(BaseModel):
"""Frontend section in extension.json."""
contributions: ContributionConfig = Field(
default_factory=ContributionConfig,
description="UI contribution points",
)
moduleFederation: ModuleFederationConfig = Field( # noqa: N815
default_factory=ModuleFederationConfig,
description="Module Federation configuration",
)
class Manifest(SharedBase, total=False):
backend: BackendManifest
class ExtensionConfigBackend(BaseModel):
"""Backend section in extension.json."""
entryPoints: list[str] = Field( # noqa: N815
default_factory=list,
description="Python module entry points to load",
)
files: list[str] = Field(
default_factory=list,
description="Glob patterns for backend Python files",
)
class BackendMetadata(BackendManifest):
files: list[str]
class ExtensionConfig(BaseExtension):
"""
Schema for extension.json (source configuration).
This file is authored by developers to define extension metadata.
"""
frontend: ExtensionConfigFrontend | None = Field(
default=None,
description="Frontend configuration",
)
backend: ExtensionConfigBackend | None = Field(
default=None,
description="Backend configuration",
)
class Metadata(SharedBase):
backend: BackendMetadata
# =============================================================================
# Manifest (manifest.json)
# =============================================================================
class ManifestFrontend(BaseModel):
"""Frontend section in manifest.json."""
contributions: ContributionConfig = Field(
default_factory=ContributionConfig,
description="UI contribution points",
)
moduleFederation: ModuleFederationConfig = Field( # noqa: N815
default_factory=ModuleFederationConfig,
description="Module Federation configuration",
)
remoteEntry: str = Field( # noqa: N815
...,
description="Path to the built remote entry file",
)
class ManifestBackend(BaseModel):
"""Backend section in manifest.json."""
entryPoints: list[str] = Field( # noqa: N815
default_factory=list,
description="Python module entry points to load",
)
class Manifest(BaseExtension):
"""
Schema for manifest.json (built output).
This file is generated by the build tool from extension.json.
"""
frontend: ManifestFrontend | None = Field(
default=None,
description="Frontend manifest",
)
backend: ManifestBackend | None = Field(
default=None,
description="Backend manifest",
)
@@ -23,12 +23,17 @@ import sys
import time
import zipfile
from pathlib import Path
from typing import Any, Callable, cast
from typing import Any, Callable
import click
import semver
from jinja2 import Environment, FileSystemLoader
from superset_core.extensions.types import Manifest, Metadata
from superset_core.extensions.types import (
ExtensionConfig,
Manifest,
ManifestBackend,
ManifestFrontend,
)
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
@@ -125,40 +130,40 @@ def clean_dist_frontend(cwd: Path) -> None:
def build_manifest(cwd: Path, remote_entry: str | None) -> Manifest:
extension: Metadata = cast(Metadata, read_json(cwd / "extension.json"))
if not extension:
extension_data = read_json(cwd / "extension.json")
if not extension_data:
click.secho("❌ extension.json not found.", err=True, fg="red")
sys.exit(1)
manifest: Manifest = {
"id": extension["id"],
"name": extension["name"],
"version": extension["version"],
"permissions": extension["permissions"],
"dependencies": extension.get("dependencies", []),
}
if (
(frontend := extension.get("frontend"))
and (contributions := frontend.get("contributions"))
and (module_federation := frontend.get("moduleFederation"))
and remote_entry
):
manifest["frontend"] = {
"contributions": contributions,
"moduleFederation": module_federation,
"remoteEntry": remote_entry,
}
extension = ExtensionConfig.model_validate(extension_data)
if entry_points := extension.get("backend", {}).get("entryPoints"):
manifest["backend"] = {"entryPoints": entry_points}
frontend: ManifestFrontend | None = None
if extension.frontend and remote_entry:
frontend = ManifestFrontend(
contributions=extension.frontend.contributions,
moduleFederation=extension.frontend.moduleFederation,
remoteEntry=remote_entry,
)
return manifest
backend: ManifestBackend | None = None
if extension.backend and extension.backend.entryPoints:
backend = ManifestBackend(entryPoints=extension.backend.entryPoints)
return Manifest(
id=extension.id,
name=extension.name,
version=extension.version,
permissions=extension.permissions,
dependencies=extension.dependencies,
frontend=frontend,
backend=backend,
)
def write_manifest(cwd: Path, manifest: Manifest) -> None:
dist_dir = cwd / "dist"
(dist_dir / "manifest.json").write_text(
json.dumps(manifest, indent=2, sort_keys=True)
manifest.model_dump_json(indent=2, exclude_none=True, by_alias=True)
)
click.secho("✅ Manifest updated", fg="green")
@@ -479,6 +484,11 @@ def init(
(target_dir / "extension.json").write_text(extension_json)
click.secho("✅ Created extension.json", fg="green")
# Create .gitignore
gitignore = env.get_template(".gitignore.j2").render(ctx)
(target_dir / ".gitignore").write_text(gitignore)
click.secho("✅ Created .gitignore", fg="green")
# Initialize frontend files
if include_frontend:
frontend_dir = target_dir / "frontend"
@@ -0,0 +1,36 @@
# Dependencies
node_modules/
# Build outputs
dist/
*.supx
# Python
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
.eggs/
*.egg
.venv/
venv/
env/
ENV/
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
# Environment
.env
.env.local
+21 -22
View File
@@ -236,7 +236,7 @@ def test_build_manifest_creates_correct_manifest_structure(isolated_filesystem):
"permissions": ["read_data"],
"dependencies": ["some_dep"],
"frontend": {
"contributions": {"commands": ["test_command"]},
"contributions": {"commands": [{"id": "test_command", "title": "Test"}]},
"moduleFederation": {"exposes": ["./index"]},
},
"backend": {"entryPoints": ["test_extension.entrypoint"]},
@@ -247,23 +247,23 @@ def test_build_manifest_creates_correct_manifest_structure(isolated_filesystem):
manifest = build_manifest(isolated_filesystem, "remoteEntry.abc123.js")
# Verify manifest structure
manifest_dict = dict(manifest)
assert manifest_dict["id"] == "test_extension"
assert manifest_dict["name"] == "Test Extension"
assert manifest_dict["version"] == "1.0.0"
assert manifest_dict["permissions"] == ["read_data"]
assert manifest_dict["dependencies"] == ["some_dep"]
assert manifest.id == "test_extension"
assert manifest.name == "Test Extension"
assert manifest.version == "1.0.0"
assert manifest.permissions == ["read_data"]
assert manifest.dependencies == ["some_dep"]
# Verify frontend section
assert "frontend" in manifest
frontend = manifest["frontend"]
assert frontend["contributions"] == {"commands": ["test_command"]}
assert frontend["moduleFederation"] == {"exposes": ["./index"]}
assert frontend["remoteEntry"] == "remoteEntry.abc123.js"
assert manifest.frontend is not None
assert manifest.frontend.contributions.commands == [
{"id": "test_command", "title": "Test"}
]
assert manifest.frontend.moduleFederation.exposes == ["./index"]
assert manifest.frontend.remoteEntry == "remoteEntry.abc123.js"
# Verify backend section
assert "backend" in manifest
assert manifest["backend"]["entryPoints"] == ["test_extension.entrypoint"]
assert manifest.backend is not None
assert manifest.backend.entryPoints == ["test_extension.entrypoint"]
@pytest.mark.unit
@@ -280,14 +280,13 @@ def test_build_manifest_handles_minimal_extension(isolated_filesystem):
manifest = build_manifest(isolated_filesystem, None)
manifest_dict = dict(manifest)
assert manifest_dict["id"] == "minimal_extension"
assert manifest_dict["name"] == "Minimal Extension"
assert manifest_dict["version"] == "0.1.0"
assert manifest_dict["permissions"] == []
assert manifest_dict["dependencies"] == [] # Default empty list
assert "frontend" not in manifest
assert "backend" not in manifest
assert manifest.id == "minimal_extension"
assert manifest.name == "Minimal Extension"
assert manifest.version == "0.1.0"
assert manifest.permissions == []
assert manifest.dependencies == [] # Default empty list
assert manifest.frontend is None
assert manifest.backend is None
@pytest.mark.unit
@@ -23,6 +23,7 @@ import time
from unittest.mock import Mock, patch
import pytest
from superset_core.extensions.types import Manifest
from superset_extensions_cli.cli import app, FrontendChangeHandler
@@ -48,7 +49,7 @@ def test_dev_command_starts_watchers(
"""Test dev command starts file watchers."""
# Setup mocks
mock_rebuild_frontend.return_value = "remoteEntry.abc123.js"
mock_build_manifest.return_value = {"name": "test", "version": "1.0.0"}
mock_build_manifest.return_value = Manifest(id="test", name="test", version="1.0.0")
mock_observer = Mock()
mock_observer_class.return_value = mock_observer
@@ -100,7 +101,7 @@ def test_dev_command_initial_build(
"""Test dev command performs initial build setup."""
# Setup mocks
mock_rebuild_frontend.return_value = "remoteEntry.abc123.js"
mock_build_manifest.return_value = {"name": "test", "version": "1.0.0"}
mock_build_manifest.return_value = Manifest(id="test", name="test", version="1.0.0")
extension_setup_for_dev(isolated_filesystem)
@@ -188,11 +189,12 @@ def test_frontend_watcher_function_coverage(isolated_filesystem):
dist_dir = isolated_filesystem / "dist"
dist_dir.mkdir()
mock_manifest = Manifest(id="test", name="test", version="1.0.0")
with patch("superset_extensions_cli.cli.rebuild_frontend") as mock_rebuild:
with patch("superset_extensions_cli.cli.build_manifest") as mock_build:
with patch("superset_extensions_cli.cli.write_manifest") as mock_write:
mock_rebuild.return_value = "remoteEntry.abc123.js"
mock_build.return_value = {"name": "test", "version": "1.0.0"}
mock_build.return_value = mock_manifest
# Simulate frontend watcher function logic
frontend_dir = isolated_filesystem / "frontend"
@@ -209,9 +211,7 @@ def test_frontend_watcher_function_coverage(isolated_filesystem):
mock_build.assert_called_once_with(
isolated_filesystem, "remoteEntry.abc123.js"
)
mock_write.assert_called_once_with(
isolated_filesystem, {"name": "test", "version": "1.0.0"}
)
mock_write.assert_called_once_with(isolated_filesystem, mock_manifest)
@pytest.mark.unit
+28 -4
View File
@@ -296,10 +296,34 @@ def test_init_command_output_messages(cli_runner, isolated_filesystem, cli_input
output = result.output
# Check for expected success messages
assert "Created extension.json" in output
assert "Created frontend folder structure" in output
assert "Created backend folder structure" in output
assert "🎉 Extension Test Extension (ID: test_extension) initialized" in output
assert "Created extension.json" in output
assert "Created .gitignore" in output
assert "Created frontend folder structure" in output
assert "Created backend folder structure" in output
assert "Extension Test Extension (ID: test_extension) initialized" in output
@pytest.mark.cli
def test_gitignore_content_is_correct(cli_runner, isolated_filesystem, cli_input_both):
"""Test that the generated .gitignore has the correct content."""
result = cli_runner.invoke(app, ["init"], input=cli_input_both)
assert result.exit_code == 0
extension_path = isolated_filesystem / "test_extension"
gitignore_path = extension_path / ".gitignore"
assert_file_exists(gitignore_path, ".gitignore")
content = gitignore_path.read_text()
# Verify key patterns are present
assert "node_modules/" in content
assert "dist/" in content
assert "*.supx" in content
assert "__pycache__" in content
assert ".venv/" in content
assert ".DS_Store" in content
assert ".env" in content
@pytest.mark.cli
+1 -1
View File
@@ -191,7 +191,7 @@ def create_test_extension_structure(
Dictionary with expected paths and metadata
"""
extension_path = base_path / id_
expected_files = ["extension.json"]
expected_files = ["extension.json", ".gitignore"]
expected_dirs: list[str] = []
if include_frontend:
+315 -39
View File
@@ -4761,6 +4761,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -5533,6 +5546,20 @@
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -5601,12 +5628,57 @@
"is-arrayish": "^0.2.1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-module-lexer": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz",
"integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==",
"peer": true
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
@@ -6439,18 +6511,42 @@
}
},
"node_modules/form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
"integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.35",
"safe-buffer": "^5.2.1"
},
"engines": {
"node": ">= 0.12"
}
},
"node_modules/form-data/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@@ -6503,9 +6599,13 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/functional-red-black-tree": {
"version": "1.0.1",
@@ -6540,13 +6640,24 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1"
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -6560,6 +6671,19 @@
"node": ">=8.0.0"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@@ -6669,6 +6793,18 @@
"node": ">= 4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/got": {
"version": "11.8.6",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
@@ -6702,6 +6838,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"peer": true,
"dependencies": {
"function-bind": "^1.1.1"
},
@@ -6718,9 +6855,25 @@
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
@@ -6740,6 +6893,18 @@
"node": ">=8"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/hast-util-from-parse5": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz",
@@ -7386,9 +7551,10 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
@@ -7813,6 +7979,15 @@
"resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.3.8.tgz",
"integrity": "sha512-9FbRY3i6U+CbHgrdNbAUaisjWTozkm1ZfupYQJiZ87NtYHk2Zh9DvxMgp/fifxVhqTLpd5fCCLossUbpZxGeKw=="
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mdast-util-definitions": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz",
@@ -12941,7 +13116,7 @@
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"form-data": "^2.3.4",
"http-signature": "~1.3.6",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
@@ -14608,6 +14783,15 @@
"get-intrinsic": "^1.0.2"
}
},
"call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"requires": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
}
},
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -15176,6 +15360,16 @@
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg=="
},
"dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"requires": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
}
},
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -15238,12 +15432,41 @@
"is-arrayish": "^0.2.1"
}
},
"es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
},
"es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
},
"es-module-lexer": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz",
"integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==",
"peer": true
},
"es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"requires": {
"es-errors": "^1.3.0"
}
},
"es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"requires": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
}
},
"es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
@@ -15866,13 +16089,23 @@
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="
},
"form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
"integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.35",
"safe-buffer": "^5.2.1"
},
"dependencies": {
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}
}
},
"formdata-polyfill": {
@@ -15917,9 +16150,9 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
},
"functional-red-black-tree": {
"version": "1.0.1",
@@ -15944,13 +16177,20 @@
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ=="
},
"get-intrinsic": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1"
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
}
},
"get-package-type": {
@@ -15958,6 +16198,15 @@
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="
},
"get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"requires": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
}
},
"get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@@ -16042,6 +16291,11 @@
}
}
},
"gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
},
"got": {
"version": "11.8.6",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
@@ -16069,6 +16323,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"peer": true,
"requires": {
"function-bind": "^1.1.1"
}
@@ -16079,9 +16334,17 @@
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
},
"has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"requires": {
"has-symbols": "^1.0.3"
}
},
"hasha": {
"version": "5.2.2",
@@ -16092,6 +16355,14 @@
"type-fest": "^0.8.0"
}
},
"hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"requires": {
"function-bind": "^1.1.2"
}
},
"hast-util-from-parse5": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz",
@@ -16562,9 +16833,9 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
@@ -16899,6 +17170,11 @@
"resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.3.8.tgz",
"integrity": "sha512-9FbRY3i6U+CbHgrdNbAUaisjWTozkm1ZfupYQJiZ87NtYHk2Zh9DvxMgp/fifxVhqTLpd5fCCLossUbpZxGeKw=="
},
"math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
},
"mdast-util-definitions": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz",
@@ -33,5 +33,10 @@
"cypress": "^11.2.0",
"eslint-plugin-cypress": "^3.5.0",
"tscw-config": "^1.1.2"
},
"overrides": {
"cypress": {
"form-data": "^2.3.4"
}
}
}
+2676 -2807
View File
File diff suppressed because it is too large Load Diff
+12 -15
View File
@@ -160,7 +160,7 @@
"geostyler-openlayers-parser": "^4.3.0",
"geostyler-style": "7.5.0",
"geostyler-wfs-parser": "^2.0.3",
"googleapis": "^168.0.0",
"googleapis": "^169.0.0",
"immer": "^11.0.1",
"interweave": "^13.1.1",
"jquery": "^3.7.1",
@@ -175,10 +175,10 @@
"memoize-one": "^5.2.1",
"mousetrap": "^1.6.5",
"mustache": "^4.2.0",
"nanoid": "^5.0.9",
"nanoid": "^5.1.6",
"ol": "^7.5.2",
"prop-types": "^15.8.1",
"re-resizable": "^6.10.1",
"re-resizable": "^6.11.2",
"react": "^17.0.2",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^3.4.0",
@@ -234,7 +234,7 @@
"@babel/preset-typescript": "^7.26.0",
"@babel/register": "^7.23.7",
"@babel/runtime": "^7.28.4",
"@babel/runtime-corejs3": "^7.28.2",
"@babel/runtime-corejs3": "^7.28.4",
"@babel/types": "^7.26.9",
"@cypress/react": "^8.0.2",
"@emotion/babel-plugin": "^11.13.5",
@@ -242,8 +242,8 @@
"@hot-loader/react-dom": "^17.0.2",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@mihkeleidast/storybook-addon-source": "^1.0.1",
"@playwright/test": "^1.56.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.17",
"@playwright/test": "^1.57.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-actions": "8.6.14",
"@storybook/addon-controls": "8.6.14",
"@storybook/addon-essentials": "8.6.14",
@@ -269,18 +269,15 @@
"@types/jest": "^30.0.0",
"@types/js-levenshtein": "^1.1.3",
"@types/json-bigint": "^1.0.4",
"@types/math-expression-evaluator": "^2.0.0",
"@types/mousetrap": "^1.6.15",
"@types/node": "^25.0.2",
"@types/node": "^25.0.3",
"@types/react": "^17.0.83",
"@types/react-dom": "^17.0.26",
"@types/react-json-tree": "^0.13.0",
"@types/react-loadable": "^5.5.11",
"@types/react-redux": "^7.1.10",
"@types/react-resizable": "^3.0.8",
"@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "^4.4.12",
"@types/react-virtualized-auto-sizer": "^1.0.8",
"@types/react-window": "^1.8.8",
"@types/redux-localstorage": "^1.0.8",
"@types/redux-mock-store": "^1.0.6",
@@ -295,7 +292,7 @@
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-typescript-to-proptypes": "^2.0.0",
"baseline-browser-mapping": "^2.9.7",
"baseline-browser-mapping": "^2.9.9",
"cheerio": "1.1.0",
"concurrently": "^9.2.1",
"copy-webpack-plugin": "^13.0.1",
@@ -326,7 +323,7 @@
"fetch-mock": "^11.1.5",
"fork-ts-checker-webpack-plugin": "^9.1.0",
"history": "^5.3.0",
"html-webpack-plugin": "^5.6.4",
"html-webpack-plugin": "^5.6.5",
"http-server": "^14.1.1",
"imports-loader": "^5.0.0",
"jest": "^30.2.0",
@@ -350,7 +347,7 @@
"source-map": "^0.7.4",
"source-map-support": "^0.5.21",
"speed-measure-webpack-plugin": "^1.5.0",
"storybook": "8.6.14",
"storybook": "8.6.15",
"style-loader": "^4.0.0",
"swc-loader": "^0.2.6",
"terser-webpack-plugin": "^5.3.16",
@@ -363,7 +360,7 @@
"vm-browserify": "^1.1.2",
"wait-on": "^9.0.3",
"webpack": "^5.103.0",
"webpack-bundle-analyzer": "^4.10.1",
"webpack-bundle-analyzer": "^5.1.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.2",
"webpack-manifest-plugin": "^5.0.1",
@@ -386,7 +383,7 @@
"d3-color": "^3.1.0",
"puppeteer": "^22.4.1",
"underscore": "^1.13.7",
"jspdf": "^3.0.1",
"jspdf": "^3.0.2",
"nwsapi": "^2.2.13",
"@deck.gl/aggregation-layers": "~9.2.2",
"@deck.gl/core": "~9.2.2",
@@ -1,6 +1,6 @@
{
"name": "@apache-superset/core",
"version": "0.0.1-rc5",
"version": "0.0.1-rc6",
"description": "This package contains UI elements, APIs, and utility functions used by Superset.",
"sideEffects": false,
"main": "lib/index.js",
@@ -17,7 +17,7 @@
"@babel/preset-react": "^7.26.3",
"@babel/preset-typescript": "^7.26.0",
"install": "^0.13.0",
"npm": "^11.1.0",
"npm": "^11.7.0",
"typescript": "^5.0.0",
"@emotion/styled": "^11.14.1",
"@types/lodash": "^4.17.21",
@@ -32,7 +32,7 @@
"ag-grid-community": "34.3.1",
"ag-grid-react": "34.3.1",
"brace": "^0.11.1",
"classnames": "^2.2.5",
"classnames": "^2.5.1",
"csstype": "^3.1.3",
"core-js": "^3.38.1",
"d3-format": "^1.3.2",
@@ -78,8 +78,7 @@
"@types/react-syntax-highlighter": "^15.5.13",
"@types/jquery": "^3.5.33",
"@types/lodash": "^4.17.21",
"@types/math-expression-evaluator": "^2.0.0",
"@types/node": "^25.0.2",
"@types/node": "^25.0.3",
"@types/prop-types": "^15.7.15",
"@types/rison": "0.1.0",
"@types/seedrandom": "^3.0.8",
@@ -149,6 +149,7 @@ import {
CompressOutlined,
HistoryOutlined,
SlackOutlined,
ApiOutlined,
} from '@ant-design/icons';
import { FC } from 'react';
import { IconType } from './types';
@@ -287,6 +288,7 @@ const AntdIcons = {
CompressOutlined,
HistoryOutlined,
SlackOutlined,
ApiOutlined,
} as const;
type AntdIconNames = keyof typeof AntdIcons;
@@ -240,3 +240,86 @@ test('should call setSortBy when clicking sortable column header', () => {
},
]);
});
test('should not apply highlight class when highlightRowId is undefined', () => {
const propsWithoutHighlight = {
...defaultProps,
highlightRowId: undefined,
};
const { container } = render(<TableCollection {...propsWithoutHighlight} />);
// Check that no rows have the highlight class
const highlightedRows = container.querySelectorAll('.table-row-highlighted');
expect(highlightedRows).toHaveLength(0);
});
test('should not apply highlight class when highlightRowId is null', () => {
const propsWithNullHighlight = {
...defaultProps,
highlightRowId: null,
};
const { container } = render(<TableCollection {...propsWithNullHighlight} />);
// Check that no rows have the highlight class
const highlightedRows = container.querySelectorAll('.table-row-highlighted');
expect(highlightedRows).toHaveLength(0);
});
test('should apply highlight class only to matching row when highlightRowId is provided', () => {
// Create data where the first row has id: 1 to match highlightRowId: 1
const dataWithIds = [
{
col1: 'Line 01 - Col 01',
col2: 'Line 01 - Col 02',
id: 1, // This should be highlighted
parent: { child: 'Nested Value 1' },
},
{
col1: 'Line 02 - Col 01',
col2: 'Line 02 - Col 02',
id: 2,
parent: { child: 'Nested Value 2' },
},
{
col1: 'Line 03 - Col 01',
col2: 'Line 03 - Col 02',
id: 3,
parent: { child: 'Nested Value 3' },
},
];
// Create new table hook with data that has ids
const { result } = renderHook(() =>
useTable({ columns: tableHook.columns, data: dataWithIds }),
);
const newTableHook = result.current;
const propsWithHighlight = {
...defaultProps,
highlightRowId: 1,
rows: newTableHook.rows,
prepareRow: newTableHook.prepareRow,
};
const { container } = render(<TableCollection {...propsWithHighlight} />);
// Check that only one row has the highlight class
const highlightedRows = container.querySelectorAll('.table-row-highlighted');
expect(highlightedRows).toHaveLength(1);
});
test('should not apply highlight when records have no id field and highlightRowId is undefined', () => {
// This is the key test for the bug fix - use original data without id field
const propsWithNoIds = {
...defaultProps,
highlightRowId: undefined,
};
const { container } = render(<TableCollection {...propsWithNoIds} />);
// Check that no rows have the highlight class (was the bug: all rows were highlighted)
const highlightedRows = container.querySelectorAll('.table-row-highlighted');
expect(highlightedRows).toHaveLength(0);
});
@@ -280,7 +280,9 @@ function TableCollection<T extends object>({
const getRowClassName = useCallback(
(record: Record<string, unknown>) =>
record?.id === highlightRowId ? 'table-row-highlighted' : '',
highlightRowId !== undefined && record?.id === highlightRowId
? 'table-row-highlighted'
: '',
[highlightRowId],
);
@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import type { FC } from 'react';
import { css, styled, useTheme } from '@apache-superset/core/ui';
// eslint-disable-next-line no-restricted-imports
@@ -161,7 +162,9 @@ export const StyledLineEditableTabs = styled(EditableTabs)`
}
`;
export const LineEditableTabs = Object.assign(StyledLineEditableTabs, {
export const LineEditableTabs: FC<TabsProps> & {
TabPane: typeof StyledTabPane;
} = Object.assign(StyledLineEditableTabs, {
TabPane: StyledTabPane,
});
@@ -26,6 +26,7 @@ export enum FeatureFlag {
AlertReports = 'ALERT_REPORTS',
AlertReportTabs = 'ALERT_REPORT_TABS',
AlertReportSlackV2 = 'ALERT_REPORT_SLACK_V2',
AlertReportWebhook = 'ALERT_REPORT_WEBHOOK',
AlertReportsFilter = 'ALERT_REPORTS_FILTER',
AllowFullCsvExport = 'ALLOW_FULL_CSV_EXPORT',
AvoidColorsCollision = 'AVOID_COLORS_COLLISION',
@@ -25,6 +25,7 @@ import { createTooltipContent } from '../../utilities/tooltipUtils';
import TooltipRow from '../../TooltipRow';
import { unitToRadius } from '../../utils/geo';
import { HIGHLIGHT_COLOR_ARRAY } from '../../utils';
import { isMetricValue, extractMetricKey } from '../utils/metricUtils';
function getMetricLabel(metric: any) {
if (typeof metric === 'string') {
@@ -44,9 +45,18 @@ function setTooltipContent(
verboseMap?: Record<string, string>,
) {
const defaultTooltipGenerator = (o: JsonObject) => {
const label =
verboseMap?.[formData.point_radius_fixed.value] ||
getMetricLabel(formData.point_radius_fixed?.value);
// Only show metric info if point_radius_fixed is metric-based
let metricKey = null;
if (isMetricValue(formData.point_radius_fixed)) {
metricKey = extractMetricKey(formData.point_radius_fixed?.value);
}
// Normalize metricKey for verboseMap lookup
const lookupKey = typeof metricKey === 'string' ? metricKey : null;
const label = lookupKey
? verboseMap?.[lookupKey] || getMetricLabel(lookupKey)
: null;
return (
<div className="deckgl-tooltip">
<TooltipRow
@@ -59,7 +69,7 @@ function setTooltipContent(
value={`${o.object?.cat_color}`}
/>
)}
{o.object?.metric && (
{o.object?.metric && label && (
<TooltipRow label={`${label}: `} value={`${o.object?.metric}`} />
)}
</div>
@@ -0,0 +1,312 @@
/**
* 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 buildQuery, { DeckScatterFormData } from './buildQuery';
const baseFormData: DeckScatterFormData = {
datasource: '1__table',
viz_type: 'deck_scatter',
spatial: {
type: 'latlong',
latCol: 'LATITUDE',
lonCol: 'LONGITUDE',
},
row_limit: 100,
};
test('Scatter buildQuery should not include metric when point_radius_fixed is fixed type', () => {
const formData: DeckScatterFormData = {
...baseFormData,
point_radius_fixed: {
type: 'fix',
value: '1000',
},
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([]);
expect(query.orderby).toEqual([]);
});
test('Scatter buildQuery should include metric when point_radius_fixed is metric type', () => {
const formData: DeckScatterFormData = {
...baseFormData,
point_radius_fixed: {
type: 'metric',
value: 'AVG(radius_value)',
},
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
expect(query.metrics).toContain('AVG(radius_value)');
expect(query.orderby).toEqual([['AVG(radius_value)', false]]);
});
test('Scatter buildQuery should handle numeric value in fixed type', () => {
const formData: DeckScatterFormData = {
...baseFormData,
point_radius_fixed: {
type: 'fix',
value: 500,
},
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
// Fixed numeric value should not be included as a metric
expect(query.metrics).toEqual([]);
expect(query.orderby).toEqual([]);
});
test('Scatter buildQuery should handle missing point_radius_fixed', () => {
const formData: DeckScatterFormData = {
...baseFormData,
// no point_radius_fixed
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([]);
expect(query.orderby).toEqual([]);
});
test('Scatter buildQuery should include spatial columns in query', () => {
const formData: DeckScatterFormData = {
...baseFormData,
point_radius_fixed: {
type: 'fix',
value: '1000',
},
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
expect(query.columns).toContain('LATITUDE');
expect(query.columns).toContain('LONGITUDE');
});
test('Scatter buildQuery should include dimension column when specified', () => {
const formData: DeckScatterFormData = {
...baseFormData,
dimension: 'category',
point_radius_fixed: {
type: 'fix',
value: '1000',
},
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
expect(query.columns).toContain('category');
});
test('Scatter buildQuery should add spatial null filters', () => {
const formData: DeckScatterFormData = {
...baseFormData,
point_radius_fixed: {
type: 'fix',
value: '1000',
},
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
const latFilter = query.filters?.find(
f => f.col === 'LATITUDE' && f.op === 'IS NOT NULL',
);
const lonFilter = query.filters?.find(
f => f.col === 'LONGITUDE' && f.op === 'IS NOT NULL',
);
expect(latFilter).toBeDefined();
expect(lonFilter).toBeDefined();
});
test('Scatter buildQuery should throw error when spatial is missing', () => {
const formData: DeckScatterFormData = {
...baseFormData,
spatial: undefined,
};
expect(() => buildQuery(formData)).toThrow(
'Spatial configuration is required for Scatter charts',
);
});
test('Scatter buildQuery should handle geohash spatial type', () => {
const formData: DeckScatterFormData = {
...baseFormData,
spatial: {
type: 'geohash',
geohashCol: 'geohash_column',
},
point_radius_fixed: {
type: 'metric',
value: 'COUNT(*)',
},
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
expect(query.columns).toContain('geohash_column');
expect(query.metrics).toContain('COUNT(*)');
});
test('Scatter buildQuery should handle tooltip_contents', () => {
const formData: DeckScatterFormData = {
...baseFormData,
tooltip_contents: ['name', 'description'],
point_radius_fixed: {
type: 'fix',
value: '1000',
},
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
expect(query.columns).toContain('name');
expect(query.columns).toContain('description');
});
test('Scatter buildQuery should handle js_columns', () => {
const formData: DeckScatterFormData = {
...baseFormData,
js_columns: ['custom_col1', 'custom_col2'],
point_radius_fixed: {
type: 'fix',
value: '1000',
},
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
expect(query.columns).toContain('custom_col1');
expect(query.columns).toContain('custom_col2');
});
test('Scatter buildQuery should convert numeric metric value to string', () => {
const formData: DeckScatterFormData = {
...baseFormData,
point_radius_fixed: {
type: 'metric',
value: 123, // numeric metric (edge case)
},
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
expect(query.metrics).toContain('123');
expect(query.orderby).toEqual([['123', false]]);
});
test('Scatter buildQuery should set is_timeseries to false', () => {
const formData: DeckScatterFormData = {
...baseFormData,
point_radius_fixed: {
type: 'fix',
value: '1000',
},
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
expect(query.is_timeseries).toBe(false);
});
test('Scatter buildQuery should preserve row_limit', () => {
const formData: DeckScatterFormData = {
...baseFormData,
row_limit: 5000,
point_radius_fixed: {
type: 'fix',
value: '1000',
},
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
expect(query.row_limit).toBe(5000);
});
test('Scatter buildQuery should preserve existing metrics when adding radius metric', () => {
const formData: DeckScatterFormData = {
...baseFormData,
metrics: ['COUNT(*)'],
point_radius_fixed: {
type: 'metric',
value: 'AVG(radius_value)',
},
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
expect(query.metrics).toContain('COUNT(*)');
expect(query.metrics).toContain('AVG(radius_value)');
expect(query.metrics).toHaveLength(2);
});
test('Scatter buildQuery should not modify existing metrics for fixed radius', () => {
const formData: DeckScatterFormData = {
...baseFormData,
metrics: ['COUNT(*)', 'SUM(value)'],
point_radius_fixed: {
type: 'fix',
value: '1000',
},
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
expect(query.metrics).toEqual(['COUNT(*)', 'SUM(value)']);
});
test('Scatter buildQuery should deduplicate metrics when radius metric already exists', () => {
const formData: DeckScatterFormData = {
...baseFormData,
metrics: ['COUNT(*)', 'AVG(price)'],
point_radius_fixed: {
type: 'metric',
value: 'AVG(price)',
},
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
// Should not have duplicate AVG(price)
expect(query.metrics).toEqual(['COUNT(*)', 'AVG(price)']);
expect(query.metrics).toHaveLength(2);
});
@@ -34,11 +34,13 @@ import {
processMetricsArray,
addTooltipColumnsToQuery,
} from '../buildQueryUtils';
import { isMetricValue, extractMetricKey } from '../utils/metricUtils';
export interface DeckScatterFormData
extends Omit<SpatialFormData, 'color_picker'>, SqlaFormData {
point_radius_fixed?: {
value?: string;
type?: 'fix' | 'metric';
value?: string | number;
};
multiplier?: number;
point_unit?: string;
@@ -78,15 +80,29 @@ export default function buildQuery(formData: DeckScatterFormData) {
columns = withJsColumns as QueryFormColumn[];
columns = addTooltipColumnsToQuery(columns, tooltip_contents);
const metrics = processMetricsArray([point_radius_fixed?.value]);
// Only add metric if point_radius_fixed is a metric type
const isMetric = isMetricValue(point_radius_fixed);
const metricValue = isMetric
? extractMetricKey(point_radius_fixed?.value)
: null;
// Preserve existing metrics and only add radius metric if it's metric-based
const existingMetrics = baseQueryObject.metrics || [];
const radiusMetrics = processMetricsArray(
metricValue ? [metricValue] : [],
);
// Deduplicate metrics to avoid adding the same metric twice
const metricsSet = new Set([...existingMetrics, ...radiusMetrics]);
const metrics = Array.from(metricsSet);
const filters = addSpatialNullFilters(
spatial,
ensureIsArray(baseQueryObject.filters || []),
);
const orderby = point_radius_fixed?.value
? ([[point_radius_fixed.value, false]] as QueryFormOrderBy[])
: (baseQueryObject.orderby as QueryFormOrderBy[]) || [];
const orderby =
isMetric && metricValue
? ([[metricValue, false]] as QueryFormOrderBy[])
: (baseQueryObject.orderby as QueryFormOrderBy[]) || [];
return [
{
@@ -0,0 +1,303 @@
/**
* 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 { ChartProps, DatasourceType } from '@superset-ui/core';
import transformProps from './transformProps';
interface ScatterFeature {
position: [number, number];
radius?: number;
metric?: number;
cat_color?: string;
extraProps?: Record<string, unknown>;
}
jest.mock('../spatialUtils', () => ({
...jest.requireActual('../spatialUtils'),
getMapboxApiKey: jest.fn(() => 'mock-mapbox-key'),
}));
const mockChartProps: Partial<ChartProps> = {
rawFormData: {
spatial: {
type: 'latlong',
latCol: 'LATITUDE',
lonCol: 'LONGITUDE',
},
viewport: {},
},
queriesData: [
{
data: [
{
LATITUDE: 37.8,
LONGITUDE: -122.4,
population: 50000,
'AVG(radius_value)': 100,
},
{
LATITUDE: 37.9,
LONGITUDE: -122.3,
population: 75000,
'AVG(radius_value)': 200,
},
],
},
],
datasource: {
type: DatasourceType.Table,
id: 1,
name: 'test_datasource',
columns: [],
metrics: [],
},
height: 400,
width: 600,
hooks: {},
filterState: {},
emitCrossFilters: false,
};
test('Scatter transformProps should use fixed radius value when point_radius_fixed type is "fix"', () => {
const fixedProps = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
point_radius_fixed: {
type: 'fix',
value: '1000',
},
},
};
const result = transformProps(fixedProps as ChartProps);
const features = result.payload.data.features as ScatterFeature[];
expect(features).toHaveLength(2);
expect(features[0]?.radius).toBe(1000);
expect(features[1]?.radius).toBe(1000);
// metric should not be set for fixed radius
expect(features[0]?.metric).toBeUndefined();
expect(features[1]?.metric).toBeUndefined();
});
test('Scatter transformProps should use metric value for radius when point_radius_fixed type is "metric"', () => {
const metricProps = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
point_radius_fixed: {
type: 'metric',
value: 'AVG(radius_value)',
},
},
};
const result = transformProps(metricProps as ChartProps);
const features = result.payload.data.features as ScatterFeature[];
expect(features).toHaveLength(2);
expect(features[0]?.radius).toBe(100);
expect(features[0]?.metric).toBe(100);
expect(features[1]?.radius).toBe(200);
expect(features[1]?.metric).toBe(200);
});
test('Scatter transformProps should handle numeric fixed radius value', () => {
const fixedProps = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
point_radius_fixed: {
type: 'fix',
value: 500, // numeric value
},
},
};
const result = transformProps(fixedProps as ChartProps);
const features = result.payload.data.features as ScatterFeature[];
expect(features).toHaveLength(2);
expect(features[0]?.radius).toBe(500);
expect(features[1]?.radius).toBe(500);
});
test('Scatter transformProps should handle missing point_radius_fixed', () => {
const propsWithoutRadius = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
// no point_radius_fixed
},
};
const result = transformProps(propsWithoutRadius as ChartProps);
const features = result.payload.data.features as ScatterFeature[];
expect(features).toHaveLength(2);
// radius should not be set
expect(features[0]?.radius).toBeUndefined();
expect(features[1]?.radius).toBeUndefined();
});
test('Scatter transformProps should handle dimension for category colors', () => {
const propsWithDimension = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
dimension: 'category',
point_radius_fixed: {
type: 'fix',
value: '1000',
},
},
queriesData: [
{
data: [
{
LATITUDE: 37.8,
LONGITUDE: -122.4,
category: 'A',
},
{
LATITUDE: 37.9,
LONGITUDE: -122.3,
category: 'B',
},
],
},
],
};
const result = transformProps(propsWithDimension as ChartProps);
const features = result.payload.data.features as ScatterFeature[];
expect(features).toHaveLength(2);
expect(features[0]?.cat_color).toBe('A');
expect(features[1]?.cat_color).toBe('B');
});
test('Scatter transformProps should not include metric labels for fixed radius', () => {
const fixedProps = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
point_radius_fixed: {
type: 'fix',
value: '1000',
},
},
};
const result = transformProps(fixedProps as ChartProps);
// metricLabels should be empty for fixed radius
expect(result.payload.data.metricLabels).toEqual([]);
});
test('Scatter transformProps should include metric labels for metric-based radius', () => {
const metricProps = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
point_radius_fixed: {
type: 'metric',
value: 'AVG(radius_value)',
},
},
};
const result = transformProps(metricProps as ChartProps);
// metricLabels should include the metric name
expect(result.payload.data.metricLabels).toContain('AVG(radius_value)');
});
test('Scatter transformProps should handle no records', () => {
const noDataProps = {
...mockChartProps,
queriesData: [{ data: [] }],
rawFormData: {
...mockChartProps.rawFormData,
point_radius_fixed: {
type: 'fix',
value: '1000',
},
},
};
const result = transformProps(noDataProps as ChartProps);
const features = result.payload.data.features as ScatterFeature[];
expect(features).toHaveLength(0);
});
test('Scatter transformProps should handle missing spatial configuration gracefully', () => {
const noSpatialProps = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
spatial: undefined,
point_radius_fixed: {
type: 'fix',
value: '1000',
},
},
};
const result = transformProps(noSpatialProps as ChartProps);
const features = result.payload.data.features as ScatterFeature[];
expect(features).toHaveLength(0);
});
test('Scatter transformProps should preserve extra properties from records', () => {
const propsWithExtraData = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
point_radius_fixed: {
type: 'fix',
value: '1000',
},
},
queriesData: [
{
data: [
{
LATITUDE: 37.8,
LONGITUDE: -122.4,
custom_field: 'value1',
another_field: 123,
},
],
},
],
};
const result = transformProps(propsWithExtraData as ChartProps);
const features = result.payload.data.features as ScatterFeature[];
expect(features).toHaveLength(1);
expect(features[0]).toMatchObject({
custom_field: 'value1',
another_field: 123,
});
});
@@ -26,6 +26,7 @@ import {
addPropertiesToFeature,
} from '../transformUtils';
import { DeckScatterFormData } from './buildQuery';
import { isFixedValue, getFixedValue } from '../utils/metricUtils';
interface ScatterPoint {
position: [number, number];
@@ -43,6 +44,7 @@ function processScatterData(
radiusMetricLabel?: string,
categoryColumn?: string,
jsColumns?: string[],
fixedRadiusValue?: number | string | null,
): ScatterPoint[] {
if (!spatial || !records.length) {
return [];
@@ -72,7 +74,15 @@ function processScatterData(
extraProps: feature.extraProps || {},
};
if (radiusMetricLabel && feature[radiusMetricLabel] != null) {
// Handle radius: either from metric or fixed value
if (fixedRadiusValue != null) {
// Use fixed radius value for all points
const parsedFixedRadius = parseMetricValue(fixedRadiusValue);
if (parsedFixedRadius !== undefined) {
scatterPoint.radius = parsedFixedRadius;
}
} else if (radiusMetricLabel && feature[radiusMetricLabel] != null) {
// Use metric value for radius
const radiusValue = parseMetricValue(feature[radiusMetricLabel]);
if (radiusValue !== undefined) {
scatterPoint.radius = radiusValue;
@@ -98,14 +108,21 @@ export default function transformProps(chartProps: ChartProps) {
const { spatial, point_radius_fixed, dimension, js_columns } =
formData as DeckScatterFormData;
// Check if this is a fixed value or metric
const fixedRadiusValue = isFixedValue(point_radius_fixed)
? getFixedValue(point_radius_fixed)
: null;
const radiusMetricLabel = getMetricLabelFromFormData(point_radius_fixed);
const records = getRecordsFromQuery(chartProps.queriesData);
const features = processScatterData(
records,
spatial,
radiusMetricLabel,
dimension,
js_columns,
fixedRadiusValue,
);
return createBaseTransformResult(
@@ -0,0 +1,184 @@
/**
* 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 { getMetricLabel } from '@superset-ui/core';
import { getMetricLabelFromFormData, parseMetricValue } from './transformUtils';
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
getMetricLabel: jest.fn((metric: string) => metric),
}));
beforeEach(() => {
jest.clearAllMocks();
});
test('getMetricLabelFromFormData should return undefined for undefined input', () => {
const result = getMetricLabelFromFormData(undefined);
expect(result).toBeUndefined();
});
test('getMetricLabelFromFormData should return undefined for null input', () => {
const result = getMetricLabelFromFormData(null as any);
expect(result).toBeUndefined();
});
test('getMetricLabelFromFormData should handle string metric directly', () => {
const result = getMetricLabelFromFormData('AVG(value)');
expect(result).toBe('AVG(value)');
expect(getMetricLabel).toHaveBeenCalledWith('AVG(value)');
});
test('getMetricLabelFromFormData should return undefined for fixed type', () => {
const result = getMetricLabelFromFormData({
type: 'fix',
value: '1000',
});
expect(result).toBeUndefined();
expect(getMetricLabel).not.toHaveBeenCalled();
});
test('getMetricLabelFromFormData should return undefined for fixed type with numeric value', () => {
const result = getMetricLabelFromFormData({
type: 'fix',
value: 1000,
});
expect(result).toBeUndefined();
expect(getMetricLabel).not.toHaveBeenCalled();
});
test('getMetricLabelFromFormData should return metric label for metric type with string value', () => {
const result = getMetricLabelFromFormData({
type: 'metric',
value: 'SUM(amount)',
});
expect(result).toBe('SUM(amount)');
expect(getMetricLabel).toHaveBeenCalledWith('SUM(amount)');
});
test('getMetricLabelFromFormData should handle object metric values', () => {
const result = getMetricLabelFromFormData({
type: 'metric',
value: {
label: 'Total Sales',
sqlExpression: 'SUM(sales)',
},
});
expect(result).toBe('Total Sales');
expect(getMetricLabel).toHaveBeenCalledWith('Total Sales');
});
test('getMetricLabelFromFormData should use sqlExpression if label is missing', () => {
const result = getMetricLabelFromFormData({
type: 'metric',
value: {
sqlExpression: 'COUNT(*)',
},
});
expect(result).toBe('COUNT(*)');
expect(getMetricLabel).toHaveBeenCalledWith('COUNT(*)');
});
test('getMetricLabelFromFormData should use value field as fallback', () => {
const result = getMetricLabelFromFormData({
type: 'metric',
value: {
value: 'AVG(price)',
},
});
expect(result).toBe('AVG(price)');
expect(getMetricLabel).toHaveBeenCalledWith('AVG(price)');
});
test('getMetricLabelFromFormData should handle metric type with numeric value', () => {
const result = getMetricLabelFromFormData({
type: 'metric',
value: 123,
});
expect(result).toBe('123');
expect(getMetricLabel).toHaveBeenCalledWith('123');
});
test('getMetricLabelFromFormData should return undefined for object without type', () => {
const result = getMetricLabelFromFormData({
value: 'AVG(value)',
});
expect(result).toBeUndefined();
});
test('getMetricLabelFromFormData should return undefined for empty object', () => {
const result = getMetricLabelFromFormData({});
expect(result).toBeUndefined();
});
test('getMetricLabelFromFormData should return undefined for metric type without value', () => {
const result = getMetricLabelFromFormData({
type: 'metric',
});
expect(result).toBeUndefined();
});
test('parseMetricValue should parse numeric strings', () => {
expect(parseMetricValue('123')).toBe(123);
expect(parseMetricValue('123.45')).toBe(123.45);
expect(parseMetricValue('0')).toBe(0);
expect(parseMetricValue('-123')).toBe(-123);
});
test('parseMetricValue should handle numbers directly', () => {
expect(parseMetricValue(123)).toBe(123);
expect(parseMetricValue(123.45)).toBe(123.45);
expect(parseMetricValue(0)).toBe(0);
expect(parseMetricValue(-123)).toBe(-123);
});
test('parseMetricValue should return undefined for null', () => {
expect(parseMetricValue(null)).toBeUndefined();
});
test('parseMetricValue should return undefined for undefined', () => {
expect(parseMetricValue(undefined)).toBeUndefined();
});
test('parseMetricValue should return undefined for non-numeric strings', () => {
expect(parseMetricValue('abc')).toBeUndefined();
expect(parseMetricValue('12a34')).toBe(12); // parseFloat returns 12
expect(parseMetricValue('')).toBeUndefined();
});
test('parseMetricValue should handle edge cases', () => {
expect(parseMetricValue('Infinity')).toBe(Infinity);
expect(parseMetricValue('-Infinity')).toBe(-Infinity);
expect(parseMetricValue('NaN')).toBeUndefined();
});
test('parseMetricValue should handle boolean values', () => {
expect(parseMetricValue(true as any)).toBeUndefined();
expect(parseMetricValue(false as any)).toBeUndefined();
});
test('parseMetricValue should handle objects', () => {
expect(parseMetricValue({} as any)).toBeUndefined();
expect(parseMetricValue({ value: 123 } as any)).toBeUndefined();
});
test('parseMetricValue should handle arrays', () => {
expect(parseMetricValue([] as any)).toBeUndefined();
expect(parseMetricValue([123] as any)).toBe(123); // String([123]) = '123'
});
@@ -16,8 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ChartProps, getMetricLabel } from '@superset-ui/core';
import { ChartProps } from '@superset-ui/core';
import { getMapboxApiKey, DataRecord } from './spatialUtils';
import {
getMetricLabelFromValue,
FixedOrMetricValue,
} from './utils/metricUtils';
const NOOP = () => {};
@@ -134,9 +138,7 @@ export function addPropertiesToFeature<T extends Record<string, unknown>>(
}
export function getMetricLabelFromFormData(
metric: string | { value?: string } | undefined,
metric: string | FixedOrMetricValue | undefined | null,
): string | undefined {
if (!metric) return undefined;
if (typeof metric === 'string') return getMetricLabel(metric);
return metric.value ? getMetricLabel(metric.value) : undefined;
return getMetricLabelFromValue(metric);
}
@@ -0,0 +1,121 @@
/**
* 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 { getMetricLabel } from '@superset-ui/core';
import {
isMetricValue,
isFixedValue,
extractMetricKey,
getMetricLabelFromValue,
getFixedValue,
} from './metricUtils';
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
getMetricLabel: jest.fn((metric: string) => metric),
}));
beforeEach(() => {
jest.clearAllMocks();
});
test('isMetricValue should identify metric values correctly', () => {
expect(isMetricValue({ type: 'metric', value: 'COUNT(*)' })).toBe(true);
expect(isMetricValue({ type: 'fix', value: '1000' })).toBe(false);
expect(isMetricValue('AVG(value)')).toBe(true); // legacy string format
expect(isMetricValue(undefined)).toBe(false);
expect(isMetricValue(null)).toBe(false);
});
test('isFixedValue should identify fixed values correctly', () => {
expect(isFixedValue({ type: 'fix', value: '1000' })).toBe(true);
expect(isFixedValue({ type: 'metric', value: 'COUNT(*)' })).toBe(false);
expect(isFixedValue('AVG(value)')).toBe(false); // legacy string format
expect(isFixedValue(undefined)).toBe(false);
expect(isFixedValue(null)).toBe(false);
});
test('extractMetricKey should handle string values', () => {
expect(extractMetricKey('COUNT(*)')).toBe('COUNT(*)');
expect(extractMetricKey('')).toBe('');
});
test('extractMetricKey should handle number values', () => {
expect(extractMetricKey(123)).toBe('123');
expect(extractMetricKey(0)).toBe('0');
});
test('extractMetricKey should extract from object properties', () => {
expect(extractMetricKey({ label: 'Total Sales' })).toBe('Total Sales');
expect(extractMetricKey({ sqlExpression: 'SUM(sales)' })).toBe('SUM(sales)');
expect(extractMetricKey({ value: 'AVG(price)' })).toBe('AVG(price)');
expect(
extractMetricKey({ label: 'Label', sqlExpression: 'SQL', value: 'Value' }),
).toBe('Label'); // priority order
});
test('extractMetricKey should handle null/undefined', () => {
expect(extractMetricKey(null)).toBeUndefined();
expect(extractMetricKey(undefined)).toBeUndefined();
expect(extractMetricKey({})).toBeUndefined();
});
test('getMetricLabelFromValue should return label for metric values', () => {
getMetricLabelFromValue({ type: 'metric', value: 'COUNT(*)' });
expect(getMetricLabel).toHaveBeenCalledWith('COUNT(*)');
getMetricLabelFromValue({ type: 'metric', value: { label: 'Total Sales' } });
expect(getMetricLabel).toHaveBeenCalledWith('Total Sales');
});
test('getMetricLabelFromValue should return undefined for fixed values', () => {
const result = getMetricLabelFromValue({ type: 'fix', value: '1000' });
expect(result).toBeUndefined();
expect(getMetricLabel).not.toHaveBeenCalled();
});
test('getMetricLabelFromValue should handle legacy string format', () => {
getMetricLabelFromValue('AVG(value)');
expect(getMetricLabel).toHaveBeenCalledWith('AVG(value)');
});
test('getFixedValue should return value for fixed types', () => {
expect(getFixedValue({ type: 'fix', value: '1000' })).toBe('1000');
expect(getFixedValue({ type: 'fix', value: 500 })).toBe(500);
});
test('getFixedValue should return undefined for metric types', () => {
expect(getFixedValue({ type: 'metric', value: 'COUNT(*)' })).toBeUndefined();
});
test('getFixedValue should return undefined for string values', () => {
expect(getFixedValue('AVG(value)')).toBeUndefined();
});
test('getFixedValue should handle object values', () => {
expect(
getFixedValue({ type: 'fix', value: { label: 'object' } }),
).toBeUndefined();
});
test('getFixedValue should handle missing values', () => {
expect(getFixedValue({ type: 'fix' })).toBeUndefined();
expect(getFixedValue(undefined)).toBeUndefined();
expect(getFixedValue(null)).toBeUndefined();
});
@@ -0,0 +1,120 @@
/**
* 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 { getMetricLabel } from '@superset-ui/core';
export type MetricFormValue =
| string
| number
| { label?: string; sqlExpression?: string; value?: string }
| undefined
| null;
export interface FixedOrMetricValue {
type?: 'fix' | 'metric';
value?: MetricFormValue;
}
/**
* Checks if a value is configured as a metric (vs fixed value)
*/
export function isMetricValue(
fixedOrMetric: string | FixedOrMetricValue | undefined | null,
): boolean {
if (!fixedOrMetric) return false;
if (typeof fixedOrMetric === 'string') return true;
return fixedOrMetric.type === 'metric';
}
/**
* Checks if a value is configured as a fixed value (vs metric)
*/
export function isFixedValue(
fixedOrMetric: string | FixedOrMetricValue | undefined | null,
): boolean {
if (!fixedOrMetric) return false;
if (typeof fixedOrMetric === 'string') return false;
return fixedOrMetric.type === 'fix';
}
/**
* Extracts the metric key from a metric value object
* Handles label, sqlExpression, and value properties
*/
export function extractMetricKey(value: MetricFormValue): string | undefined {
if (value == null) return undefined;
if (typeof value === 'string') return value;
if (typeof value === 'number') return String(value);
// Handle object metrics (adhoc or saved metrics)
const metricObj = value as {
label?: string;
sqlExpression?: string;
value?: string;
};
return metricObj.label || metricObj.sqlExpression || metricObj.value;
}
/**
* Gets the metric label from a fixed/metric form value
* Returns undefined for fixed values, metric label for metric values
*/
export function getMetricLabelFromValue(
fixedOrMetric: string | FixedOrMetricValue | undefined | null,
): string | undefined {
if (!fixedOrMetric) return undefined;
// Legacy string format - treat as metric
if (typeof fixedOrMetric === 'string') {
return getMetricLabel(fixedOrMetric);
}
// Only return metric label if it's a metric type, not a fixed value
if (isMetricValue(fixedOrMetric) && fixedOrMetric.value) {
const metricKey = extractMetricKey(fixedOrMetric.value);
if (metricKey && typeof metricKey === 'string') {
return getMetricLabel(metricKey);
}
}
return undefined;
}
/**
* Gets the fixed value from a fixed/metric form value
* Returns the value for fixed types, undefined for metrics
*/
export function getFixedValue(
fixedOrMetric: string | FixedOrMetricValue | undefined | null,
): string | number | undefined {
if (!fixedOrMetric || typeof fixedOrMetric === 'string') {
return undefined;
}
if (isFixedValue(fixedOrMetric) && fixedOrMetric.value) {
if (
typeof fixedOrMetric.value === 'string' ||
typeof fixedOrMetric.value === 'number'
) {
return fixedOrMetric.value;
}
}
return undefined;
}
@@ -45,7 +45,6 @@
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "*",
"@types/classnames": "*",
"@types/react": "*",
"match-sorter": "^6.3.3",
"react": "^17.0.2",
@@ -67,11 +67,22 @@ function getValueRange(
alignPositiveNegative: boolean,
data: InputData[],
) {
if (typeof data?.[0]?.[key] === 'number') {
const nums = data.map(row => row[key]) as number[];
return (
alignPositiveNegative ? [0, d3Max(nums.map(Math.abs))] : d3Extent(nums)
) as ValueRange;
const nums = data
.map(row => {
const raw = row[key];
return raw instanceof Number ? raw.valueOf() : raw;
})
.filter(
(value): value is number =>
typeof value === 'number' && Number.isFinite(value),
) as number[];
if (nums.length > 0) {
const maxAbs = d3Max(nums.map(Math.abs));
if (alignPositiveNegative) {
return [0, maxAbs ?? 0] as ValueRange;
}
const extent = d3Extent(nums) as ValueRange | undefined;
return extent ?? [0, 0];
}
return null;
}
@@ -437,15 +437,21 @@ export function getLegendProps(
zoomable = false,
legendState?: LegendState,
padding?: LegendPaddingType,
): LegendComponentOption | LegendComponentOption[] {
const legend: LegendComponentOption | LegendComponentOption[] = {
): LegendComponentOption {
const isHorizontal =
orientation === LegendOrientation.Top ||
orientation === LegendOrientation.Bottom;
const effectiveType =
type === LegendType.Scroll || !isHorizontal ? type : LegendType.Scroll;
const legend: LegendComponentOption = {
orient: [LegendOrientation.Top, LegendOrientation.Bottom].includes(
orientation,
)
? 'horizontal'
: 'vertical',
show,
type,
type: effectiveType,
selected: legendState,
selector: ['all', 'inverse'],
selectorLabel: {
@@ -139,7 +139,6 @@ describe('Gantt transformProps', () => {
legend: expect.objectContaining({
show: true,
type: 'scroll',
selector: ['all', 'inverse'],
}),
tooltip: {
formatter: expect.anything(),
@@ -891,14 +891,27 @@ describe('getLegendProps', () => {
});
});
it('should return the correct props for plain type with bottom orientation', () => {
it('should default plain legends to scroll for bottom orientation', () => {
expect(
getLegendProps(LegendType.Plain, LegendOrientation.Bottom, false, theme),
).toEqual({
show: false,
bottom: 0,
orient: 'horizontal',
type: 'plain',
type: 'scroll',
...expectedThemeProps,
});
});
it('should default plain legends to scroll for top orientation', () => {
expect(
getLegendProps(LegendType.Plain, LegendOrientation.Top, false, theme),
).toEqual({
show: false,
top: 0,
right: 0,
orient: 'horizontal',
type: 'scroll',
...expectedThemeProps,
});
});
@@ -45,7 +45,6 @@
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "*",
"@types/classnames": "*",
"@types/react": "*",
"match-sorter": "^6.3.3",
"react": "^17.0.2",
@@ -344,7 +344,7 @@ function StickyWrap({
style={{
height: bodyHeight,
overflow: 'auto',
scrollbarGutter: 'stable',
scrollbarGutter: hasVerticalScroll ? 'stable' : undefined,
width: maxWidth,
boxSizing: 'border-box',
}}
@@ -385,7 +385,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
const nums = data
?.map(row => row?.[key])
.filter(value => typeof value === 'number') as number[];
if (data && nums.length === data.length) {
if (nums.length > 0) {
return (
alignPositiveNegative
? [0, d3Max(nums.map(Math.abs))]
@@ -958,6 +958,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
display: block;
top: 0;
${valueRange &&
typeof value === 'number' &&
`
width: ${`${cellWidth({
value: value as number,
@@ -26,6 +26,7 @@ import {
} from '@superset-ui/core/spec';
import { cloneDeep } from 'lodash';
import TableChart, { sanitizeHeaderId } from '../src/TableChart';
import { GenericDataType } from '@apache-superset/core/api/core';
import transformProps from '../src/transformProps';
import DateWithFormatter from '../src/utils/DateWithFormatter';
import testData from './testData';
@@ -803,6 +804,77 @@ describe('plugin-chart-table', () => {
cells = document.querySelectorAll('td');
});
test('render cell bars even when column contains NULL values', () => {
const props = transformProps({
...testData.raw,
queriesData: [
{
...testData.raw.queriesData[0],
colnames: ['category', 'value1', 'value2', 'value3', 'value4'],
coltypes: [
GenericDataType.String,
GenericDataType.Numeric,
GenericDataType.Numeric,
GenericDataType.Numeric,
GenericDataType.Numeric,
],
data: [
{
category: 'Category A',
value1: 10,
value2: 20,
value3: 30,
value4: null,
},
{
category: 'Category B',
value1: 15,
value2: 25,
value3: 35,
value4: 100,
},
{
category: 'Category C',
value1: 18,
value2: 28,
value3: 38,
value4: null,
},
],
},
],
rawFormData: {
...testData.raw.rawFormData,
show_cell_bars: true,
metrics: ['value1', 'value2', 'value3', 'value4'],
},
});
const { container } = render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
// Get all cell bars - should exist for both columns with and without NULL values
const cellBars = container.querySelectorAll('div.cell-bar');
// Should have cell bars in all numeric columns, even those with NULL values
// value1, value2, value3 all have 3 values, value4 has 1 non-NULL value
// Total: 3 + 3 + 3 + 1 = 10 cell bars
expect(cellBars.length).toBeGreaterThan(0);
// Specifically check that value4 column (which has NULLs) still renders bars for non-NULL cells
const rows = container.querySelectorAll('tbody tr');
expect(rows.length).toBe(3);
// Row 2 should have a cell bar in value4 column (value: 100)
const row2Cells = rows[1].querySelectorAll('td');
const value4Cell = row2Cells[4]; // 5th column (0-indexed)
const value4Bar = value4Cell.querySelector('div.cell-bar');
expect(value4Bar).toBeTruthy();
});
test('render color with string column color formatter(operator begins with)', () => {
render(
ProviderWrapper({
+6
View File
@@ -28,6 +28,7 @@ import { IntersectionObserver } from './IntersectionObserver';
import { ResizeObserver } from './ResizeObserver';
import setupSupersetClient from './setupSupersetClient';
import CacheStorage from './CacheStorage';
import { TextEncoder, TextDecoder } from 'util';
const exposedProperties = ['window', 'navigator', 'document'];
@@ -53,6 +54,11 @@ g.window.featureFlags ??= {};
g.URL.createObjectURL ??= () => '';
g.caches = new CacheStorage();
// Add shims for TextEncoder and TextDecoder after upgrading jspdf to v3.0.2+
// Source: https://github.com/parallax/jsPDF/issues/3882
g.TextDecoder = TextDecoder;
g.TextEncoder = TextEncoder;
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
@@ -135,7 +135,8 @@ export { customRender as render };
export { default as userEvent } from '@testing-library/user-event';
export async function selectOption(option: string, selectName?: string) {
const select = screen.getByRole(
// Use findByRole (async) to wait for element to be ready, preventing race conditions on slow CI
const select = await screen.findByRole(
'combobox',
selectName ? { name: selectName } : {},
);
+16 -12
View File
@@ -1098,12 +1098,14 @@ export function reFetchQueryResults(query) {
export function expandTable(table) {
return function (dispatch) {
const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
? SupersetClient.post({
endpoint: encodeURI(`/tableschemaview/${table.id}/expanded`),
postPayload: { expanded: true },
})
: Promise.resolve();
const sync =
isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) &&
table.initialized
? SupersetClient.post({
endpoint: encodeURI(`/tableschemaview/${table.id}/expanded`),
postPayload: { expanded: true },
})
: Promise.resolve();
return sync
.then(() => dispatch({ type: EXPAND_TABLE, table }))
@@ -1122,12 +1124,14 @@ export function expandTable(table) {
export function collapseTable(table) {
return function (dispatch) {
const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
? SupersetClient.post({
endpoint: encodeURI(`/tableschemaview/${table.id}/expanded`),
postPayload: { expanded: false },
})
: Promise.resolve();
const sync =
isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) &&
table.initialized
? SupersetClient.post({
endpoint: encodeURI(`/tableschemaview/${table.id}/expanded`),
postPayload: { expanded: false },
})
: Promise.resolve();
return sync
.then(() => dispatch({ type: COLLAPSE_TABLE, table }))
@@ -934,6 +934,10 @@ describe('async actions', () => {
fetchMock.delete(updateTableSchemaEndpoint, {});
fetchMock.post(updateTableSchemaEndpoint, JSON.stringify({ id: 1 }));
const updateTableSchemaExpandedEndpoint =
'glob:**/tableschemaview/*/expanded';
fetchMock.post(updateTableSchemaExpandedEndpoint, {});
const getTableMetadataEndpoint =
'glob:**/api/v1/database/*/table_metadata/*';
fetchMock.get(getTableMetadataEndpoint, {});
@@ -1411,10 +1415,10 @@ describe('async actions', () => {
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('expandTable', () => {
test('updates the table schema state in the backend', () => {
test('updates the table schema state in the backend when initialized', () => {
expect.assertions(2);
const table = { id: 1 };
const table = { id: 1, initialized: true };
const store = mockStore({});
const expectedActions = [
{
@@ -1424,17 +1428,108 @@ describe('async actions', () => {
];
return store.dispatch(actions.expandTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
const expandedCalls = fetchMock
.calls()
.filter(
call =>
call[0] &&
call[0].includes('/tableschemaview/') &&
call[0].includes('/expanded'),
);
expect(expandedCalls).toHaveLength(1);
});
});
test('does not call backend when table is not initialized', () => {
expect.assertions(2);
const table = { id: 'yVJPtuSackF', initialized: false };
const store = mockStore({});
const expectedActions = [
{
type: actions.EXPAND_TABLE,
table,
},
];
return store.dispatch(actions.expandTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
// Check all POST calls to find the expanded endpoint
const expandedCalls = fetchMock
.calls()
.filter(
call =>
call[0] &&
call[0].includes('/tableschemaview/') &&
call[0].includes('/expanded'),
);
expect(expandedCalls).toHaveLength(0);
});
});
test('does not call backend when initialized is undefined', () => {
expect.assertions(2);
const table = { id: 'yVJPtuSackF' };
const store = mockStore({});
const expectedActions = [
{
type: actions.EXPAND_TABLE,
table,
},
];
return store.dispatch(actions.expandTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
// Check all POST calls to find the expanded endpoint
const expandedCalls = fetchMock
.calls()
.filter(
call =>
call[0] &&
call[0].includes('/tableschemaview/') &&
call[0].includes('/expanded'),
);
expect(expandedCalls).toHaveLength(0);
});
});
test('does not call backend when feature flag is off', () => {
expect.assertions(2);
isFeatureEnabled.mockImplementation(
feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'),
);
const table = { id: 1, initialized: true };
const store = mockStore({});
const expectedActions = [
{
type: actions.EXPAND_TABLE,
table,
},
];
return store.dispatch(actions.expandTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
// Check all POST calls to find the expanded endpoint
const expandedCalls = fetchMock
.calls()
.filter(
call =>
call[0] &&
call[0].includes('/tableschemaview/') &&
call[0].includes('/expanded'),
);
expect(expandedCalls).toHaveLength(0);
isFeatureEnabled.mockRestore();
});
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('collapseTable', () => {
test('updates the table schema state in the backend', () => {
test('updates the table schema state in the backend when initialized', () => {
expect.assertions(2);
const table = { id: 1 };
const table = { id: 1, initialized: true };
const store = mockStore({});
const expectedActions = [
{
@@ -1444,7 +1539,95 @@ describe('async actions', () => {
];
return store.dispatch(actions.collapseTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
const expandedCalls = fetchMock
.calls()
.filter(
call =>
call[0] &&
call[0].includes('/tableschemaview/') &&
call[0].includes('/expanded'),
);
expect(expandedCalls).toHaveLength(1);
});
});
test('does not call backend when table is not initialized', () => {
expect.assertions(2);
const table = { id: 'yVJPtuSackF', initialized: false };
const store = mockStore({});
const expectedActions = [
{
type: actions.COLLAPSE_TABLE,
table,
},
];
return store.dispatch(actions.collapseTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
const expandedCalls = fetchMock
.calls()
.filter(
call =>
call[0] &&
call[0].includes('/tableschemaview/') &&
call[0].includes('/expanded'),
);
expect(expandedCalls).toHaveLength(0);
});
});
test('does not call backend when initialized is undefined', () => {
expect.assertions(2);
const table = { id: 'yVJPtuSackF' };
const store = mockStore({});
const expectedActions = [
{
type: actions.COLLAPSE_TABLE,
table,
},
];
return store.dispatch(actions.collapseTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
const expandedCalls = fetchMock
.calls()
.filter(
call =>
call[0] &&
call[0].includes('/tableschemaview/') &&
call[0].includes('/expanded'),
);
expect(expandedCalls).toHaveLength(0);
});
});
test('does not call backend when feature flag is off', () => {
expect.assertions(2);
isFeatureEnabled.mockImplementation(
feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'),
);
const table = { id: 1, initialized: true };
const store = mockStore({});
const expectedActions = [
{
type: actions.COLLAPSE_TABLE,
table,
},
];
return store.dispatch(actions.collapseTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
const expandedCalls = fetchMock
.calls()
.filter(
call =>
call[0] &&
call[0].includes('/tableschemaview/') &&
call[0].includes('/expanded'),
);
expect(expandedCalls).toHaveLength(0);
isFeatureEnabled.mockRestore();
});
});
});
@@ -19,9 +19,10 @@
import { isValidElement } from 'react';
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import userEvent from '@testing-library/user-event';
import QueryTable from 'src/SqlLab/components/QueryTable';
import { runningQuery, successfulQuery, user } from 'src/SqlLab/fixtures';
import { render, screen } from 'spec/helpers/testing-library';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
const mockedProps = {
queries: [runningQuery, successfulQuery],
@@ -29,6 +30,11 @@ const mockedProps = {
latestQueryId: 'ryhMUZCGb',
};
const queryWithResults = {
...successfulQuery,
resultsKey: 'test-results-key-123',
};
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('QueryTable', () => {
test('is valid', () => {
@@ -92,4 +98,93 @@ describe('QueryTable', () => {
),
).toHaveLength(1);
});
test('renders View button when query has resultsKey', () => {
const mockStore = configureStore([thunk]);
const propsWithResults = {
...mockedProps,
columns: ['started', 'duration', 'rows', 'results'],
queries: [queryWithResults],
};
render(<QueryTable {...propsWithResults} />, {
store: mockStore({ user, sqlLab: { queries: {} } }),
});
expect(screen.getByRole('button', { name: /view/i })).toBeInTheDocument();
});
test('does not render View button when query has no resultsKey', () => {
const mockStore = configureStore([thunk]);
const queryWithoutResults = {
...successfulQuery,
resultsKey: null,
};
const propsWithoutResults = {
...mockedProps,
columns: ['started', 'duration', 'rows', 'results'],
queries: [queryWithoutResults],
};
render(<QueryTable {...propsWithoutResults} />, {
store: mockStore({ user, sqlLab: { queries: {} } }),
});
expect(
screen.queryByRole('button', { name: /view/i }),
).not.toBeInTheDocument();
});
test('clicking View button opens data preview modal', async () => {
const mockStore = configureStore([thunk]);
const propsWithResults = {
...mockedProps,
columns: ['started', 'duration', 'rows', 'results'],
queries: [queryWithResults],
};
render(<QueryTable {...propsWithResults} />, {
store: mockStore({
user,
sqlLab: {
queries: {
[queryWithResults.id]: queryWithResults,
},
},
}),
});
const viewButton = screen.getByRole('button', { name: /view/i });
await userEvent.click(viewButton);
expect(await screen.findByText('Data preview')).toBeInTheDocument();
});
test('modal closes when exiting', async () => {
const mockStore = configureStore([thunk]);
const propsWithResults = {
...mockedProps,
columns: ['started', 'duration', 'rows', 'results'],
queries: [queryWithResults],
};
render(<QueryTable {...propsWithResults} />, {
store: mockStore({
user,
sqlLab: {
queries: {
[queryWithResults.id]: queryWithResults,
},
},
}),
});
const viewButton = screen.getByRole('button', { name: /view/i });
await userEvent.click(viewButton);
expect(await screen.findByText('Data preview')).toBeInTheDocument();
const closeButton = screen.getByRole('button', { name: /close/i });
await userEvent.click(closeButton);
await waitFor(() => {
expect(screen.queryByText('Data preview')).not.toBeInTheDocument();
});
});
});
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useMemo, ReactNode } from 'react';
import { useMemo, ReactNode, useState, useRef } from 'react';
import {
Card,
Button,
@@ -27,9 +27,9 @@ import {
TableView,
} from '@superset-ui/core/components';
import ProgressBar from '@superset-ui/core/components/ProgressBar';
import { t, QueryResponse } from '@superset-ui/core';
import { t, QueryResponse, QueryState } from '@superset-ui/core';
import { useTheme } from '@apache-superset/core/ui';
import { useDispatch, useSelector } from 'react-redux';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import {
queryEditorSetSql,
@@ -37,6 +37,7 @@ import {
fetchQueryResults,
clearQueryResults,
removeQuery,
startQuery,
} from 'src/SqlLab/actions/sqlLab';
import { fDuration, extendedDayjs } from '@superset-ui/core/utils/dates';
import { SqlLabRootState } from 'src/SqlLab/types';
@@ -44,7 +45,7 @@ import { UserWithPermissionsAndRoles as User } from 'src/types/bootstrapTypes';
import { makeUrl } from 'src/utils/pathUtils';
import ResultSet from '../ResultSet';
import HighlightedSql from '../HighlightedSql';
import { StaticPosition, StyledTooltip } from './styles';
import { StaticPosition, StyledTooltip, ModalResultSetWrapper } from './styles';
interface QueryTableQuery extends Omit<
QueryResponse,
@@ -82,6 +83,15 @@ const QueryTable = ({
}: QueryTableProps) => {
const theme = useTheme();
const dispatch = useDispatch();
const [selectedQuery, setSelectedQuery] = useState<QueryResponse | null>(
null,
);
const selectedQueryRef = useRef<QueryResponse | null>(null);
const modalRef = useRef<{
close: () => void;
open: (e: React.MouseEvent) => void;
showModal: boolean;
} | null>(null);
const QUERY_HISTORY_TABLE_HEADERS_LOCALIZED = {
state: t('State'),
@@ -116,6 +126,14 @@ const QueryTable = ({
);
const user = useSelector<SqlLabRootState, User>(state => state.user);
const reduxQueries = useSelector<
SqlLabRootState,
Record<string, QueryResponse>
>(state => state.sqlLab?.queries ?? {}, shallowEqual);
const openAsyncResults = (query: QueryResponse, displayLimit: number) => {
dispatch(fetchQueryResults(query, displayLimit));
};
const data = useMemo(() => {
const restoreSql = (query: QueryResponse) => {
@@ -128,10 +146,6 @@ const QueryTable = ({
dispatch(cloneQueryToNewTab(query, true));
};
const openAsyncResults = (query: QueryResponse, displayLimit: number) => {
dispatch(fetchQueryResults(query, displayLimit));
};
const statusAttributes = {
success: {
config: {
@@ -289,26 +303,17 @@ const QueryTable = ({
);
if (q.resultsKey) {
q.results = (
<ModalTrigger
className="ResultsModal"
triggerNode={
<Button buttonSize="xsmall" buttonStyle="secondary">
{t('View')}
</Button>
}
modalTitle={t('Data preview')}
beforeOpen={() => openAsyncResults(query, displayLimit)}
onExit={() => dispatch(clearQueryResults(query))}
modalBody={
<ResultSet
showSql
queryId={query.id}
displayLimit={displayLimit}
defaultQueryLimit={1000}
/>
}
responsive
/>
<Button
buttonSize="xsmall"
buttonStyle="secondary"
onClick={(e: React.MouseEvent) => {
selectedQueryRef.current = query;
setSelectedQuery(query);
modalRef.current?.open(e);
}}
>
{t('View')}
</Button>
);
} else {
q.results = <></>;
@@ -365,6 +370,55 @@ const QueryTable = ({
return (
<div className="QueryTable">
<ModalTrigger
ref={modalRef}
triggerNode={null}
className="ResultsModal"
modalTitle={t('Data preview')}
beforeOpen={() => {
const query = selectedQueryRef.current;
if (query) {
const existingQuery = reduxQueries[query.id];
if (!existingQuery?.sql && query.sql) {
dispatch(startQuery({ ...query, sql: query.sql }, false));
}
openAsyncResults(query, displayLimit);
}
}}
onExit={() => {
const query = selectedQueryRef.current;
if (query) {
dispatch(clearQueryResults(query));
selectedQueryRef.current = null;
setSelectedQuery(null);
}
}}
modalBody={
selectedQuery ? (
<ModalResultSetWrapper>
{(() => {
const height =
reduxQueries[selectedQuery.id]?.state ===
QueryState.Success &&
reduxQueries[selectedQuery.id]?.results
? Math.floor(window.innerHeight * 0.5)
: undefined;
return (
<ResultSet
showSql
queryId={selectedQuery.id}
displayLimit={displayLimit}
defaultQueryLimit={1000}
useFixedHeight
height={height}
/>
);
})()}
</ModalResultSetWrapper>
) : null
}
responsive
/>
<TableView
columns={columnsOfTable}
data={data}
@@ -39,3 +39,10 @@ export const StyledTooltip = styled(IconTooltip)`
}
}
`;
export const ModalResultSetWrapper = styled.div`
display: flex;
flex-direction: column;
overflow: hidden;
max-height: 50vh;
`;
@@ -104,12 +104,14 @@ export interface ResultSetProps {
csv?: boolean;
database?: Record<string, any>;
displayLimit: number;
height?: number;
queryId: string;
search?: boolean;
showSql?: boolean;
showSqlInline?: boolean;
visualize?: boolean;
defaultQueryLimit: number;
useFixedHeight?: boolean;
}
const ResultContainer = styled.div`
@@ -177,12 +179,14 @@ const ResultSet = ({
csv = true,
database = {},
displayLimit,
height,
queryId,
search = true,
showSql = false,
showSqlInline = false,
visualize = true,
defaultQueryLimit,
useFixedHeight = false,
}: ResultSetProps) => {
const user = useSelector(({ user }: SqlLabRootState) => user, shallowEqual);
const streamingThreshold = useSelector(
@@ -711,6 +715,16 @@ const ResultSet = ({
LocalStorageKeys.SqllabIsRenderHtmlEnabled,
true,
);
const tableProps = {
data,
queryId: query.id,
orderedColumnKeys: results.columns.map(col => col.column_name),
filterText: searchText,
expandedColumns,
allowHTML,
};
return (
<>
<ResultContainer>
@@ -753,27 +767,21 @@ const ResultSet = ({
{sql}
</>
)}
<div
css={css`
flex: 1 1 auto;
`}
>
<AutoSizer disableWidth>
{({ height }) => (
<ResultTable
data={data}
queryId={query.id}
orderedColumnKeys={results.columns.map(
col => col.column_name,
)}
height={height}
filterText={searchText}
expandedColumns={expandedColumns}
allowHTML={allowHTML}
/>
)}
</AutoSizer>
</div>
{useFixedHeight && height !== undefined ? (
<ResultTable {...tableProps} height={height} />
) : (
<div
css={css`
flex: 1 1 auto;
`}
>
<AutoSizer disableWidth>
{({ height: autoHeight }) => (
<ResultTable {...tableProps} height={autoHeight} />
)}
</AutoSizer>
</div>
)}
</ResultContainer>
<StreamingExportModal
visible={showStreamingModal}
@@ -17,21 +17,21 @@
* under the License.
*/
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { bindActionCreators, Dispatch, AnyAction } from 'redux';
import * as actions from './chartAction';
import { logEvent } from '../../logger/actions';
import Chart from './Chart';
import { updateDataMask } from '../../dataMask/actions';
function mapDispatchToProps(dispatch) {
function mapDispatchToProps(dispatch: Dispatch<AnyAction>) {
return {
actions: bindActionCreators(
{
...actions,
updateDataMask,
logEvent,
},
} as any,
dispatch,
),
};
@@ -476,7 +476,16 @@ export function exploreJSON(
return dispatch(chartUpdateSucceeded(queriesResponse, key));
})
.catch(response => {
// Ignore abort errors - they're expected when filters change quickly
const isAbort =
response?.name === 'AbortError' || response?.statusText === 'abort';
if (isAbort) {
// Abort is expected: filters changed, chart unmounted, etc.
return dispatch(chartUpdateStopped(key));
}
if (isFeatureEnabled(FeatureFlag.GlobalAsyncQueries)) {
// In async mode we just pass the raw error response through
return dispatch(chartUpdateFailed([response], key));
}
@@ -494,10 +503,7 @@ export function exploreJSON(
}),
);
};
if (response.name === 'AbortError') {
appendErrorLog('abort');
return dispatch(chartUpdateStopped(key));
}
return getClientErrorObject(response).then(parsedResponse => {
if (response.statusText === 'timeout') {
appendErrorLog('timeout');
@@ -357,6 +357,28 @@ describe('chart actions', () => {
});
});
test('should dispatch CHART_UPDATE_STOPPED action upon abort', () => {
fetchMock.post(
MOCK_URL,
{ throws: { name: 'AbortError' } },
{ overwriteRoutes: true },
);
const timeoutInSec = 100;
const actionThunk = actions.postChartFormData({}, false, timeoutInSec);
return actionThunk(dispatch, mockGetState).then(() => {
const types = dispatch.args
.map(call => call[0] && call[0].type)
.filter(Boolean);
expect(types).toContain(actions.CHART_UPDATE_STOPPED);
expect(types).not.toContain(actions.CHART_UPDATE_FAILED);
setupDefaultFetchMock();
});
});
test('should handle the bigint without regression', async () => {
getExploreUrlStub.restore();
const mockBigIntUrl = '/mock/chart/data/bigint';
@@ -29,14 +29,14 @@ import {
DATASOURCE_ENDPOINT,
setupDatasourceEditorMocks,
cleanupAsyncOperations,
asyncRender,
fastRender,
dismissDatasourceWarning,
} from './DatasourceEditor.test.utils';
type MetricType = DatasetObject['metrics'][number];
// Factory function for currency props - returns fresh copy to prevent test pollution
// Using single metric to minimize DOM size for faster test execution while still validating currency functionality
// Using single metric to minimize DOM size for faster test execution
const createPropsWithCurrency = () => {
const baseProps = createProps();
return {
@@ -54,6 +54,22 @@ const createPropsWithCurrency = () => {
};
};
// Shared setup to navigate to expanded currency section
const setupCurrencySection = async () => {
await dismissDatasourceWarning();
// Navigate to metrics tab - use findBy which has built-in waiting
const metricButton = await screen.findByTestId('collection-tab-Metrics');
await userEvent.click(metricButton);
// Expand the metric row
const expandToggles = await screen.findAllByLabelText(/expand row/i);
await userEvent.click(expandToggles[0]);
// Wait for currency section to be visible
await screen.findByText('Metric currency');
};
beforeEach(() => {
fetchMock.get(DATASOURCE_ENDPOINT, [], { overwriteRoutes: true });
setupDatasourceEditorMocks();
@@ -66,99 +82,57 @@ afterEach(async () => {
test('renders currency section in metrics tab', async () => {
const testProps = createPropsWithCurrency();
await asyncRender(testProps);
fastRender(testProps);
await dismissDatasourceWarning();
await setupCurrencySection();
// Navigate to metrics tab
const metricButton = await screen.findByTestId('collection-tab-Metrics');
await userEvent.click(metricButton);
// Expand the single metric row with currency
const expandToggles = await screen.findAllByLabelText(/expand row/i);
await userEvent.click(expandToggles[0]);
// Check for currency section header
const currencyHeader = await screen.findByText('Metric currency');
expect(currencyHeader).toBeVisible();
// Verify currency position selector exists
const positionSelector = screen.getByRole('combobox', {
name: 'Currency prefix or suffix',
});
expect(positionSelector).toBeInTheDocument();
// Verify currency symbol selector exists
const symbolSelector = screen.getByRole('combobox', {
name: 'Currency symbol',
});
expect(symbolSelector).toBeInTheDocument();
// Verify currency selectors exist
expect(
screen.getByRole('combobox', { name: 'Currency prefix or suffix' }),
).toBeInTheDocument();
expect(
screen.getByRole('combobox', { name: 'Currency symbol' }),
).toBeInTheDocument();
});
// Allow extra headroom for dropdown render on slower CI runners
test('changes currency position from prefix to suffix', async () => {
const testProps = createPropsWithCurrency();
fastRender(testProps);
await asyncRender(testProps);
await setupCurrencySection();
await dismissDatasourceWarning();
// Navigate to metrics tab
const metricButton = await screen.findByTestId('collection-tab-Metrics');
await userEvent.click(metricButton);
// Expand the metric with currency
const expandToggles = await screen.findAllByLabelText(/expand row/i);
await userEvent.click(expandToggles[0]);
// Select suffix option via shared helper (rc-virtual-list aware)
await selectOption('Suffix', 'Currency prefix or suffix');
await cleanupAsyncOperations();
// Verify onChange was called with suffix position
await waitFor(() => {
expect(testProps.onChange).toHaveBeenCalledTimes(1);
const callArg = testProps.onChange.mock.calls[0][0];
const metrics = callArg.metrics || [];
const updatedMetric = metrics.find(
(m: MetricType) => m.currency && m.currency.symbolPosition === 'suffix',
);
expect(updatedMetric?.currency?.symbol).toBe('USD');
});
}, 30000);
// Allow extra headroom for dropdown render on slower CI runners
// Verify the exact call arguments
const callArg = testProps.onChange.mock.calls[0][0];
const metrics = callArg.metrics || [];
const updatedMetric = metrics.find(
(m: MetricType) => m.currency?.symbolPosition === 'suffix',
);
expect(updatedMetric?.currency?.symbol).toBe('USD');
}, 60000);
test('changes currency symbol from USD to GBP', async () => {
const testProps = createPropsWithCurrency();
fastRender(testProps);
await asyncRender(testProps);
await setupCurrencySection();
await dismissDatasourceWarning();
// Navigate to metrics tab
const metricButton = await screen.findByTestId('collection-tab-Metrics');
await userEvent.click(metricButton);
// Expand the metric with currency
const expandToggles = await screen.findAllByLabelText(/expand row/i);
await userEvent.click(expandToggles[0]);
// Select GBP option via shared helper (rc-virtual-list aware)
await selectOption('£ (GBP)', 'Currency symbol');
await cleanupAsyncOperations();
// Verify onChange was called with GBP
await waitFor(() => {
expect(testProps.onChange).toHaveBeenCalledTimes(1);
const callArg = testProps.onChange.mock.calls[0][0];
const metrics = callArg.metrics || [];
const updatedMetric = metrics.find(
(m: MetricType) => m.currency && m.currency.symbol === 'GBP',
);
expect(updatedMetric?.currency?.symbolPosition).toBe('prefix');
});
}, 30000);
// Verify the exact call arguments
const callArg = testProps.onChange.mock.calls[0][0];
const metrics = callArg.metrics || [];
const updatedMetric = metrics.find(
(m: MetricType) => m.currency?.symbol === 'GBP',
);
expect(updatedMetric?.currency?.symbolPosition).toBe('prefix');
}, 60000);
@@ -0,0 +1,230 @@
/**
* 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,
fireEvent,
} from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { createRef } from 'react';
import SearchFilter from './Search';
import type { FilterHandler } from './types';
const mockOnSubmit = jest.fn();
const defaultProps = {
Header: 'Search Header',
name: 'search-input',
initialValue: '',
toolTipDescription: undefined,
onSubmit: mockOnSubmit,
autoComplete: undefined,
};
const setup = (props = {}) =>
render(<SearchFilter {...defaultProps} {...props} />);
beforeEach(() => {
mockOnSubmit.mockClear();
});
test('renders search filter with header', () => {
setup();
expect(screen.getByText('Search Header')).toBeInTheDocument();
});
test('renders input with placeholder', () => {
setup();
expect(screen.getByPlaceholderText('Type a value')).toBeInTheDocument();
});
test('renders search icon', () => {
setup();
expect(screen.getByTestId('filters-search')).toBeInTheDocument();
// The icon should be present as a prefix
const input = screen.getByTestId('filters-search');
expect(input.parentElement?.querySelector('.anticon-search')).toBeTruthy();
});
test('renders with initial value', () => {
setup({ initialValue: 'initial search term' });
const input = screen.getByTestId('filters-search') as HTMLInputElement;
expect(input.value).toBe('initial search term');
});
test('renders tooltip when toolTipDescription is provided', () => {
setup({ toolTipDescription: 'This is a helpful tooltip' });
const tooltip = screen.getByRole('img', { name: /info-circle/i });
expect(tooltip).toBeInTheDocument();
});
test('does not render tooltip when toolTipDescription is undefined', () => {
setup({ toolTipDescription: undefined });
const tooltip = screen.queryByRole('img', { name: /info-circle/i });
expect(tooltip).not.toBeInTheDocument();
});
test('updates input value on change', async () => {
setup();
const input = screen.getByTestId('filters-search') as HTMLInputElement;
await userEvent.type(input, 'test query');
expect(input.value).toBe('test query');
});
test('calls onSubmit with trimmed value on blur', async () => {
setup();
const input = screen.getByTestId('filters-search');
await userEvent.type(input, ' search term ');
await userEvent.tab(); // Trigger blur
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith('search term');
});
});
test('calls onSubmit with trimmed value on Enter key', async () => {
setup();
const input = screen.getByTestId('filters-search');
await userEvent.type(input, ' another search ');
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith('another search');
});
});
test('calls onSubmit with empty string when input is cleared', async () => {
setup({ initialValue: 'initial value' });
const input = screen.getByTestId('filters-search');
await userEvent.clear(input);
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith('');
});
});
test('does not call onSubmit when empty value is submitted on blur', async () => {
setup();
const input = screen.getByTestId('filters-search');
await userEvent.click(input);
await userEvent.tab(); // Trigger blur without typing
// onSubmit should not be called for empty value on blur/enter
expect(mockOnSubmit).not.toHaveBeenCalled();
});
test('calls onSubmit with empty string when only whitespace is entered and cleared', async () => {
setup();
const input = screen.getByTestId('filters-search');
await userEvent.type(input, ' ');
// When cleared (which happens when the value becomes empty), onSubmit is called with ''
await userEvent.clear(input);
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith('');
});
});
test('clearFilter resets value and calls onSubmit with empty string', async () => {
const ref = createRef<FilterHandler>();
render(<SearchFilter {...defaultProps} initialValue="test" ref={ref} />);
const input = screen.getByTestId('filters-search') as HTMLInputElement;
expect(input.value).toBe('test');
// Call clearFilter via ref
ref.current?.clearFilter();
await waitFor(() => {
expect(input.value).toBe('');
expect(mockOnSubmit).toHaveBeenCalledWith('');
});
});
test('uses custom name attribute when provided', () => {
setup({ name: 'custom-search-name' });
const input = screen.getByTestId('filters-search') as HTMLInputElement;
expect(input.name).toBe('custom-search-name');
});
test('sets autocomplete to off by default', () => {
setup();
const input = screen.getByTestId('filters-search') as HTMLInputElement;
expect(input.autocomplete).toBe('off');
});
test('uses custom autocomplete value when provided', () => {
setup({ autoComplete: 'new-password' });
const input = screen.getByTestId('filters-search') as HTMLInputElement;
expect(input.autocomplete).toBe('new-password');
});
test('renders with allowClear prop', () => {
setup({ initialValue: 'test value' });
const input = screen.getByTestId('filters-search');
// Ant Design Input with allowClear should have a clear button when there's a value
expect(input).toBeInTheDocument();
});
test('multiple rapid changes only submit the final value', async () => {
setup();
const input = screen.getByTestId('filters-search');
await userEvent.type(input, 'first');
await userEvent.type(input, ' second');
await userEvent.type(input, ' third');
await userEvent.tab();
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith('first second third');
// Should only be called once on blur, not for each keystroke
expect(mockOnSubmit).toHaveBeenCalledTimes(1);
});
});
test('handles programmatic value changes through clearFilter', async () => {
const ref = createRef<FilterHandler>();
render(
<SearchFilter {...defaultProps} initialValue="initial value" ref={ref} />,
);
const input = screen.getByTestId('filters-search') as HTMLInputElement;
// Type something new
await userEvent.clear(input);
await userEvent.type(input, 'new value');
expect(input.value).toBe('new value');
// Clear using ref
ref.current?.clearFilter();
await waitFor(() => {
expect(input.value).toBe('');
expect(mockOnSubmit).toHaveBeenCalledWith('');
});
});
@@ -42,6 +42,7 @@ interface SearchHeaderProps extends BaseFilter {
onSubmit: (val: string) => void;
name: string;
toolTipDescription: string | undefined;
autoComplete?: string;
}
function SearchFilter(
@@ -51,6 +52,7 @@ function SearchFilter(
initialValue,
toolTipDescription,
onSubmit,
autoComplete = 'off',
}: SearchHeaderProps,
ref: RefObject<FilterHandler>,
) {
@@ -91,6 +93,7 @@ function SearchFilter(
allowClear
data-test="filters-search"
placeholder={t('Type a value')}
autoComplete={autoComplete}
name={name}
value={value}
onChange={handleChange}

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