Compare commits

...

118 Commits

Author SHA1 Message Date
yousoph
ea3ba12ebf Update transformProps.ts 2025-07-17 14:45:06 -07:00
yousoph
268d8e8fee update test 2025-07-17 13:53:09 -07:00
yousoph
ae71b837ff update tests 2025-07-17 13:48:59 -07:00
yousoph
12dd4a712e Update controlPanel.tsx 2025-07-17 10:22:32 -07:00
yousoph
04714b83c2 Update controlPanel.tsx 2025-07-17 00:07:57 -07:00
yousoph
6070e84944 Update transformProps.ts 2025-07-17 00:07:09 -07:00
yousoph
1cd939e612 Update transformProps.ts 2025-07-16 23:52:00 -07:00
yousoph
e71a19a19e Update types.ts 2025-07-16 23:39:10 -07:00
yousoph
e50e378d8b Update controlPanel.tsx 2025-07-16 23:31:23 -07:00
JUST.in DO IT
96cb6030c8 fix(explore): Display missing dataset for denied access (#34129) 2025-07-16 13:36:03 -07:00
Jun Yoneyama
94d47113ea feat(snowflake): Support Snowflake private keys w/o passphrase (#34156) 2025-07-16 20:53:41 +02:00
Mehmet Salih Yavuz
f756cee01b fix(theming): Remove leftover antd5 prefix (#34188) 2025-07-16 19:31:14 +03:00
Cesc Bausà
e8926f177d feat(i18n): add Catalan (ca) translations (#33953) 2025-07-16 16:38:51 +02:00
Mehmet Salih Yavuz
16f4516903 chore(Oracle): Update oracle column length to 128 (#34179) 2025-07-16 14:58:19 +03:00
Beto Dealmeida
000d353ef3 fix(sqllab): database ID (#34181) 2025-07-15 19:37:24 -04:00
Beto Dealmeida
83b6f672ff fix(databricks): string escaper (#34180) 2025-07-15 19:27:55 -04:00
Beto Dealmeida
0dc48e9b41 fix(sqllab): pass DB id instead of name (#33955) 2025-07-15 18:43:34 -04:00
Maxime Beauchemin
fe9eef9198 feat: removing dup logic in sqla/models.py and models/helpers.py (#34177) 2025-07-15 14:02:57 -07:00
Hari Kiran
8a8248b575 docs(development): Fix typo in the documentation (#34163) 2025-07-15 14:30:32 -03:00
Gabriel Torres Ruiz
42d9a78777 feat(theming): Introduce bootstrap-driven Superset theme configurations (#34144) 2025-07-15 09:43:08 -07:00
Mehmet Salih Yavuz
31a15c5162 fix(DrillBy): make drill by work with multi metric charts (#34171) 2025-07-15 15:25:48 +02:00
SBIN2010
67b21c45df feat(filter panel): hide filter panel on all dashboard by default. (#32870) 2025-07-15 10:47:47 +03:00
SBIN2010
b280ab9e1f fix: adding and removing tags does not work in control panel properties modal (#34147) 2025-07-14 23:20:00 +03:00
Maxime Beauchemin
c42be77c25 feat(i18n): load language pack asynchronously (#34119) 2025-07-14 10:59:29 -07:00
Maxime Beauchemin
160917eae8 fix: frontend translation framework crashes on string errors (#34118) 2025-07-14 10:23:18 -07:00
Beto Dealmeida
68b84acd93 feat: improve Doris catalog support (#34140) 2025-07-14 12:01:08 -04:00
ongdisheng
0aa48b6564 fix(dataset): trigger onChange when switching to physical dataset to clear SQL (#34153) 2025-07-14 14:30:05 +03:00
Mehmet Salih Yavuz
0fc1955049 chore(Tags): Sort tags by name if possible (#34149) 2025-07-14 11:20:01 +03:00
ongdisheng
8a704d293b docs: remove duplicated line in Running tests with act section (#34145) 2025-07-12 15:25:11 -07:00
Đỗ Trọng Hải
f4754641c8 build(dev-deps): clean up deprecated Babel proposal plugins (#34125) 2025-07-12 07:07:56 +07:00
dependabot[bot]
7c98c3f4f6 chore(deps): bump flask-cors from 4.0.2 to 6.0.0 (#34138)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-11 13:38:13 -07:00
Beto Dealmeida
5a32777dd0 chore: remove unnecessary disables (#34139) 2025-07-11 11:25:02 -04:00
Damian Pendrak
7229e1ccf3 feat(deckgl): add new color controls with color breakpoints (#34017) 2025-07-11 17:29:26 +03:00
Enzo Martellucci
30695d75d7 fix(DatabaseModal): Resolve Connect button issue for SQLAlchemy URI database connections (#34112) 2025-07-11 14:03:36 +03:00
Vitor Avila
75ee4edc6a fix: Apply metric d3format when currency config is {} for table charts (#34127) 2025-07-10 22:44:22 -03:00
dependabot[bot]
d269e3d187 chore(deps): bump react-json-tree from 0.17.0 to 0.20.0 in /superset-frontend (#33990)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2025-07-10 11:54:59 -07:00
aikawa-ohno
7d0fabe1ab fix(i18n): Update Japanese translations (#33974) 2025-07-10 12:35:44 +03:00
Evan Rusackas
9695249976 fix(screenshots): Change default for SCREENSHOT_PLAYWRIGHT_WAIT_EVENT to domcontentloaded (#34114) 2025-07-10 12:33:40 +03:00
dependabot[bot]
17c1a37afb chore(deps): bump react-error-boundary from 5.0.0 to 6.0.0 in /superset-frontend (#33486)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2025-07-10 10:38:46 +03:00
Maxime Beauchemin
73dfe57ae2 fix: make flask-cors a core dependency (#34115) 2025-07-09 14:54:39 -07:00
Maxime Beauchemin
0d236c4ade fix: improve login page placement and width (#34108) 2025-07-09 11:33:16 -07:00
Maxime Beauchemin
bc0a10fc73 chore: clean up more flask/jinja html views (#34093) 2025-07-09 10:16:57 -07:00
Enzo Martellucci
5efca408eb fix(UI): Adjust background color for Dashboard, Tabs, and ListView component (#34113) 2025-07-09 18:41:09 +02:00
Vitor Avila
29f638e239 chore: Improve performance to load chart's save modal (#34097) 2025-07-09 12:20:40 -03:00
Vitor Avila
ddeb612429 chore: Improve performance to load the chart properties modal (#34079) 2025-07-09 12:19:08 -03:00
SBIN2010
0bc214e889 fix: upload data model Collapse state (#32734) 2025-07-09 11:53:10 +03:00
Paul Lavacquery
d951158ce6 feat(deckgl): add support for OpenStreetMap as our new default and make "tile-providers" more configurable (#33603)
Co-authored-by: Maxime Beauchemin <maximebeauchemin@gmail.com>
2025-07-08 14:04:10 -07:00
Richard Fogaca Nienkotter
85034b9748 feat(deck-gl): Enable individual deck.gl layer selection in FilterScope tree (#33769)
Co-authored-by: richardfn <richard.fogaca@appsilon.com>
Co-authored-by: amaannawab923 <amaannawab923@gmail.com>
2025-07-08 18:37:47 +03:00
dependabot[bot]
11215b092a chore(deps-dev): bump webpack-dev-server from 4.15.2 to 5.2.1 in /superset-frontend (#34104)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2025-07-08 21:53:20 +07:00
Damian Pendrak
2129e22423 fix(deps): Revert "chore(deps): update @deck.gl/aggregation-layers requirement from ^9.0.38 to ^9.1.12 in /superset-frontend/plugins/legacy-preset-chart-deckgl" (#34103) 2025-07-08 15:13:11 +03:00
Vitor Avila
7ea1fca4f7 feat: Don't show the row limit warning for embedded dashboards by default (#34095) 2025-07-08 08:50:25 -03:00
Vitor Avila
9c8fdc0fc1 fix: Apply metric d3format from dataset when currency config is {} (#34098) 2025-07-08 08:48:09 -03:00
Joe Li
e25be0f3d9 chore: move auth e2e tests to component tests (#34057) 2025-07-07 16:41:44 -07:00
dependabot[bot]
d633fe47ef chore(deps): bump @fontsource/inter from 5.1.1 to 5.2.6 in /superset-frontend (#34042)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2025-07-07 16:38:02 -07:00
PolinaFam
c25e734407 fix(translations): Fix language switching behavior when default language is not English (#34049)
Co-authored-by: Polina Fam <pfam@ptsecurity.com>
2025-07-07 11:57:17 -07:00
Mehmet Salih Yavuz
d8fd6de940 fix(deps): Revert "chore(deps-dev): bump webpack-dev-server from 4.15.2 to 5.2.1 (#34090) 2025-07-07 21:20:39 +03:00
Vitor Avila
733f112142 fix: Support metric currency as dict during import (#34080) 2025-07-07 14:00:56 -03:00
dependabot[bot]
f55476034b chore(deps): bump ioredis from 4.28.5 to 5.6.1 in /superset-websocket (#34029)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2025-07-07 22:07:52 +07:00
amaannawab923
0a5941edd7 feat(viz-type): Ag grid table plugin Integration (#33517)
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Amaan Nawab <nelsondrew07@gmail.com>
Co-authored-by: Levis Mbote <111055098+LevisNgigi@users.noreply.github.com>
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
Co-authored-by: Paul Rhodes <withnale@users.noreply.github.com>
Co-authored-by: Vitor Avila <96086495+Vitor-Avila@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
Co-authored-by: Sam Firke <sfirke@users.noreply.github.com>
2025-07-07 16:50:44 +03:00
Damian Pendrak
0fc4119728 feat(deckgl): add cross-filters to deck.gl charts (#33789) 2025-07-07 16:23:27 +03:00
Mehmet Salih Yavuz
6adfd33e3a fix(Table): Allow timeshifts to be overriden (#34014) 2025-07-07 13:20:09 +03:00
Vitor Avila
829e4d92d9 chore: Use select_columns on chart's dashboard filter (#34075) 2025-07-05 18:48:30 -03:00
Mehmet Salih Yavuz
42db43c686 fix(styles): Remove custom z-indexes (#34066) 2025-07-04 23:47:32 +03:00
Maxime Beauchemin
a0f9efd45e chore: refactor react-syntax-highlither to handle dark themes (#34028) 2025-07-04 11:44:11 -07:00
Maxime Beauchemin
5d6a979cd0 chore: remove some of the deprecated theme.colors.* (#34056) 2025-07-04 11:39:03 -07:00
Michael S. Molina
d6ed819fe2 fix: Annotation layer errors (#34074) 2025-07-04 15:06:30 -03:00
Pius Iniobong
ef14a5fbb4 feat(filter): Add Slider Range Inputs Option for Numerical Range Filters (#33170)
Co-authored-by: Geido <60598000+geido@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Mehmet Salih Yavuz <salih.yavuz@proton.me>
2025-07-04 13:31:23 +03:00
dependabot[bot]
4718767ddb chore(deps): bump tar-fs from 2.1.2 to 3.1.0 in /superset-frontend (#34059)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 18:41:30 -07:00
dependabot[bot]
dfb377c636 chore(deps): update yeoman-generator requirement from ^7.4.0 to ^7.5.1 in /superset-frontend/packages/generator-superset (#32928)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 18:33:15 -07:00
dependabot[bot]
7082933b96 chore(deps): bump react from 17.0.2 to 19.1.0 in /superset-frontend/plugins/legacy-plugin-chart-chord (#32949)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 18:18:08 -07:00
dependabot[bot]
11b6263d55 chore(deps-dev): update fork-ts-checker-webpack-plugin requirement from ^9.0.2 to ^9.1.0 in /superset-frontend/packages/superset-ui-demo (#33481)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 18:07:25 -07:00
dependabot[bot]
b0cf7b61ad chore(deps): update @deck.gl/aggregation-layers requirement from ^9.0.38 to ^9.1.12 in /superset-frontend/plugins/legacy-preset-chart-deckgl (#33485)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 18:06:59 -07:00
dependabot[bot]
96a1b33f22 chore(deps-dev): bump webpack-dev-server from 4.15.2 to 5.2.1 in /superset-frontend (#32946)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 18:06:06 -07:00
dependabot[bot]
5cff87c048 chore(deps-dev): bump @types/jest from 29.5.14 to 30.0.0 in /superset-frontend/plugins/plugin-chart-handlebars (#33986)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 17:58:56 -07:00
dependabot[bot]
b9052fa461 chore(deps-dev): bump yeoman-test from 8.3.0 to 10.1.1 in /superset-frontend (#33496)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 17:43:26 -07:00
dependabot[bot]
0ea2066d5b chore(deps): bump @storybook/addon-actions from 8.1.11 to 9.0.8 in /superset-frontend/packages/superset-ui-demo (#33995)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 17:14:57 -07:00
dependabot[bot]
992aa3a4d5 chore(deps): update @types/d3-scale requirement from ^4.0.8 to ^4.0.9 in /superset-frontend/plugins/plugin-chart-word-cloud (#32441)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 17:08:17 -07:00
dependabot[bot]
36c7b15342 chore(deps): update dompurify requirement from ^3.2.4 to ^3.2.6 in /superset-frontend/plugins/legacy-preset-chart-nvd3 (#32082)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 17:07:49 -07:00
dependabot[bot]
2ab85f3b67 chore(deps): bump remark-gfm from 3.0.1 to 4.0.1 in /superset-frontend (#32945)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 17:05:06 -07:00
dependabot[bot]
1b690a9876 chore(deps): bump @ant-design/icons from 5.6.1 to 6.0.0 in /docs (#32953)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 17:04:10 -07:00
dependabot[bot]
f18d9b6bf4 chore(deps-dev): bump typescript from 5.6.2 to 5.7.3 in /superset-websocket (#32439)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 17:02:10 -07:00
dependabot[bot]
ebca5169a0 chore(deps-dev): update @types/lodash requirement from ^4.17.16 to ^4.17.20 in /superset-frontend/packages/superset-ui-core (#32080)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 17:00:42 -07:00
dependabot[bot]
b9ba4d6fda chore(deps-dev): bump cheerio from 1.0.0-rc.10 to 1.1.0 in /superset-frontend (#33991)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 16:58:59 -07:00
dependabot[bot]
20371940d3 chore(deps-dev): bump webpack-visualizer-plugin2 from 1.1.0 to 1.2.0 in /superset-frontend (#33989)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 16:58:13 -07:00
dependabot[bot]
ee4944bc1a chore(deps-dev): update fs-extra requirement from ^11.2.0 to ^11.3.0 in /superset-frontend/packages/generator-superset (#32093)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 16:41:38 -07:00
dependabot[bot]
5a1023da89 chore(deps): update @types/geojson requirement from ^7946.0.15 to ^7946.0.16 in /superset-frontend/plugins/legacy-preset-chart-deckgl (#32077)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 16:27:45 -07:00
dependabot[bot]
4b94d25869 chore(deps): bump @emotion/styled from 11.3.0 to 11.14.0 in /superset-frontend (#31560)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 15:50:51 -07:00
dependabot[bot]
2ceced71c5 chore(deps-dev): update jest requirement from ^30.0.2 to ^30.0.4 in /superset-frontend/plugins/plugin-chart-handlebars (#34034)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 15:32:17 -07:00
Vladislav Korenkov
9f0523977d feat(plugin-chart-echarts): add Gantt Chart plugin (#33716) 2025-07-03 14:23:50 -07:00
Joe Li
cb6342fc73 fix: Update spacing on echart legends (#34018) 2025-07-03 13:33:15 -07:00
dependabot[bot]
5214ee6fd4 chore(deps-dev): update jest requirement from ^30.0.2 to ^30.0.4 in /superset-frontend/plugins/plugin-chart-pivot-table (#34036)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2025-07-03 12:58:28 -07:00
dependabot[bot]
7b3329f315 chore(deps-dev): update @types/lodash requirement from ^4.17.16 to ^4.17.20 in /superset-frontend/plugins/plugin-chart-handlebars (#34037)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2025-07-03 12:57:42 -07:00
Vladislav Korenkov
d1b372f670 fix(chart controls): remove duplicated descriptions for chart controls (#33954) 2025-07-03 16:01:59 -03:00
dependabot[bot]
09c657c899 chore(deps-dev): update @babel/types requirement from ^7.26.9 to ^7.28.0 in /superset-frontend/plugins/plugin-chart-pivot-table (#34038)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2025-07-03 11:44:21 -07:00
dependabot[bot]
24500e99f8 chore(deps): bump ace-builds from 1.43.0 to 1.43.1 in /superset-frontend (#34043)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 09:39:22 -07:00
dependabot[bot]
b2a173977e chore(deps): bump mapbox-gl from 2.15.0 to 3.13.0 in /superset-frontend (#34008)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 09:38:26 -07:00
dependabot[bot]
8be79f4170 chore(deps-dev): bump @types/lodash from 4.17.13 to 4.17.20 in /superset-websocket (#34035)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 09:37:47 -07:00
dependabot[bot]
debaf8d6e9 chore(deps): bump @emotion/styled from 11.14.0 to 11.14.1 in /superset-frontend (#33992)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 09:35:41 -07:00
dependabot[bot]
1133d84775 chore(deps-dev): bump @applitools/eyes-storybook from 3.53.4 to 3.55.6 in /superset-frontend (#33987)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 09:34:07 -07:00
Maxime Beauchemin
8c4ca60b28 fix(styling): various minor visual tweaks and adjustments (#34031) 2025-07-03 08:55:28 -07:00
dependabot[bot]
90eded1a04 chore(deps-dev): bump prettier from 3.4.2 to 3.6.2 in /superset-websocket (#34033)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 22:46:56 +07:00
dependabot[bot]
093ee37aac chore(deps): bump swagger-ui-react from 5.25.3 to 5.26.0 in /docs (#34041)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 22:45:37 +07:00
Titan_gamini
23b1fe3b9e fix(dashboard): prevent crash on invalid CSS selectors in CSS templates (#33971)
Co-authored-by: Mehmet Salih Yavuz <salih.yavuz@proton.me>
2025-07-03 09:48:09 +03:00
Đỗ Trọng Hải
42288c4784 build(dev-deps): upgrade Jest to major version v30 (#33979) 2025-07-03 11:01:40 +07:00
Vitor Avila
c008190a08 fix: Dashboard native filter fixes (#33958) 2025-07-02 23:08:42 -03:00
dependabot[bot]
2734688f4f chore(deps): bump hot-shots from 10.2.1 to 11.1.0 in /superset-websocket (#34004)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-02 16:36:41 -07:00
dependabot[bot]
e3d8326d81 chore(deps-dev): bump @docusaurus/tsconfig from 3.8.0 to 3.8.1 in /docs (#34003)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-02 16:35:44 -07:00
dependabot[bot]
e1ab27f484 chore(deps): bump ace-builds from 1.41.0 to 1.43.0 in /superset-frontend (#34002)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-02 16:35:24 -07:00
dependabot[bot]
d1d43be9d1 chore(deps): bump swagger-ui-react from 5.25.2 to 5.25.3 in /docs (#34001)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-02 16:35:00 -07:00
dependabot[bot]
f9cec3e366 chore(deps-dev): bump eslint from 9.27.0 to 9.30.0 in /superset-websocket (#34000)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-02 16:34:29 -07:00
dependabot[bot]
f92431176a chore(deps-dev): bump @babel/cli from 7.26.4 to 7.27.2 in /superset-frontend (#33985)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-02 16:31:35 -07:00
dependabot[bot]
c31daf8c92 chore(deps): bump actions/cache from 3 to 4 (#33999)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-02 09:14:28 -07:00
Alexandru Soare
c74fae9663 feat(flag): Added feature_flag for superset security_views (#34023) 2025-07-02 14:31:09 +03:00
LisaHusband
308007f909 fix(handlebars): remove serverPaginationControlSetRow from control pa… (#34016) 2025-07-01 22:38:01 +03:00
dependabot[bot]
9aaab7374e chore(deps): bump antd from 5.25.1 to 5.26.3 in /docs (#33982)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-01 12:13:51 -07:00
Gabriel Torres Ruiz
057218d87f feat: Add confirmation modal for unsaved changes (#33809) 2025-07-01 19:38:51 +03:00
Đỗ Trọng Hải
050ccdcb3d chore: replace querystring usage with native URLSearchParams API (#33967)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2025-07-01 07:17:35 +07:00
Hugo Ferrando Seage
09d975cc3f docs: Fix typo in UPDATING.md regarding translations in version 5.0.0 (#33972) 2025-06-30 23:44:56 +07:00
479 changed files with 46116 additions and 8892 deletions

View File

@@ -12,6 +12,10 @@ updates:
# not until React >= 18.0.0
- dependency-name: "storybook"
- dependency-name: "@storybook*"
# JSDOM v30 doesn't play well with Jest v30
# Source: https://jestjs.io/blog#known-issues
# GH thread: https://github.com/jsdom/jsdom/issues/3492
- dependency-name: "jest-environment-jsdom"
directory: "/superset-frontend/"
schedule:
interval: "monthly"

View File

@@ -24,6 +24,12 @@ jobs:
submodules: recursive
fetch-depth: 1
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Python
if: steps.check.outputs.python
uses: ./.github/actions/setup-backend/
@@ -33,10 +39,20 @@ jobs:
run: ./scripts/uv-pip-compile.sh
- name: Check for uncommitted changes
if: steps.check.outputs.python
run: |
if [[ -n "$(git diff)" ]]; then
echo "Full diff (for logging/debugging):"
git diff
echo "Filtered diff (excluding comments and whitespace):"
filtered_diff=$(git diff -U0 | grep '^[-+]' | grep -vE '^[-+]{3}' | grep -vE '^[-+][[:space:]]*#' | grep -vE '^[-+][[:space:]]*$' || true)
echo "$filtered_diff"
if [[ -n "$filtered_diff" ]]; then
echo
echo "ERROR: The pinned dependencies are not up-to-date."
echo "Please run './scripts/uv-pip-compile.sh' and commit the changes."
echo "More info: https://github.com/apache/superset/tree/master/requirements"
exit 1
else
echo "Pinned dependencies are up-to-date."

View File

@@ -54,7 +54,7 @@ jobs:
yarn install --immutable
- name: Cache pre-commit environments
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit-v2-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }}

View File

@@ -5,7 +5,7 @@
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
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0

View File

@@ -43,7 +43,7 @@ assists people when migrating to a new version.
- [31198](https://github.com/apache/superset/pull/31198) Disallows by default the use of the following ClickHouse functions: "version", "currentDatabase", "hostName".
- [29798](https://github.com/apache/superset/pull/29798) Since 3.1.0, the intial schedule for an alert or report was mistakenly offset by the specified timezone's relation to UTC. The initial schedule should now begin at the correct time.
- [30021](https://github.com/apache/superset/pull/30021) The `dev` layer in our Dockerfile no long includes firefox binaries, only Chromium to reduce bloat/docker-build-time.
- [30099](https://github.com/apache/superset/pull/30099) Translations are no longer included in the default docker image builds. If your environment requires translations, you'll want to set the docker build arg `BUILD_TRANSACTION=true`.
- [30099](https://github.com/apache/superset/pull/30099) Translations are no longer included in the default docker image builds. If your environment requires translations, you'll want to set the docker build arg `BUILD_TRANSLATIONS=true`.
- [31262](https://github.com/apache/superset/pull/31262) NOTE: deprecated `pylint` in favor of `ruff` as our only python linter. Only affect development workflows positively (not the release itself). It should cover most important rules, be much faster, but some things linting rules that were enforced before may not be enforce in the exact same way as before.
- [31173](https://github.com/apache/superset/pull/31173) Modified `fetch_csrf_token` to align with HTTP standards, particularly regarding how cookies are handled. If you encounter any issues related to CSRF functionality, please report them as a new issue and reference this PR for context.
- [31413](https://github.com/apache/superset/pull/31413) Enable the DATE_FORMAT_IN_EMAIL_SUBJECT feature flag to allow users to specify a date format for the email subject, which will then be replaced with the actual date.

View File

@@ -8,11 +8,11 @@ version: 1
## CORS
To configure CORS, or cross-origin resource sharing, the following dependency must be installed:
```python
pip install apache_superset[cors]
```
:::note
In Superset versions prior to `5.x` you have to install to install `flask-cors` with `pip install flask-cors` to enable CORS support.
:::
The following keys in `superset_config.py` can be specified to configure CORS:

View File

@@ -78,7 +78,7 @@ Affecting the Docker build process:
- **SUPERSET_BUILD_TARGET (default=dev):** which --target to build, either `lean` or `dev` are commonly used
- **INCLUDE_FIREFOX (default=false):** whether to include the Firefox headless browser in the build
- **INCLUDE_CHROMIUM (default=false):** whether to include the Firefox headless browser in the build
- **INCLUDE_CHROMIUM (default=false):** whether to include the Chromium headless browser in the build
- **BUILD_TRANSLATIONS(default=false):** whether to compile the translations from the .po files available
- **SUPERSET_LOAD_EXAMPLES (default=yes):** whether to load the examples into the database upon startup,
save some precious time on startup by `SUPERSET_LOAD_EXAMPLES=no docker compose up`
@@ -614,9 +614,6 @@ act --job test-python-38 --secret GITHUB_TOKEN=$GITHUB_TOKEN --event pull_reques
There is also a utility script included in the Superset codebase to run Python integration tests. The [readme can be found here](https://github.com/apache/superset/tree/master/scripts/tests).
There is also a utility script included in the Superset codebase to run python integration tests. The [readme can be
found here](https://github.com/apache/superset/tree/master/scripts/tests)
To run all integration tests, for example, run this script from the root directory:
```bash

View File

@@ -18,7 +18,7 @@
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx"
},
"dependencies": {
"@ant-design/icons": "^5.5.2",
"@ant-design/icons": "^6.0.0",
"@docusaurus/core": "3.8.1",
"@docusaurus/plugin-client-redirects": "3.8.1",
"@docusaurus/preset-classic": "3.8.1",
@@ -26,7 +26,7 @@
"@emotion/styled": "^10.0.27",
"@saucelabs/theme-github-codeblock": "^0.3.0",
"@superset-ui/style": "^0.14.23",
"antd": "^5.25.1",
"antd": "^5.26.3",
"docusaurus-plugin-less": "^2.0.2",
"less": "^4.3.0",
"less-loader": "^12.3.0",
@@ -35,11 +35,11 @@
"react-dom": "^18.3.1",
"react-github-btn": "^1.4.0",
"react-svg-pan-zoom": "^3.13.1",
"swagger-ui-react": "^5.25.2"
"swagger-ui-react": "^5.26.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.8.1",
"@docusaurus/tsconfig": "^3.8.0",
"@docusaurus/tsconfig": "^3.8.1",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",

View File

@@ -158,13 +158,20 @@
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
"@ant-design/colors@^7.0.0", "@ant-design/colors@^7.2.0":
version "7.2.0"
resolved "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.0.tgz"
integrity sha512-bjTObSnZ9C/O8MB/B4OUtd/q9COomuJAR2SYfhxLyHvCKn4EKwCN3e+fWGMo7H5InAyV0wL17jdE9ALrdOW/6A==
"@ant-design/colors@^7.0.0", "@ant-design/colors@^7.2.1":
version "7.2.1"
resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-7.2.1.tgz#3bbc1c6c18550020d1622a0067ff03492318df98"
integrity sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==
dependencies:
"@ant-design/fast-color" "^2.0.6"
"@ant-design/colors@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-8.0.0.tgz#92b5aa1cd44896b62c7b67133b4d5a6a00266162"
integrity sha512-6YzkKCw30EI/E9kHOIXsQDHmMvTllT8STzjMb4K2qzit33RW2pqCJP0sk+hidBntXxE+Vz4n1+RvCTfBw6OErw==
dependencies:
"@ant-design/fast-color" "^3.0.0"
"@ant-design/cssinjs-utils@^1.1.3":
version "1.1.3"
resolved "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz"
@@ -194,12 +201,17 @@
dependencies:
"@babel/runtime" "^7.24.7"
"@ant-design/fast-color@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@ant-design/fast-color/-/fast-color-3.0.0.tgz#fb5178203de825f284809538f5142203d0ef3d80"
integrity sha512-eqvpP7xEDm2S7dUzl5srEQCBTXZMmY3ekf97zI+M2DHOYyKdJGH0qua0JACHTqbkRnD/KHFQP9J1uMJ/XWVzzA==
"@ant-design/icons-svg@^4.4.0":
version "4.4.2"
resolved "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz"
integrity sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==
"@ant-design/icons@^5.5.2", "@ant-design/icons@^5.6.1":
"@ant-design/icons@^5.6.1":
version "5.6.1"
resolved "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz"
integrity sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==
@@ -210,6 +222,16 @@
classnames "^2.2.6"
rc-util "^5.31.1"
"@ant-design/icons@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.0.0.tgz#302c935b8b0b429e4444cbc45809247276186d94"
integrity sha512-o0aCCAlHc1o4CQcapAwWzHeaW2x9F49g7P3IDtvtNXgHowtRWYb7kiubt8sQPFvfVIVU/jLw2hzeSlNt0FU+Uw==
dependencies:
"@ant-design/colors" "^8.0.0"
"@ant-design/icons-svg" "^4.4.0"
"@rc-component/util" "^1.2.1"
classnames "^2.2.6"
"@ant-design/react-slick@~1.1.2":
version "1.1.2"
resolved "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz"
@@ -1084,15 +1106,7 @@
"@babel/plugin-transform-modules-commonjs" "^7.26.3"
"@babel/plugin-transform-typescript" "^7.27.0"
"@babel/runtime-corejs3@^7.20.7", "@babel/runtime-corejs3@^7.22.15", "@babel/runtime-corejs3@^7.25.9", "@babel/runtime-corejs3@^7.26.10":
version "7.27.0"
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.27.0.tgz#c766df350ec7a2caf3ed64e3659b100954589413"
integrity sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew==
dependencies:
core-js-pure "^3.30.2"
regenerator-runtime "^0.14.0"
"@babel/runtime-corejs3@^7.27.1":
"@babel/runtime-corejs3@^7.20.7", "@babel/runtime-corejs3@^7.22.15", "@babel/runtime-corejs3@^7.25.9", "@babel/runtime-corejs3@^7.26.10", "@babel/runtime-corejs3@^7.27.1":
version "7.27.6"
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.27.6.tgz#97644153808a62898e7c05f3361501417db3c48b"
integrity sha512-vDVrlmRAY8z9Ul/HxT+8ceAru95LQgkSKiXkSYZvqtbkPSfhZJgpRp45Cldbh1GJ1kxzQkI70AqyrTI58KpaWQ==
@@ -1965,10 +1979,10 @@
fs-extra "^11.1.1"
tslib "^2.6.0"
"@docusaurus/tsconfig@^3.8.0":
version "3.8.0"
resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.8.0.tgz#ea7ee0917e1562cf0a6e95e049c42f1f61351f32"
integrity sha512-utLl48nNjSYBoq47RKukZ9fPLEX3nJWThzrujb0ndQQ1jc/gh4RhTRaAqItH9nImnsgGKmLMnyoMBpfGmoop+w==
"@docusaurus/tsconfig@^3.8.1":
version "3.8.1"
resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.8.1.tgz#a1f7daadfc93455289200647f4ee10cdca540f7b"
integrity sha512-XBWCcqhRHhkhfolnSolNL+N7gj3HVE3CoZVqnVjfsMzCoOsuQw2iCLxVVHtO+rePUUfouVZHURDgmqIySsF66A==
"@docusaurus/types@3.8.1":
version "3.8.1"
@@ -2443,10 +2457,10 @@
classnames "^2.3.2"
rc-util "^5.24.4"
"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.2.6":
version "2.2.6"
resolved "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.6.tgz"
integrity sha512-/9zuTnWwhQ3S3WT1T8BubuFTT46kvnXgaERR9f4BTKyn61/wpf/BvbImzYBubzJibU707FxwbKszLlHjcLiv1Q==
"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.2.7":
version "2.2.7"
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.2.7.tgz#a2b97ecbb93280a3c424e51fa415b371b355d76a"
integrity sha512-Qggj4Z0AA2i5dJhzlfFSmg1Qrziu8dsdHOihROL5Kl18seO2Eh/ZaTYt2c8a/CyGaTChnFry7BEYew1+/fhSbA==
dependencies:
"@babel/runtime" "^7.23.2"
"@rc-component/portal" "^1.1.0"
@@ -2455,6 +2469,13 @@
rc-resize-observer "^1.3.1"
rc-util "^5.44.0"
"@rc-component/util@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.2.1.tgz#2c3158f11a4193478cec44ca42915da31f67d8a0"
integrity sha512-AUVu6jO+lWjQnUOOECwu8iR0EdElQgWW5NBv5vP/Uf9dWbAX3udhMutRlkVXjuac2E40ghkFy+ve00mc/3Fymg==
dependencies:
react-is "^18.2.0"
"@saucelabs/theme-github-codeblock@^0.3.0":
version "0.3.0"
resolved "https://registry.npmjs.org/@saucelabs/theme-github-codeblock/-/theme-github-codeblock-0.3.0.tgz"
@@ -4017,12 +4038,12 @@ 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@^5.25.1:
version "5.25.1"
resolved "https://registry.yarnpkg.com/antd/-/antd-5.25.1.tgz#859b419a18d113492304ccd66c29074a71902241"
integrity sha512-4KC7KuPCjr0z3Vuw9DsF+ceqJaPLbuUI3lOX1sY8ix25ceamp+P8yxOmk3Y2JHCD2ZAhq+5IQ/DTJRN2adWYKQ==
antd@^5.26.3:
version "5.26.3"
resolved "https://registry.yarnpkg.com/antd/-/antd-5.26.3.tgz#cbbb7e1b48a972dc7b6ee8b6948f51cc91c263f8"
integrity sha512-M/s9Q39h/+G7AWnS6fbNxmAI9waTH4ti022GVEXBLq2j810V1wJ3UOQps13nEilzDNcyxnFN/EIbqIgS7wSYaA==
dependencies:
"@ant-design/colors" "^7.2.0"
"@ant-design/colors" "^7.2.1"
"@ant-design/cssinjs" "^1.23.0"
"@ant-design/cssinjs-utils" "^1.1.3"
"@ant-design/fast-color" "^2.0.6"
@@ -4033,7 +4054,7 @@ antd@^5.25.1:
"@rc-component/mutate-observer" "^1.1.0"
"@rc-component/qrcode" "~1.0.0"
"@rc-component/tour" "~1.15.1"
"@rc-component/trigger" "^2.2.6"
"@rc-component/trigger" "^2.2.7"
classnames "^2.5.1"
copy-to-clipboard "^3.3.3"
dayjs "^1.11.11"
@@ -4041,7 +4062,7 @@ antd@^5.25.1:
rc-checkbox "~3.5.0"
rc-collapse "~3.9.0"
rc-dialog "~9.6.0"
rc-drawer "~7.2.0"
rc-drawer "~7.3.0"
rc-dropdown "~4.2.1"
rc-field-form "~2.7.0"
rc-image "~7.12.0"
@@ -4057,17 +4078,17 @@ antd@^5.25.1:
rc-rate "~2.13.1"
rc-resize-observer "^1.4.3"
rc-segmented "~2.7.0"
rc-select "~14.16.7"
rc-select "~14.16.8"
rc-slider "~11.1.8"
rc-steps "~6.0.1"
rc-switch "~4.1.0"
rc-table "~7.50.4"
rc-table "~7.51.1"
rc-tabs "~15.6.1"
rc-textarea "~1.10.0"
rc-tooltip "~6.4.0"
rc-tree "~5.13.1"
rc-tree-select "~5.27.0"
rc-upload "~4.9.0"
rc-upload "~4.9.2"
rc-util "^5.44.4"
scroll-into-view-if-needed "^3.1.0"
throttle-debounce "^5.0.2"
@@ -10377,10 +10398,10 @@ rc-dialog@~9.6.0:
rc-motion "^2.3.0"
rc-util "^5.21.0"
rc-drawer@~7.2.0:
version "7.2.0"
resolved "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.2.0.tgz"
integrity sha512-9lOQ7kBekEJRdEpScHvtmEtXnAsy+NGDXiRWc2ZVC7QXAazNVbeT4EraQKYwCME8BJLa8Bxqxvs5swwyOepRwg==
rc-drawer@~7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-7.3.0.tgz#1bb5fe5f9da38b6a2b2a7dffc9fcb647252a328f"
integrity sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==
dependencies:
"@babel/runtime" "^7.23.9"
"@rc-component/portal" "^1.1.1"
@@ -10552,7 +10573,7 @@ rc-segmented@~2.7.0:
rc-motion "^2.4.4"
rc-util "^5.17.0"
rc-select@~14.16.2, rc-select@~14.16.7:
rc-select@~14.16.2, rc-select@~14.16.8:
version "14.16.8"
resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-14.16.8.tgz#78e6782f1ccc1f03d9003bc3effa4ed609d29a97"
integrity sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==
@@ -10592,10 +10613,10 @@ rc-switch@~4.1.0:
classnames "^2.2.1"
rc-util "^5.30.0"
rc-table@~7.50.4:
version "7.50.4"
resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.50.4.tgz#687b5bf76d1a94168f75481cbc83be9442010432"
integrity sha512-Y+YuncnQqoS5e7yHvfvlv8BmCvwDYDX/2VixTBEhkMDk9itS9aBINp4nhzXFKiBP/frG4w0pS9d9Rgisl0T1Bw==
rc-table@~7.51.1:
version "7.51.1"
resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.51.1.tgz#cd69ae3262d3b61e4c93c979c12786906e944691"
integrity sha512-5iq15mTHhvC42TlBLRCoCBLoCmGlbRZAlyF21FonFnS/DIC8DeRqnmdyVREwt2CFbPceM0zSNdEeVfiGaqYsKw==
dependencies:
"@babel/runtime" "^7.10.1"
"@rc-component/context" "^1.4.0"
@@ -10660,10 +10681,10 @@ rc-tree@~5.13.0, rc-tree@~5.13.1:
rc-util "^5.16.1"
rc-virtual-list "^3.5.1"
rc-upload@~4.9.0:
version "4.9.0"
resolved "https://registry.yarnpkg.com/rc-upload/-/rc-upload-4.9.0.tgz#911963ab5a0b538c743765371c05e2de9e3f5436"
integrity sha512-pAzlPnyiFn1GCtEybEG2m9nXNzQyWXqWV2xFYCmDxjN9HzyjS5Pz2F+pbNdYw8mMJsixLEKLG0wVy9vOGxJMJA==
rc-upload@~4.9.2:
version "4.9.2"
resolved "https://registry.yarnpkg.com/rc-upload/-/rc-upload-4.9.2.tgz#297f52fd1b1c2a4b570c3e42444609b7530531bb"
integrity sha512-nHx+9rbd1FKMiMRYsqQ3NkXUv7COHPBo3X1Obwq9SWS6/diF/A0aJ5OHubvwUAIDs+4RMleljV0pcrNUc823GQ==
dependencies:
"@babel/runtime" "^7.18.3"
classnames "^2.2.5"
@@ -11998,10 +12019,10 @@ swagger-client@^3.35.5:
ramda "^0.30.1"
ramda-adjunct "^5.1.0"
swagger-ui-react@^5.25.2:
version "5.25.2"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.25.2.tgz#27e8570c7225a0beffcfae26b569c8dd1274c545"
integrity sha512-derGtL7NKvF0w4XIULgiWmS1y3EMEVQAJwLVLTAzVo6/ilLhwP9HCz2yjkVVHzK58gvOzrhO3DKk/Effpr5LJw==
swagger-ui-react@^5.26.0:
version "5.26.0"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.26.0.tgz#b15a903d556cc0ec2a56a969beb9d5bc9ea52910"
integrity sha512-4e6bP9bdJyh+SqQW0lxulPn/SDno4+oWrKXsuon5Z9kjtV0zeoWEJ1c70Qxp8kN/c3caFwec8OyxDNhvo14pkw==
dependencies:
"@babel/runtime-corejs3" "^7.27.1"
"@scarf/scarf" "=1.4.0"

View File

@@ -40,12 +40,13 @@ dependencies = [
"click>=8.0.3",
"click-option-group",
"colorama",
"flask-cors>=4.0.2, <7.0",
"croniter>=0.3.28",
"cron-descriptor",
"cryptography>=42.0.4, <45.0.0",
"deprecation>=2.1.0, <2.2.0",
"flask>=2.2.5, <3.0.0",
"flask-appbuilder>=4.7.0, <5.0.0",
"flask-appbuilder>=4.8.0, <5.0.0",
"flask-caching>=2.1.0, <3",
"flask-compress>=1.13, <2.0",
"flask-talisman>=1.0.0, <2.0",
@@ -115,7 +116,6 @@ bigquery = [
]
clickhouse = ["clickhouse-connect>=0.5.14, <1.0"]
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
cors = ["flask-cors>=4.0.2, <5.0"]
crate = ["sqlalchemy-cratedb>=0.40.1, <1"]
databend = ["databend-sqlalchemy>=0.3.2, <1.0"]
databricks = [

View File

@@ -7,7 +7,14 @@ To alter the pinned dependency, you can edit/alter the `.in` and `pyproject.toml
```bash
./scripts/uv-pip-compile.sh
```
:::warning
The pinned dependencies are based on the `current` version of python supported in Superset.
Output of `./scripts/uv-pip-compile.sh` may vary slightly based on the python version you are using to run the command.
Check the `pyproject.toml` file for the current version of python supported.
:::
This will generate the pinned requirements in the `.txt` files, which will be used in our CI/CD pipelines and in the Docker images.
We recommend to everyone in the community to use the pinned requirements in their local development environments, to ensure consistency across different environments, though we don't force requirements as part of our python package semantics to allow flexibility for users to install different versions of the dependencies if they wish.
Note that `development.txt` is a superset of what's in `base.txt`, and all version numbers for shared library should fully match at all times. `translations.txt` is meant as a supplemental file to be used in conjunction with the other requirements files, and is not meant to be used standalone.

View File

@@ -104,6 +104,7 @@ flask==2.3.3
# flask-babel
# flask-caching
# flask-compress
# flask-cors
# flask-jwt-extended
# flask-limiter
# flask-login
@@ -111,7 +112,7 @@ flask==2.3.3
# flask-session
# flask-sqlalchemy
# flask-wtf
flask-appbuilder==4.7.0
flask-appbuilder==4.8.0
# via apache-superset (pyproject.toml)
flask-babel==2.0.0
# via flask-appbuilder
@@ -119,6 +120,8 @@ flask-caching==2.3.1
# via apache-superset (pyproject.toml)
flask-compress==1.17
# via apache-superset (pyproject.toml)
flask-cors==4.0.2
# via apache-superset (pyproject.toml)
flask-jwt-extended==4.7.1
# via flask-appbuilder
flask-limiter==3.12

View File

@@ -16,4 +16,4 @@
# specific language governing permissions and limitations
# under the License.
#
-e .[development,bigquery,cors,druid,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]
-e .[development,bigquery,druid,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]

View File

@@ -1,19 +1,28 @@
# This file was autogenerated by uv via the following command:
# uv pip compile pyproject.toml requirements/development.in -o requirements/development.txt
# uv pip compile requirements/development.in -c requirements/base.txt -o requirements/development.txt
-e .
# via -r requirements/development.in
alembic==1.15.2
# via flask-migrate
# via
# -c requirements/base.txt
# flask-migrate
amqp==5.3.1
# via kombu
# via
# -c requirements/base.txt
# kombu
apispec==6.6.1
# via flask-appbuilder
# via
# -c requirements/base.txt
# flask-appbuilder
apsw==3.50.1.0
# via shillelagh
# via
# -c requirements/base.txt
# shillelagh
astroid==3.3.10
# via pylint
attrs==25.3.0
# via
# -c requirements/base.txt
# cattrs
# jsonschema
# outcome
@@ -21,50 +30,69 @@ attrs==25.3.0
# requests-cache
# trio
babel==2.17.0
# via flask-babel
# via
# -c requirements/base.txt
# flask-babel
backoff==2.2.1
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
bcrypt==4.3.0
# via paramiko
# via
# -c requirements/base.txt
# paramiko
billiard==4.2.1
# via celery
# via
# -c requirements/base.txt
# celery
blinker==1.9.0
# via flask
# via
# -c requirements/base.txt
# flask
bottleneck==1.5.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
brotli==1.1.0
# via flask-compress
# via
# -c requirements/base.txt
# flask-compress
cachelib==0.13.0
# via
# -c requirements/base.txt
# flask-caching
# flask-session
cachetools==5.5.2
# via google-auth
# via
# -c requirements/base.txt
# google-auth
cattrs==25.1.1
# via requests-cache
# via
# -c requirements/base.txt
# requests-cache
celery==5.5.2
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
certifi==2025.6.15
# via
# -c requirements/base.txt
# requests
# selenium
cffi==1.17.1
# via
# -c requirements/base.txt
# cryptography
# pynacl
cfgv==3.4.0
# via pre-commit
charset-normalizer==3.4.2
# via requests
# via
# -c requirements/base.txt
# requests
click==8.2.1
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# celery
# click-didyoumean
@@ -74,20 +102,26 @@ click==8.2.1
# flask
# flask-appbuilder
click-didyoumean==0.3.1
# via celery
# via
# -c requirements/base.txt
# celery
click-option-group==0.5.7
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
click-plugins==1.1.1
# via celery
# via
# -c requirements/base.txt
# celery
click-repl==0.3.0
# via celery
# via
# -c requirements/base.txt
# celery
cmdstanpy==1.1.0
# via prophet
colorama==0.4.6
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# flask-appbuilder
contourpy==1.0.7
@@ -96,15 +130,15 @@ coverage==7.6.8
# via pytest-cov
cron-descriptor==1.4.5
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
croniter==6.0.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
cryptography==44.0.3
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# paramiko
# pyopenssl
@@ -113,30 +147,40 @@ cycler==0.12.1
db-dtypes==1.3.1
# via pandas-gbq
defusedxml==0.7.1
# via odfpy
# via
# -c requirements/base.txt
# odfpy
deprecated==1.2.18
# via limits
# via
# -c requirements/base.txt
# limits
deprecation==2.1.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
dill==0.4.0
# via pylint
distlib==0.3.8
# via virtualenv
dnspython==2.7.0
# via email-validator
# via
# -c requirements/base.txt
# email-validator
docker==7.0.0
# via apache-superset
email-validator==2.2.0
# via flask-appbuilder
# via
# -c requirements/base.txt
# flask-appbuilder
et-xmlfile==2.0.0
# via openpyxl
# via
# -c requirements/base.txt
# openpyxl
filelock==3.12.2
# via virtualenv
flask==2.3.3
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# flask-appbuilder
# flask-babel
@@ -151,52 +195,61 @@ flask==2.3.3
# flask-sqlalchemy
# flask-testing
# flask-wtf
flask-appbuilder==4.7.0
flask-appbuilder==4.8.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
flask-babel==2.0.0
# via flask-appbuilder
# via
# -c requirements/base.txt
# flask-appbuilder
flask-caching==2.3.1
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
flask-compress==1.17
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
flask-cors==4.0.2
# via apache-superset
# via
# -c requirements/base.txt
# apache-superset
flask-jwt-extended==4.7.1
# via flask-appbuilder
# via
# -c requirements/base.txt
# flask-appbuilder
flask-limiter==3.12
# via flask-appbuilder
# via
# -c requirements/base.txt
# flask-appbuilder
flask-login==0.6.3
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# flask-appbuilder
flask-migrate==3.1.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
flask-session==0.8.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
flask-sqlalchemy==2.5.1
# via
# -c requirements/base.txt
# flask-appbuilder
# flask-migrate
flask-talisman==1.1.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
flask-testing==0.8.1
# via apache-superset
flask-wtf==1.2.2
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# flask-appbuilder
fonttools==4.55.0
@@ -206,10 +259,12 @@ freezegun==1.5.1
future==1.0.0
# via pyhive
geographiclib==2.0
# via geopy
# via
# -c requirements/base.txt
# geopy
geopy==2.4.1
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
gevent==24.2.1
# via apache-superset
@@ -222,6 +277,7 @@ google-api-core==2.23.0
# sqlalchemy-bigquery
google-auth==2.40.3
# via
# -c requirements/base.txt
# google-api-core
# google-auth-oauthlib
# google-cloud-bigquery
@@ -253,7 +309,7 @@ googleapis-common-protos==1.66.0
# grpcio-status
greenlet==3.1.1
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# gevent
# shillelagh
@@ -266,27 +322,30 @@ grpcio-status==1.60.1
# via google-api-core
gunicorn==23.0.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
h11==0.16.0
# via wsproto
# via
# -c requirements/base.txt
# wsproto
hashids==1.3.1
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
holidays==0.25
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# prophet
humanize==4.12.3
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
identify==2.5.36
# via pre-commit
idna==3.10
# via
# -c requirements/base.txt
# email-validator
# requests
# trio
@@ -297,24 +356,27 @@ iniconfig==2.0.0
# via pytest
isodate==0.7.2
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
isort==6.0.1
# via pylint
itsdangerous==2.2.0
# via
# -c requirements/base.txt
# flask
# flask-wtf
jinja2==3.1.6
# via
# -c requirements/base.txt
# flask
# flask-babel
jsonpath-ng==1.7.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
jsonschema==4.23.0
# via
# -c requirements/base.txt
# flask-appbuilder
# openapi-schema-validator
# openapi-spec-validator
@@ -322,66 +384,82 @@ jsonschema-path==0.3.4
# via openapi-spec-validator
jsonschema-specifications==2025.4.1
# via
# -c requirements/base.txt
# jsonschema
# openapi-schema-validator
kiwisolver==1.4.7
# via matplotlib
kombu==5.5.3
# via celery
# via
# -c requirements/base.txt
# celery
korean-lunar-calendar==0.3.1
# via holidays
# via
# -c requirements/base.txt
# holidays
lazy-object-proxy==1.10.0
# via openapi-spec-validator
limits==5.1.0
# via flask-limiter
# via
# -c requirements/base.txt
# flask-limiter
mako==1.3.10
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# alembic
# apache-superset
markdown==3.8
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
markdown-it-py==3.0.0
# via rich
# via
# -c requirements/base.txt
# rich
markupsafe==3.0.2
# via
# -c requirements/base.txt
# jinja2
# mako
# werkzeug
# wtforms
marshmallow==3.26.1
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# flask-appbuilder
# marshmallow-sqlalchemy
marshmallow-sqlalchemy==1.4.0
# via flask-appbuilder
# via
# -c requirements/base.txt
# flask-appbuilder
matplotlib==3.9.0
# via prophet
mccabe==0.7.0
# via pylint
mdurl==0.1.2
# via markdown-it-py
# via
# -c requirements/base.txt
# markdown-it-py
msgpack==1.0.8
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
msgspec==0.19.0
# via flask-session
# via
# -c requirements/base.txt
# flask-session
mysqlclient==2.2.6
# via apache-superset
nh3==0.2.21
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
nodeenv==1.8.0
# via pre-commit
numpy==1.26.4
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# bottleneck
# cmdstanpy
@@ -394,22 +472,31 @@ numpy==1.26.4
oauthlib==3.2.2
# via requests-oauthlib
odfpy==1.4.1
# via pandas
# via
# -c requirements/base.txt
# pandas
openapi-schema-validator==0.6.3
# via openapi-spec-validator
# via
# -c requirements/base.txt
# openapi-spec-validator
openapi-spec-validator==0.7.1
# via apache-superset
openpyxl==3.1.5
# via pandas
# via
# -c requirements/base.txt
# pandas
ordered-set==4.1.0
# via flask-limiter
# via
# -c requirements/base.txt
# flask-limiter
outcome==1.3.0.post0
# via
# -c requirements/base.txt
# trio
# trio-websocket
packaging==25.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# apispec
# db-dtypes
@@ -425,7 +512,7 @@ packaging==25.0
# sqlalchemy-bigquery
pandas==2.0.3
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# cmdstanpy
# db-dtypes
@@ -437,18 +524,18 @@ parameterized==0.9.0
# via apache-superset
paramiko==3.5.1
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# sshtunnel
parsedatetime==2.6
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
pathable==0.4.3
# via jsonschema-path
pgsanity==0.2.9
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
pillow==10.3.0
# via
@@ -456,25 +543,32 @@ pillow==10.3.0
# matplotlib
platformdirs==4.3.8
# via
# -c requirements/base.txt
# pylint
# requests-cache
# virtualenv
pluggy==1.5.0
# via pytest
ply==3.11
# via jsonpath-ng
# via
# -c requirements/base.txt
# jsonpath-ng
polyline==2.0.2
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
pre-commit==4.1.0
# via apache-superset
prison==0.2.1
# via flask-appbuilder
# via
# -c requirements/base.txt
# flask-appbuilder
progress==1.6
# via apache-superset
prompt-toolkit==3.0.51
# via click-repl
# via
# -c requirements/base.txt
# click-repl
prophet==1.1.5
# via apache-superset
proto-plus==1.25.0
@@ -494,21 +588,25 @@ psycopg2-binary==2.9.6
# via apache-superset
pyarrow==18.1.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# db-dtypes
# pandas-gbq
pyasn1==0.6.1
# via
# -c requirements/base.txt
# pyasn1-modules
# python-ldap
# rsa
pyasn1-modules==0.4.2
# via
# -c requirements/base.txt
# google-auth
# python-ldap
pycparser==2.22
# via cffi
# via
# -c requirements/base.txt
# cffi
pydata-google-auth==1.9.0
# via pandas-gbq
pydruid==0.6.9
@@ -516,30 +614,38 @@ pydruid==0.6.9
pyfakefs==5.3.5
# via apache-superset
pygments==2.19.1
# via rich
# via
# -c requirements/base.txt
# rich
pyhive==0.7.0
# via apache-superset
pyinstrument==4.4.0
# via apache-superset
pyjwt==2.10.1
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# flask-appbuilder
# flask-jwt-extended
pylint==3.3.7
# via apache-superset
pynacl==1.5.0
# via paramiko
# via
# -c requirements/base.txt
# paramiko
pyopenssl==25.1.0
# via shillelagh
# via
# -c requirements/base.txt
# shillelagh
pyparsing==3.2.3
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# matplotlib
pysocks==1.7.1
# via urllib3
# via
# -c requirements/base.txt
# urllib3
pytest==7.4.4
# via
# apache-superset
@@ -551,7 +657,7 @@ pytest-mock==3.10.0
# via apache-superset
python-dateutil==2.9.0.post0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# celery
# croniter
@@ -566,40 +672,45 @@ python-dateutil==2.9.0.post0
# trino
python-dotenv==1.1.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
python-geohash==0.8.5
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
python-ldap==3.4.4
# via apache-superset
pytz==2025.2
# via
# -c requirements/base.txt
# croniter
# flask-babel
# pandas
# trino
pyxlsb==1.0.10
# via pandas
# via
# -c requirements/base.txt
# pandas
pyyaml==6.0.2
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# apispec
# jsonschema-path
# pre-commit
redis==4.6.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
referencing==0.36.2
# via
# -c requirements/base.txt
# jsonschema
# jsonschema-path
# jsonschema-specifications
requests==2.32.4
# via
# -c requirements/base.txt
# docker
# google-api-core
# google-cloud-bigquery
@@ -611,24 +722,33 @@ requests==2.32.4
# shillelagh
# trino
requests-cache==1.2.1
# via shillelagh
# via
# -c requirements/base.txt
# shillelagh
requests-oauthlib==2.0.0
# via google-auth-oauthlib
rfc3339-validator==0.1.4
# via openapi-schema-validator
# via
# -c requirements/base.txt
# openapi-schema-validator
rich==13.9.4
# via flask-limiter
# via
# -c requirements/base.txt
# flask-limiter
rpds-py==0.25.0
# via
# -c requirements/base.txt
# jsonschema
# referencing
rsa==4.9.1
# via google-auth
# via
# -c requirements/base.txt
# google-auth
ruff==0.8.0
# via apache-superset
selenium==4.32.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
setuptools==80.7.1
# via
@@ -639,29 +759,34 @@ setuptools==80.7.1
# zope-interface
shillelagh==1.3.5
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
simplejson==3.20.1
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
six==1.17.0
# via
# -c requirements/base.txt
# prison
# python-dateutil
# rfc3339-validator
# wtforms-json
slack-sdk==3.35.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
sniffio==1.3.1
# via trio
# via
# -c requirements/base.txt
# trio
sortedcontainers==2.4.0
# via trio
# via
# -c requirements/base.txt
# trio
sqlalchemy==1.4.54
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# alembic
# apache-superset
# flask-appbuilder
@@ -674,24 +799,24 @@ sqlalchemy-bigquery==1.12.0
# via apache-superset
sqlalchemy-utils==0.38.3
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# flask-appbuilder
sqlglot==26.28.1
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
sqloxide==0.1.51
# via apache-superset
sshtunnel==0.4.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
statsd==4.0.1
# via apache-superset
tabulate==0.9.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
tomlkit==0.13.3
# via pylint
@@ -703,13 +828,16 @@ trino==0.330.0
# via apache-superset
trio==0.30.0
# via
# -c requirements/base.txt
# selenium
# trio-websocket
trio-websocket==0.12.2
# via selenium
# via
# -c requirements/base.txt
# selenium
typing-extensions==4.14.0
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# alembic
# apache-superset
# cattrs
@@ -720,55 +848,71 @@ typing-extensions==4.14.0
# shillelagh
tzdata==2025.2
# via
# -c requirements/base.txt
# kombu
# pandas
tzlocal==5.2
# via trino
url-normalize==2.2.1
# via requests-cache
# via
# -c requirements/base.txt
# requests-cache
urllib3==2.5.0
# via
# -c requirements/base.txt
# docker
# requests
# requests-cache
# selenium
vine==5.1.0
# via
# -c requirements/base.txt
# amqp
# celery
# kombu
virtualenv==20.29.2
# via pre-commit
wcwidth==0.2.13
# via prompt-toolkit
# via
# -c requirements/base.txt
# prompt-toolkit
websocket-client==1.8.0
# via selenium
# via
# -c requirements/base.txt
# selenium
werkzeug==3.1.3
# via
# -c requirements/base.txt
# flask
# flask-appbuilder
# flask-jwt-extended
# flask-login
wrapt==1.17.2
# via deprecated
# via
# -c requirements/base.txt
# deprecated
wsproto==1.2.0
# via trio-websocket
# via
# -c requirements/base.txt
# trio-websocket
wtforms==3.2.1
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# flask-appbuilder
# flask-wtf
# wtforms-json
wtforms-json==0.3.5
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
xlrd==2.0.1
# via pandas
# via
# -c requirements/base.txt
# pandas
xlsxwriter==3.0.9
# via
# apache-superset (pyproject.toml)
# -c requirements/base.txt
# apache-superset
# pandas
zope-event==5.0
@@ -776,4 +920,6 @@ zope-event==5.0
zope-interface==5.4.0
# via gevent
zstandard==0.23.0
# via flask-compress
# via
# -c requirements/base.txt
# flask-compress

View File

@@ -1,430 +1,4 @@
# This file was autogenerated by uv via the following command:
# uv pip compile pyproject.toml requirements/translations.in -o requirements/translations.txt
alembic==1.16.2
# via flask-migrate
amqp==5.3.1
# via kombu
apispec==6.8.2
# via flask-appbuilder
apsw==3.50.2.0
# via shillelagh
attrs==25.3.0
# via
# cattrs
# jsonschema
# outcome
# referencing
# requests-cache
# trio
# uv pip compile requirements/translations.in -o requirements/translations.txt
babel==2.17.0
# via
# -r requirements/translations.in
# flask-babel
backoff==2.2.1
# via apache-superset (pyproject.toml)
bcrypt==4.3.0
# via paramiko
billiard==4.2.1
# via celery
blinker==1.9.0
# via flask
bottleneck==1.5.0
# via apache-superset (pyproject.toml)
brotli==1.1.0
# via flask-compress
cachelib==0.13.0
# via
# flask-caching
# flask-session
cachetools==5.5.2
# via google-auth
cattrs==25.1.1
# via requests-cache
celery==5.5.3
# via apache-superset (pyproject.toml)
certifi==2025.6.15
# via
# requests
# selenium
cffi==1.17.1
# via
# cryptography
# pynacl
charset-normalizer==3.4.2
# via requests
click==8.2.1
# via
# apache-superset (pyproject.toml)
# celery
# click-didyoumean
# click-option-group
# click-plugins
# click-repl
# flask
# flask-appbuilder
click-didyoumean==0.3.1
# via celery
click-option-group==0.5.7
# via apache-superset (pyproject.toml)
click-plugins==1.1.1.2
# via celery
click-repl==0.3.0
# via celery
colorama==0.4.6
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
cron-descriptor==1.4.5
# via apache-superset (pyproject.toml)
croniter==6.0.0
# via apache-superset (pyproject.toml)
cryptography==44.0.3
# via
# apache-superset (pyproject.toml)
# paramiko
# pyopenssl
defusedxml==0.7.1
# via odfpy
deprecated==1.2.18
# via limits
deprecation==2.1.0
# via apache-superset (pyproject.toml)
dnspython==2.7.0
# via email-validator
email-validator==2.2.0
# via flask-appbuilder
et-xmlfile==2.0.0
# via openpyxl
flask==2.3.3
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
# flask-babel
# flask-caching
# flask-compress
# flask-jwt-extended
# flask-limiter
# flask-login
# flask-migrate
# flask-session
# flask-sqlalchemy
# flask-wtf
flask-appbuilder==4.7.0
# via apache-superset (pyproject.toml)
flask-babel==2.0.0
# via flask-appbuilder
flask-caching==2.3.1
# via apache-superset (pyproject.toml)
flask-compress==1.17
# via apache-superset (pyproject.toml)
flask-jwt-extended==4.7.1
# via flask-appbuilder
flask-limiter==3.12
# via flask-appbuilder
flask-login==0.6.3
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
flask-migrate==3.1.0
# via apache-superset (pyproject.toml)
flask-session==0.8.0
# via apache-superset (pyproject.toml)
flask-sqlalchemy==2.5.1
# via
# flask-appbuilder
# flask-migrate
flask-talisman==1.1.0
# via apache-superset (pyproject.toml)
flask-wtf==1.2.2
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
geographiclib==2.0
# via geopy
geopy==2.4.1
# via apache-superset (pyproject.toml)
google-auth==2.40.3
# via shillelagh
greenlet==3.1.1
# via
# apache-superset (pyproject.toml)
# shillelagh
gunicorn==23.0.0
# via apache-superset (pyproject.toml)
h11==0.16.0
# via wsproto
hashids==1.3.1
# via apache-superset (pyproject.toml)
holidays==0.25
# via apache-superset (pyproject.toml)
humanize==4.12.3
# via apache-superset (pyproject.toml)
idna==3.10
# via
# email-validator
# requests
# trio
# url-normalize
isodate==0.7.2
# via apache-superset (pyproject.toml)
itsdangerous==2.2.0
# via
# flask
# flask-wtf
jinja2==3.1.6
# via
# flask
# flask-babel
jsonpath-ng==1.7.0
# via apache-superset (pyproject.toml)
jsonschema==4.24.0
# via flask-appbuilder
jsonschema-specifications==2025.4.1
# via jsonschema
kombu==5.5.4
# via celery
korean-lunar-calendar==0.3.1
# via holidays
limits==5.4.0
# via flask-limiter
mako==1.3.10
# via
# apache-superset (pyproject.toml)
# alembic
markdown==3.8.2
# via apache-superset (pyproject.toml)
markdown-it-py==3.0.0
# via rich
markupsafe==3.0.2
# via
# jinja2
# mako
# werkzeug
# wtforms
marshmallow==3.26.1
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
# marshmallow-sqlalchemy
marshmallow-sqlalchemy==1.4.2
# via flask-appbuilder
mdurl==0.1.2
# via markdown-it-py
msgpack==1.0.8
# via apache-superset (pyproject.toml)
msgspec==0.19.0
# via flask-session
nh3==0.2.21
# via apache-superset (pyproject.toml)
numpy==2.2.6
# via
# apache-superset (pyproject.toml)
# bottleneck
# pandas
odfpy==1.4.1
# via pandas
openpyxl==3.1.5
# via pandas
ordered-set==4.1.0
# via flask-limiter
outcome==1.3.0.post0
# via
# trio
# trio-websocket
packaging==25.0
# via
# apache-superset (pyproject.toml)
# apispec
# deprecation
# gunicorn
# kombu
# limits
# marshmallow
# shillelagh
pandas==2.0.3
# via apache-superset (pyproject.toml)
paramiko==3.5.1
# via
# apache-superset (pyproject.toml)
# sshtunnel
parsedatetime==2.6
# via apache-superset (pyproject.toml)
pgsanity==0.2.9
# via apache-superset (pyproject.toml)
platformdirs==4.3.8
# via requests-cache
ply==3.11
# via jsonpath-ng
polyline==2.0.2
# via apache-superset (pyproject.toml)
prison==0.2.1
# via flask-appbuilder
prompt-toolkit==3.0.51
# via click-repl
pyarrow==18.1.0
# via apache-superset (pyproject.toml)
pyasn1==0.6.1
# via
# pyasn1-modules
# rsa
pyasn1-modules==0.4.2
# via google-auth
pycparser==2.22
# via cffi
pygments==2.19.2
# via rich
pyjwt==2.10.1
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
# flask-jwt-extended
pynacl==1.5.0
# via paramiko
pyopenssl==25.1.0
# via shillelagh
pyparsing==3.2.3
# via apache-superset (pyproject.toml)
pysocks==1.7.1
# via urllib3
python-dateutil==2.9.0.post0
# via
# apache-superset (pyproject.toml)
# celery
# croniter
# flask-appbuilder
# holidays
# pandas
# shillelagh
python-dotenv==1.1.1
# via apache-superset (pyproject.toml)
python-geohash==0.8.5
# via apache-superset (pyproject.toml)
pytz==2025.2
# via
# croniter
# flask-babel
# pandas
pyxlsb==1.0.10
# via pandas
pyyaml==6.0.2
# via
# apache-superset (pyproject.toml)
# apispec
redis==4.6.0
# via apache-superset (pyproject.toml)
referencing==0.36.2
# via
# jsonschema
# jsonschema-specifications
requests==2.32.4
# via
# requests-cache
# shillelagh
requests-cache==1.2.1
# via shillelagh
rich==13.9.4
# via flask-limiter
rpds-py==0.25.1
# via
# jsonschema
# referencing
rsa==4.9.1
# via google-auth
selenium==4.34.0
# via apache-superset (pyproject.toml)
shillelagh==1.3.5
# via apache-superset (pyproject.toml)
simplejson==3.20.1
# via apache-superset (pyproject.toml)
six==1.17.0
# via
# prison
# python-dateutil
# wtforms-json
slack-sdk==3.35.0
# via apache-superset (pyproject.toml)
sniffio==1.3.1
# via trio
sortedcontainers==2.4.0
# via trio
sqlalchemy==1.4.54
# via
# apache-superset (pyproject.toml)
# alembic
# flask-appbuilder
# flask-sqlalchemy
# marshmallow-sqlalchemy
# shillelagh
# sqlalchemy-utils
sqlalchemy-utils==0.38.3
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
sqlglot==26.31.0
# via apache-superset (pyproject.toml)
sshtunnel==0.4.0
# via apache-superset (pyproject.toml)
tabulate==0.9.0
# via apache-superset (pyproject.toml)
trio==0.30.0
# via
# selenium
# trio-websocket
trio-websocket==0.12.2
# via selenium
typing-extensions==4.14.0
# via
# apache-superset (pyproject.toml)
# alembic
# cattrs
# limits
# pyopenssl
# referencing
# selenium
# shillelagh
tzdata==2025.2
# via
# kombu
# pandas
url-normalize==2.2.1
# via requests-cache
urllib3==2.4.0
# via
# requests
# requests-cache
# selenium
vine==5.1.0
# via
# amqp
# celery
# kombu
wcwidth==0.2.13
# via prompt-toolkit
websocket-client==1.8.0
# via selenium
werkzeug==3.1.3
# via
# flask
# flask-appbuilder
# flask-jwt-extended
# flask-login
wrapt==1.17.2
# via deprecated
wsproto==1.2.0
# via trio-websocket
wtforms==3.2.1
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
# flask-wtf
# wtforms-json
wtforms-json==0.3.5
# via apache-superset (pyproject.toml)
xlrd==2.0.2
# via pandas
xlsxwriter==3.0.9
# via
# apache-superset (pyproject.toml)
# pandas
zstandard==0.23.0
# via flask-compress
# via -r requirements/translations.in

View File

@@ -24,7 +24,8 @@ ADDITIONAL_ARGS="$@"
# Generate the requirements/base.txt file
uv pip compile pyproject.toml requirements/base.in -o requirements/base.txt $ADDITIONAL_ARGS
# Generate the requirements/development.txt file, making sure requirements/base.txt is a constraint to keep the versions in sync
# Generate the requirements/development.txt file, making sure requirements/base.txt is a constraint to keep the versions in sync. Note that `development.txt` is a Superset of `base.txt` where version for the shared libs should match their version.
uv pip compile requirements/development.in -c requirements/base.txt -o requirements/development.txt $ADDITIONAL_ARGS
# NOTE translation is intended as a "supplemental" set of pins that can be combined with either base or dev as needed
uv pip compile requirements/translations.in -o requirements/translations.txt $ADDITIONAL_ARGS

View File

@@ -46,6 +46,7 @@ export type UiConfigType = {
urlParams?: {
[key: string]: any;
};
showRowLimitWarning?: boolean;
};
export type EmbedDashboardParams = {
@@ -133,6 +134,9 @@ export async function embedDashboard({
if (dashboardUiConfig.emitDataMasks) {
configNumber += 16;
}
if (dashboardUiConfig.showRowLimitWarning) {
configNumber += 32;
}
}
return configNumber;
}

View File

@@ -69,6 +69,10 @@ const restrictedImportsRules = {
message:
'Please use the theme directly from the ThemeProvider rather than importing supersetTheme.',
},
'no-query-string': {
name: 'query-string',
message: 'Please use the URLSearchParams API instead of query-string.',
},
};
module.exports = {
@@ -82,9 +86,7 @@ module.exports = {
],
parser: '@babel/eslint-parser',
parserOptions: {
ecmaFeatures: {
experimentalObjectRestSpread: true,
},
ecmaVersion: 2018,
},
env: {
browser: true,
@@ -123,6 +125,11 @@ module.exports = {
'react-prefer-function-component',
'prettier',
],
// Add this TS ESlint rule in separate `rules` section to avoid breakages with JS/TS files in /cypress-base.
// TODO(hainenber): merge it to below `rules` section.
rules: {
'@typescript-eslint/prefer-optional-chain': 'error',
},
overrides: [
{
files: ['*.ts', '*.tsx'],
@@ -156,7 +163,7 @@ module.exports = {
'@typescript-eslint/no-non-null-assertion': 0, // disabled temporarily
'@typescript-eslint/explicit-function-return-type': 0,
'@typescript-eslint/explicit-module-boundary-types': 0, // re-enable up for discussion
'@typescript-eslint/prefer-optional-chain': 2,
'@typescript-eslint/no-unused-vars': 'warn', // downgrade to Warning severity for Jest v30 upgrade
camelcase: 0,
'class-methods-use-this': 0,
'func-names': 0,
@@ -391,6 +398,7 @@ module.exports = {
},
},
],
// eslint-disable-next-line no-dupe-keys
rules: {
'theme-colors/no-literal-colors': 'error',
'icons/no-fa-icons-usage': 'error',

View File

@@ -46,10 +46,10 @@ module.exports = {
plugins: [
'lodash',
'@babel/plugin-syntax-dynamic-import',
['@babel/plugin-proposal-class-properties', { loose: true }],
['@babel/plugin-proposal-optional-chaining', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
['@babel/plugin-proposal-nullish-coalescing-operator', { loose: true }],
['@babel/plugin-transform-class-properties', { loose: true }],
['@babel/plugin-transform-optional-chaining', { loose: true }],
['@babel/plugin-transform-private-methods', { loose: true }],
['@babel/plugin-transform-nullish-coalescing-operator', { loose: true }],
['@babel/plugin-transform-runtime', { corejs: 3 }],
// only used in packages/superset-ui-core/src/chart/components/reactify.tsx
['babel-plugin-typescript-to-proptypes', { loose: true }],

View File

@@ -27,13 +27,6 @@ describe('Login view', () => {
cy.visit(LOGIN);
});
it('should load login page', () => {
cy.getBySel('login-form').should('be.visible');
cy.getBySel('username-input').should('be.visible');
cy.getBySel('password-input').should('be.visible');
cy.getBySel('login-button').should('be.visible');
});
it('should redirect to login with incorrect username and password', () => {
interceptLogin();
cy.getBySel('login-form').should('be.visible');

View File

@@ -21,30 +21,21 @@ import qs from 'querystring';
import {
dashboardView,
nativeFilters,
exploreView,
dataTestChartName,
} from 'cypress/support/directories';
import {
addCountryNameFilter,
addParentFilterWithValue,
applyAdvancedTimeRangeFilterOnDashboard,
applyNativeFilterValueWithIndex,
cancelNativeFilterSettings,
checkNativeFilterTooltip,
clickOnAddFilterInModal,
collapseFilterOnLeftPanel,
deleteNativeFilter,
enterNativeFilterEditModal,
expandFilterOnLeftPanel,
fillNativeFilterForm,
getNativeFilterPlaceholderWithIndex,
inputNativeFilterDefaultValue,
saveNativeFilterSettings,
nativeFilterTooltips,
undoDeleteNativeFilter,
validateFilterContentOnDashboard,
valueNativeFilterOptions,
validateFilterNameOnDashboard,
testItems,
WORLD_HEALTH_CHARTS,
@@ -55,19 +46,19 @@ import {
visitDashboard,
} from './shared_dashboard_functions';
function selectFilter(index: number) {
cy.get("[data-test='filter-title-container'] [draggable='true']")
.eq(index)
.click();
}
// function selectFilter(index: number) {
// cy.get("[data-test='filter-title-container'] [draggable='true']")
// .eq(index)
// .click();
// }
function closeFilterModal() {
cy.get('body').then($body => {
if ($body.find('[data-test="native-filter-modal-cancel-button"]').length) {
cy.getBySel('native-filter-modal-cancel-button').click();
}
});
}
// function closeFilterModal() {
// cy.get('body').then($body => {
// if ($body.find('[data-test="native-filter-modal-cancel-button"]').length) {
// cy.getBySel('native-filter-modal-cancel-button').click();
// }
// });
// }
describe('Native filters', () => {
describe('Nativefilters initial state not required', () => {
@@ -183,46 +174,139 @@ describe('Native filters', () => {
validateFilterContentOnDashboard(testItems.topTenChart.filterColumnYear);
});
it('User can create a numerical range filter', () => {
visitDashboard();
enterNativeFilterEditModal(false);
fillNativeFilterForm(
testItems.filterType.numerical,
testItems.filterNumericalColumn,
testItems.datasetForNativeFilter,
testItems.filterNumericalColumn,
);
saveNativeFilterSettings([]);
// Assertions
cy.get('[data-test="range-filter-from-input"]')
.should('be.visible')
.click();
cy.get('[data-test="range-filter-from-input"]').type('{selectall}40');
cy.get('[data-test="range-filter-to-input"]')
.should('be.visible')
.click();
cy.get('[data-test="range-filter-to-input"]').type('{selectall}50');
cy.get(nativeFilters.applyFilter).click({
force: true,
describe.only('Numerical Range Filter - Display Modes', () => {
beforeEach(() => {
visitDashboard();
});
// Assert that the URL contains 'native_filters'
cy.url().then(u => {
const ur = new URL(u);
expect(ur.search).to.include('native_filters');
const expandFilterConfiguration = () => {
cy.get('.ant-collapse-header')
.contains('Filter Configuration')
.should('be.visible')
.then($header => {
cy.wrap($header)
.closest('.ant-collapse-item')
.invoke('hasClass', 'ant-collapse-item-active')
.then(isExpanded => {
if (!isExpanded) cy.wrap($header).click();
});
});
cy.get('.ant-collapse-content-box').should('be.visible');
};
const selectRangeTypeOption = (label: string) => {
cy.contains('Range Type')
.should('be.visible')
.closest('.ant-form-item')
.within(() => {
cy.get('.ant-select-selector').click();
});
cy.get('.ant-select-dropdown:visible')
.contains('.ant-select-item-option', label)
.click();
};
const applyAndAssertInputs = (from: string, to: string) => {
// Set 'from' input
cy.get('[data-test="range-filter-from-input"]').clear();
cy.get('[data-test="range-filter-from-input"]').type(from);
cy.get('[data-test="range-filter-from-input"]').blur();
// Set 'to' input
cy.get('[data-test="range-filter-to-input"]').clear();
cy.get('[data-test="range-filter-to-input"]').type(to);
cy.get('[data-test="range-filter-to-input"]').blur();
// Assert values without chaining after .invoke()
cy.get('[data-test="range-filter-from-input"]')
.invoke('val')
.should('equal', '40');
.then(val => {
expect(val).to.equal(from);
});
// Assert that the "To" input has the correct value
cy.get('[data-test="range-filter-to-input"]')
.invoke('val')
.should('equal', '50');
.then(val => {
expect(val).to.equal(to);
});
};
it('User can create a numerical range filter with "Range Inputs" display mode', () => {
enterNativeFilterEditModal(false);
fillNativeFilterForm(
testItems.filterType.numerical,
testItems.filterNumericalColumn,
testItems.datasetForNativeFilter,
testItems.filterNumericalColumn,
);
expandFilterConfiguration();
selectRangeTypeOption('Range Inputs');
saveNativeFilterSettings([]);
cy.wait(500); // allow filter to mount
applyAndAssertInputs('40', '70');
});
it('User can change the display mode to "Slider"', () => {
enterNativeFilterEditModal(false);
fillNativeFilterForm(
testItems.filterType.numerical,
testItems.filterNumericalColumn,
testItems.datasetForNativeFilter,
testItems.filterNumericalColumn,
);
expandFilterConfiguration();
cy.contains('Range Type')
.should('be.visible')
.closest('.ant-form-item')
.within(() => {
cy.get('.ant-select-selector').click({ force: true });
});
cy.get('.ant-select-dropdown:visible .ant-select-item-option')
.contains(/^Slider$/)
.click({ force: true });
cy.get('.ant-select-selector').should('contain.text', 'Slider');
saveNativeFilterSettings([]);
cy.get('.ant-slider', { timeout: 10000 }).should('be.visible');
cy.get('[data-test="range-filter-from-input"]', {
timeout: 5000,
}).should('not.exist');
cy.get('[data-test="range-filter-to-input"]', { timeout: 5000 }).should(
'not.exist',
);
});
it('User can change the display mode to "Slider and range input"', () => {
enterNativeFilterEditModal(false);
// Re-create filter
fillNativeFilterForm(
testItems.filterType.numerical,
testItems.filterNumericalColumn,
testItems.datasetForNativeFilter,
testItems.filterNumericalColumn,
);
expandFilterConfiguration();
selectRangeTypeOption('Slider and range input');
saveNativeFilterSettings([]);
cy.wait(500);
applyAndAssertInputs('40', '70');
});
});

View File

@@ -16,37 +16,29 @@
* specific language governing permissions and limitations
* under the License.
*/
import qs from 'querystring';
import {
dashboardView,
nativeFilters,
exploreView,
dataTestChartName,
} from 'cypress/support/directories';
import {
addCountryNameFilter,
addParentFilterWithValue,
applyAdvancedTimeRangeFilterOnDashboard,
applyNativeFilterValueWithIndex,
cancelNativeFilterSettings,
checkNativeFilterTooltip,
clickOnAddFilterInModal,
collapseFilterOnLeftPanel,
deleteNativeFilter,
enterNativeFilterEditModal,
expandFilterOnLeftPanel,
fillNativeFilterForm,
getNativeFilterPlaceholderWithIndex,
inputNativeFilterDefaultValue,
saveNativeFilterSettings,
nativeFilterTooltips,
undoDeleteNativeFilter,
validateFilterContentOnDashboard,
valueNativeFilterOptions,
validateFilterNameOnDashboard,
testItems,
WORLD_HEALTH_CHARTS,
} from './utils';
import {
prepareDashboardFilters,

View File

@@ -363,9 +363,26 @@ export function saveNativeFilterSettings(charts: ChartSpec[]) {
cy.get(nativeFilters.modal.footer)
.contains('Save')
.should('be.visible')
.click();
.click({ force: true });
// Wait for modal to either close or remain open
cy.get('body').should($body => {
const modalExists = $body.find(nativeFilters.modal.container).length > 0;
if (modalExists) {
cy.get(nativeFilters.modal.footer)
.contains('Save')
.should('be.visible')
.click({ force: true });
}
});
// Ensure modal is closed
cy.get(nativeFilters.modal.container).should('not.exist');
charts.forEach(waitForChartLoad);
// Wait for all charts to load
charts.forEach(chart => {
waitForChartLoad(chart);
});
}
/** ************************************************************************

View File

@@ -18,7 +18,6 @@
*/
import '@cypress/code-coverage/support';
import '@applitools/eyes-cypress/commands';
import failOnConsoleError from 'cypress-fail-on-console-error';
import { expect } from 'chai';
import rison from 'rison';

View File

@@ -10,7 +10,7 @@
"allowJs": true,
"noEmit": true
},
"files": ["cypress/support/index.d.ts", "./node_modules/@applitools/eyes-cypress/eyes-index.d.ts"],
"files": ["cypress/support/index.d.ts", "./node_modules/@applitools/eyes-cypress/types/index.d.ts"],
"include": ["cypress/**/*.ts", "./cypress.config.ts"],
"exclude": ["node_modules"]
}

View File

@@ -37,6 +37,7 @@ module.exports = {
setupFilesAfterEnv: ['<rootDir>/spec/helpers/setup.ts'],
snapshotSerializers: ['@emotion/jest/serializer'],
testEnvironmentOptions: {
globalsCleanup: true,
url: 'http://localhost',
},
collectCoverageFrom: [
@@ -55,7 +56,7 @@ module.exports = {
],
coverageReporters: ['lcov', 'json-summary', 'html', 'text'],
transformIgnorePatterns: [
'node_modules/(?!d3-(interpolate|color|time)|remark-gfm|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|jest-enzyme|geostyler|geostyler-.*)',
'node_modules/(?!d3-(interpolate|color|time)|remark-gfm|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es)',
],
preset: 'ts-jest',
transform: {

File diff suppressed because it is too large Load Diff

View File

@@ -84,7 +84,7 @@
"dependencies": {
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.3.0",
"@emotion/styled": "^11.14.1",
"@reduxjs/toolkit": "^1.9.3",
"@rjsf/core": "^5.21.1",
"@rjsf/utils": "^5.24.3",
@@ -109,6 +109,7 @@
"@superset-ui/plugin-chart-handlebars": "file:./plugins/plugin-chart-handlebars",
"@superset-ui/plugin-chart-pivot-table": "file:./plugins/plugin-chart-pivot-table",
"@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table",
"@superset-ui/plugin-chart-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table",
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
"@types/d3-format": "^3.0.1",
@@ -154,7 +155,7 @@
"json-stringify-pretty-compact": "^2.0.0",
"lodash": "^4.17.21",
"luxon": "^3.5.0",
"mapbox-gl": "^2.10.0",
"mapbox-gl": "^3.13.0",
"markdown-to-jsx": "^7.7.4",
"match-sorter": "^6.3.4",
"memoize-one": "^5.2.1",
@@ -164,8 +165,6 @@
"ol": "^7.5.2",
"polished": "^4.3.1",
"prop-types": "^15.8.1",
"query-string": "^6.13.7",
"rc-trigger": "^5.3.4",
"re-resizable": "^6.10.1",
"react": "^17.0.2",
"react-checkbox-tree": "^1.8.0",
@@ -177,7 +176,7 @@
"react-google-recaptcha": "^3.1.0",
"react-hot-loader": "^4.13.1",
"react-intersection-observer": "^9.16.0",
"react-json-tree": "^0.17.0",
"react-json-tree": "^0.20.0",
"react-lines-ellipsis": "^0.15.4",
"react-loadable": "^5.5.0",
"react-redux": "^7.2.9",
@@ -187,7 +186,6 @@
"react-search-input": "^0.11.3",
"react-sortable-hoc": "^2.0.0",
"react-split": "^2.0.9",
"react-syntax-highlighter": "^15.4.5",
"react-table": "^7.8.0",
"react-transition-group": "^4.4.5",
"react-virtualized-auto-sizer": "^1.0.25",
@@ -208,16 +206,12 @@
"yargs": "^17.7.2"
},
"devDependencies": {
"@applitools/eyes-storybook": "^3.50.9",
"@babel/cli": "^7.22.6",
"@applitools/eyes-storybook": "^3.55.6",
"@babel/cli": "^7.27.2",
"@babel/compat-data": "^7.26.8",
"@babel/core": "^7.26.0",
"@babel/eslint-parser": "^7.25.9",
"@babel/node": "^7.22.6",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
"@babel/plugin-transform-runtime": "^7.27.1",
@@ -259,13 +253,11 @@
"@types/node": "^22.12.0",
"@types/react": "^17.0.83",
"@types/react-dom": "^17.0.26",
"@types/react-gravatar": "^2.6.14",
"@types/react-json-tree": "^0.6.11",
"@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-syntax-highlighter": "^15.5.13",
"@types/react-transition-group": "^4.4.12",
"@types/react-ultimate-pagination": "^1.2.4",
"@types/react-virtualized-auto-sizer": "^1.0.4",
@@ -276,17 +268,16 @@
"@types/sinon": "^17.0.3",
"@types/testing-library__jest-dom": "^5.14.9",
"@types/tinycolor2": "^1.4.3",
"@types/yargs": "12 - 18",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"babel-jest": "^29.7.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"babel-jest": "^30.0.2",
"babel-loader": "^10.0.0",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-typescript-to-proptypes": "^2.0.0",
"cheerio": "1.0.0-rc.10",
"copy-webpack-plugin": "^12.0.2",
"cheerio": "1.1.0",
"copy-webpack-plugin": "^13.0.0",
"cross-env": "^7.0.3",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.2",
@@ -316,9 +307,9 @@
"history": "^5.3.0",
"html-webpack-plugin": "^5.6.3",
"imports-loader": "^5.0.0",
"jest": "^29.7.0",
"jest": "^30.0.2",
"jest-environment-jsdom": "^29.7.0",
"jest-html-reporter": "^3.10.2",
"jest-html-reporter": "^4.3.0",
"jest-websocket-mock": "^2.5.0",
"jsdom": "^26.0.0",
"lerna": "^8.2.1",
@@ -337,19 +328,19 @@
"storybook": "8.1.11",
"style-loader": "^4.0.0",
"thread-loader": "^4.0.4",
"ts-jest": "^29.2.5",
"ts-jest": "^29.4.0",
"ts-loader": "^9.5.1",
"tscw-config": "^1.1.2",
"tsx": "^4.19.2",
"typescript": "5.1.6",
"typescript": "5.4.5",
"vm-browserify": "^1.1.2",
"webpack": "^5.98.0",
"webpack": "^5.99.9",
"webpack-bundle-analyzer": "^4.10.1",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.15.1",
"webpack-manifest-plugin": "^5.0.0",
"webpack-sources": "^3.2.3",
"webpack-visualizer-plugin2": "^1.1.0"
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.1",
"webpack-manifest-plugin": "^5.0.1",
"webpack-sources": "^3.3.3",
"webpack-visualizer-plugin2": "^1.2.0"
},
"peerDependencies": {
"ace-builds": "^1.41.0",

View File

@@ -28,7 +28,7 @@ import { <%= packageLabel %>Props, <%= packageLabel %>StylesProps } from './type
// https://github.com/apache-superset/superset-ui/blob/master/packages/superset-ui-core/src/theme/index.ts
const Styles = styled.div<<%= packageLabel %>StylesProps>`
background-color: ${({ theme }) => theme.colors.primary.light2};
background-color: ${({ theme }) => theme.colorPrimaryBg};
padding: ${({ theme }) => theme.sizeUnit * 4}px;
border-radius: ${({ theme }) => theme.borderRadius}px;
height: ${({ height }) => height}px;

View File

@@ -30,14 +30,14 @@
"dependencies": {
"chalk": "^5.4.1",
"lodash-es": "^4.17.21",
"yeoman-generator": "^7.4.0",
"yeoman-generator": "^7.5.1",
"yosay": "^3.0.0"
},
"devDependencies": {
"cross-env": "^7.0.3",
"fs-extra": "^11.2.0",
"jest": "^29.7.0",
"yeoman-test": "^8.3.0"
"fs-extra": "^11.3.0",
"jest": "^30.0.2",
"yeoman-test": "^10.1.1"
},
"engines": {
"npm": ">= 4.0.0",

View File

@@ -27,10 +27,9 @@ import {
import { PostProcessingFactory } from './types';
import { extractExtraMetrics } from './utils';
export const sortOperator: PostProcessingFactory<PostProcessingSort> = (
formData,
queryObject,
) => {
export const sortOperator: PostProcessingFactory<
PostProcessingSort
> = formData => {
// the sortOperator only used in the barchart v2
const sortableLabels = [
getXAxisLabel(formData),

View File

@@ -29,6 +29,7 @@ export const TITLE_POSITION_OPTIONS: [string, string][] = [
['Left', t('Left')],
['Top', t('Top')],
];
export const titleControls: ControlPanelSectionConfig = {
label: t('Chart Title'),
tabOverride: 'customize',
@@ -43,7 +44,6 @@ export const titleControls: ControlPanelSectionConfig = {
label: t('X Axis Title'),
renderTrigger: true,
default: '',
description: t('Changing this control takes effect instantly'),
},
},
],
@@ -58,7 +58,6 @@ export const titleControls: ControlPanelSectionConfig = {
renderTrigger: true,
default: TITLE_MARGIN_OPTIONS[0],
choices: formatSelectOptions(TITLE_MARGIN_OPTIONS),
description: t('Changing this control takes effect instantly'),
},
},
],
@@ -71,7 +70,6 @@ export const titleControls: ControlPanelSectionConfig = {
label: t('Y Axis Title'),
renderTrigger: true,
default: '',
description: t('Changing this control takes effect instantly'),
},
},
],
@@ -86,7 +84,6 @@ export const titleControls: ControlPanelSectionConfig = {
renderTrigger: true,
default: TITLE_MARGIN_OPTIONS[1],
choices: formatSelectOptions(TITLE_MARGIN_OPTIONS),
description: t('Changing this control takes effect instantly'),
},
},
],
@@ -101,7 +98,6 @@ export const titleControls: ControlPanelSectionConfig = {
renderTrigger: true,
default: TITLE_POSITION_OPTIONS[0][0],
choices: TITLE_POSITION_OPTIONS,
description: t('Changing this control takes effect instantly'),
},
},
],

View File

@@ -17,7 +17,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryColumn, t, validateNonEmpty } from '@superset-ui/core';
import {
GenericDataType,
QueryColumn,
t,
validateNonEmpty,
} from '@superset-ui/core';
import {
ExtraControlProps,
SharedControlConfig,
@@ -52,6 +57,19 @@ type Control = {
* feature flags are set and when they're checked.
*/
function filterOptions(
options: (ColumnMeta | QueryColumn)[],
allowedDataTypes?: GenericDataType[],
) {
if (!allowedDataTypes) {
return options;
}
return options.filter(
o =>
o.type_generic !== undefined && allowedDataTypes.includes(o.type_generic),
);
}
export const dndGroupByControl: SharedControlConfig<
'DndColumnSelect' | 'SelectControl',
ColumnMeta
@@ -81,14 +99,20 @@ export const dndGroupByControl: SharedControlConfig<
const newState: ExtraControlProps = {};
const { datasource } = state;
if (datasource?.columns[0]?.hasOwnProperty('groupby')) {
const options = (datasource as Dataset).columns.filter(c => c.groupby);
const options = filterOptions(
(datasource as Dataset).columns.filter(c => c.groupby),
controlState?.allowedDataTypes,
);
if (controlState?.includeTime) {
options.unshift(DATASET_TIME_COLUMN_OPTION);
}
newState.options = options;
newState.savedMetrics = (datasource as Dataset).metrics || [];
} else {
const options = (datasource?.columns as QueryColumn[]) || [];
const options = filterOptions(
(datasource?.columns as QueryColumn[]) || [],
controlState?.allowedDataTypes,
);
if (controlState?.includeTime) {
options.unshift(QUERY_TIME_COLUMN_OPTION);
}
@@ -177,6 +201,19 @@ export const dndAdhocMetricControl: typeof dndAdhocMetricsControl = {
),
};
export const dndTooltipColumnsControl: typeof dndColumnsControl = {
...dndColumnsControl,
label: t('Tooltip (columns)'),
description: t('Columns to show in the tooltip.'),
};
export const dndTooltipMetricsControl: typeof dndAdhocMetricsControl = {
...dndAdhocMetricsControl,
label: t('Tooltip (metrics)'),
description: t('Metrics to show in the tooltip.'),
validators: [],
};
export const dndAdhocMetricControl2: typeof dndAdhocMetricControl = {
...dndAdhocMetricControl,
label: t('Right Axis Metric'),

View File

@@ -45,6 +45,7 @@ import {
isDefined,
NO_TIME_RANGE,
validateMaxValue,
getColumnLabel,
} from '@superset-ui/core';
import {
@@ -82,6 +83,8 @@ import {
dndSeriesControl,
dndAdhocMetricControl2,
dndXAxisControl,
dndTooltipColumnsControl,
dndTooltipMetricsControl,
} from './dndControls';
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
@@ -373,6 +376,14 @@ const temporal_columns_lookup: SharedControlConfig<'HiddenControl'> = {
),
};
const zoomable: SharedControlConfig<'CheckboxControl'> = {
type: 'CheckboxControl',
label: t('Data Zoom'),
default: false,
renderTrigger: true,
description: t('Enable data zooming controls'),
};
const sort_by_metric: SharedControlConfig<'CheckboxControl'> = {
type: 'CheckboxControl',
label: t('Sort by metric'),
@@ -381,6 +392,26 @@ const sort_by_metric: SharedControlConfig<'CheckboxControl'> = {
),
};
const order_by_cols: SharedControlConfig<'SelectControl'> = {
type: 'SelectControl',
label: t('Ordering'),
description: t('Order results by selected columns'),
multi: true,
default: [],
shouldMapStateToProps: () => true,
mapStateToProps: ({ datasource }) => ({
choices: (datasource?.columns || [])
.map(col =>
[true, false].map(asc => [
JSON.stringify([col.column_name, asc]),
`${getColumnLabel(col.column_name)} [${asc ? 'asc' : 'desc'}]`,
]),
)
.flat(),
}),
resetOnHide: false,
};
export default {
metrics: dndAdhocMetricsControl,
metric: dndAdhocMetricControl,
@@ -392,6 +423,8 @@ export default {
secondary_metric: dndSecondaryMetricControl,
groupby: dndGroupByControl,
columns: dndColumnsControl,
tooltip_columns: dndTooltipColumnsControl,
tooltip_metrics: dndTooltipMetricsControl,
granularity,
granularity_sqla: dndGranularitySqlaControl,
time_grain_sqla,
@@ -417,8 +450,10 @@ export default {
legacy_order_by: dndSortByControl,
truncate_metric,
x_axis: dndXAxisControl,
zoomable,
show_empty_columns,
temporal_columns_lookup,
currency_format,
sort_by_metric,
order_by_cols,
};

View File

@@ -90,6 +90,7 @@ export interface Dataset {
database?: Record<string, unknown>;
normalize_columns?: boolean;
always_filter_main_dttm?: boolean;
extra?: object | string;
}
export interface ControlPanelState {
@@ -161,6 +162,7 @@ export type InternalControlType =
| 'DatasourceControl'
| 'DateFilterControl'
| 'FixedOrMetricControl'
| 'ColorBreakpointsControl'
| 'HiddenControl'
| 'SelectAsyncControl'
| 'SelectControl'

View File

@@ -26,10 +26,10 @@
"dependencies": {
"@ant-design/icons": "^5.2.6",
"@babel/runtime": "^7.25.6",
"@fontsource/fira-code": "^5.0.18",
"@fontsource/inter": "^5.0.20",
"@fontsource/fira-code": "^5.2.6",
"@fontsource/inter": "^5.2.6",
"@types/json-bigint": "^1.0.4",
"ace-builds": "^1.41.0",
"ace-builds": "^1.43.1",
"brace": "^0.11.1",
"classnames": "^2.2.5",
"csstype": "^3.1.3",
@@ -51,13 +51,14 @@
"react-js-cron": "^5.2.0",
"react-draggable": "^4.4.6",
"react-resize-detector": "^7.1.2",
"react-syntax-highlighter": "^15.4.5",
"react-ultimate-pagination": "^1.3.2",
"react-error-boundary": "^5.0.0",
"react-error-boundary": "^6.0.0",
"react-markdown": "^8.0.7",
"regenerator-runtime": "^0.14.1",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^3.0.1",
"remark-gfm": "^4.0.1",
"reselect": "^4.0.0",
"rison": "^0.1.1",
"seedrandom": "^3.0.5",
@@ -65,15 +66,16 @@
"xss": "^1.0.14"
},
"devDependencies": {
"@emotion/styled": "^11.3.0",
"@emotion/styled": "^11.14.1",
"@types/d3-format": "^3.0.4",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-scale": "^2.1.1",
"@types/d3-time": "^3.0.4",
"@types/d3-time-format": "^4.0.3",
"@types/react-table": "^7.7.20",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/jquery": "^3.5.8",
"@types/lodash": "^4.17.16",
"@types/lodash": "^4.17.20",
"@types/math-expression-evaluator": "^1.3.3",
"@types/node": "^22.10.3",
"@types/prop-types": "^15.7.2",
@@ -88,7 +90,7 @@
"antd": "^5.24.6",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@emotion/styled": "^11.14.1",
"@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",

View File

@@ -55,7 +55,11 @@ export enum AppSection {
Embedded = 'EMBEDDED',
}
export type FilterState = { value?: any; [key: string]: any };
export type FilterState = {
value?: any;
customColumnLabel?: string;
[key: string]: any;
};
export type DataMask = {
extraFormData?: ExtraFormData;

View File

@@ -31,6 +31,7 @@ export enum VizType {
Compare = 'compare',
CountryMap = 'country_map',
Funnel = 'funnel',
Gantt = 'gantt_chart',
Gauge = 'gauge_chart',
Graph = 'graph_chart',
Handlebars = 'handlebars',
@@ -54,6 +55,7 @@ export enum VizType {
Step = 'echarts_timeseries_step',
Sunburst = 'sunburst_v2',
Table = 'table',
TableAgGrid = 'ag-grid-table',
TimePivot = 'time_pivot',
TimeTable = 'time_table',
Timeseries = 'echarts_timeseries',

View File

@@ -27,6 +27,8 @@ import getLabelsColorMap, {
import { getAnalogousColors } from './utils';
import { FeatureFlag, isFeatureEnabled } from '../utils';
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
// Use type augmentation to correct the fact that
// an instance of CategoricalScale is also a function
interface CategoricalColorScale {

View File

@@ -165,7 +165,7 @@ export default {
defaultValue: { summary: 'false' },
},
},
dropdownRender: {
popupRender: {
control: false,
description:
'Custom render function for dropdown content. `(menus: ReactNode) => ReactNode`',

View File

@@ -0,0 +1,320 @@
/**
* 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 { Typography, Flex, Space } from '@superset-ui/core/components';
import CodeSyntaxHighlighter from '.';
import type { CodeSyntaxHighlighterProps, SupportedLanguage } from '.';
const { Title, Text, Paragraph } = Typography;
const languages: SupportedLanguage[] = ['sql', 'json', 'htmlbars', 'markdown'];
// Sample code for each language
const sampleCode = {
sql: `-- Complex SQL Query Example
SELECT
u.id,
u.username,
u.email,
COUNT(o.id) as total_orders,
SUM(o.amount) as total_spent,
AVG(o.amount) as avg_order_value
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at >= '2023-01-01'
AND u.status = 'active'
GROUP BY u.id, u.username, u.email
HAVING COUNT(o.id) > 0
ORDER BY total_spent DESC, total_orders DESC
LIMIT 50;`,
json: `{
"user": {
"id": 12345,
"username": "john_doe",
"email": "john@example.com",
"profile": {
"firstName": "John",
"lastName": "Doe",
"age": 30,
"preferences": {
"theme": "dark",
"language": "en",
"notifications": true
}
},
"orders": [
{
"id": "order_001",
"amount": 99.99,
"status": "completed",
"items": ["laptop", "mouse"]
},
{
"id": "order_002",
"amount": 49.99,
"status": "pending",
"items": ["keyboard"]
}
]
}
}`,
htmlbars: `{{!-- Handlebars Template Example --}}
<div class="user-profile">
<h1>Welcome, {{user.firstName}} {{user.lastName}}!</h1>
{{#if user.orders}}
<div class="orders-section">
<h2>Your Orders ({{user.orders.length}})</h2>
{{#each user.orders}}
<div class="order-card {{status}}">
<h3>Order #{{id}}</h3>
<p class="amount">\${{amount}}</p>
<p class="status">Status: {{capitalize status}}</p>
{{#if items}}
<ul class="items">
{{#each items}}
<li>{{this}}</li>
{{/each}}
</ul>
{{/if}}
</div>
{{/each}}
</div>
{{else}}
<p class="no-orders">No orders found.</p>
{{/if}}
</div>`,
markdown: `# CodeSyntaxHighlighter Component
A **themed syntax highlighter** for Superset that supports multiple languages and automatic theme switching.
## Features
- 🎨 **Automatic theming** - Adapts to light/dark modes
- ⚡ **Lazy loading** - Languages load on-demand for better performance
- 🔧 **TypeScript support** - Full type safety
- 📱 **Responsive** - Works on all screen sizes
## Supported Languages
| Language | Extension | Use Case |
|----------|-----------|----------|
| SQL | \`.sql\` | Database queries |
| JSON | \`.json\` | Data interchange |
| HTML/Handlebars | \`.hbs\` | Templates |
| Markdown | \`.md\` | Documentation |
## Usage
\`\`\`typescript
import CodeSyntaxHighlighter from '@superset-ui/core/components/CodeSyntaxHighlighter';
<CodeSyntaxHighlighter language="sql">
SELECT * FROM users WHERE active = true;
</CodeSyntaxHighlighter>
\`\`\`
> **Note**: Languages are loaded lazily for optimal performance!`,
};
export default {
title: 'Components/CodeSyntaxHighlighter',
component: CodeSyntaxHighlighter,
parameters: {
docs: {
description: {
component:
"A themed syntax highlighter component that automatically adapts to Superset's light/dark themes and supports lazy loading of languages.",
},
},
},
};
// Gallery showing all supported languages
export const LanguageGallery = () => (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{languages.map(language => (
<div key={language}>
<Title
level={3}
style={{ textTransform: 'capitalize', marginBottom: 16 }}
>
{language.toUpperCase()} Example
</Title>
<CodeSyntaxHighlighter language={language}>
{sampleCode[language]}
</CodeSyntaxHighlighter>
</div>
))}
</Space>
);
// Interactive playground
export const InteractivePlayground = (args: CodeSyntaxHighlighterProps) => (
<CodeSyntaxHighlighter {...args}>
{args.children || sampleCode[args.language || 'sql']}
</CodeSyntaxHighlighter>
);
InteractivePlayground.args = {
language: 'sql',
showLineNumbers: false,
wrapLines: true,
children: sampleCode.sql,
};
InteractivePlayground.argTypes = {
language: {
control: { type: 'select' },
options: languages,
description: 'Programming language for syntax highlighting',
},
showLineNumbers: {
control: { type: 'boolean' },
description: 'Display line numbers alongside the code',
},
wrapLines: {
control: { type: 'boolean' },
description: 'Wrap long lines instead of showing horizontal scroll',
},
children: {
control: { type: 'text' },
description: 'Code content to highlight',
},
customStyle: {
control: { type: 'object' },
description: 'Custom CSS styles to apply to the syntax highlighter',
},
};
// Showcase different styling options
export const StylingExamples = () => (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{/* Default styling */}
<div>
<Title level={3}>Default Styling</Title>
<CodeSyntaxHighlighter language="sql">
SELECT id, name FROM users WHERE active = true;
</CodeSyntaxHighlighter>
</div>
{/* With line numbers */}
<div>
<Title level={3}>With Line Numbers</Title>
<CodeSyntaxHighlighter language="sql" showLineNumbers>
{sampleCode.sql}
</CodeSyntaxHighlighter>
</div>
{/* Custom styling */}
<div>
<Title level={3}>Custom Styling (Compact)</Title>
<CodeSyntaxHighlighter
language="json"
customStyle={{
fontSize: '12px',
padding: '12px',
maxHeight: '200px',
overflow: 'auto',
}}
>
{sampleCode.json}
</CodeSyntaxHighlighter>
</div>
{/* No line wrapping */}
<div>
<Title level={3}>No Line Wrapping</Title>
<CodeSyntaxHighlighter
language="sql"
wrapLines={false}
customStyle={{ maxWidth: '400px' }}
>
{`SELECT very_long_column_name, another_very_long_column_name, yet_another_extremely_long_column_name FROM very_long_table_name WHERE condition = 'this is a very long condition';`}
</CodeSyntaxHighlighter>
</div>
</Space>
);
// Performance and edge cases
export const EdgeCases = () => (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{/* Very long single line */}
<div>
<Title level={3}>Very Long Single Line</Title>
<CodeSyntaxHighlighter language="sql">
{`SELECT ${'very_long_column_name, '.repeat(20)}id FROM users;`}
</CodeSyntaxHighlighter>
</div>
{/* Special characters */}
<div>
<Title level={3}>Special Characters & Escaping</Title>
<CodeSyntaxHighlighter language="sql">
{`SELECT * FROM "table-name" WHERE field = 'O\\'Brien' AND data = '{"key": "value"}';`}
</CodeSyntaxHighlighter>
</div>
{/* Multiple languages showcase */}
<div>
<Title level={3}>Quick Language Comparison</Title>
<Flex gap="middle">
<div style={{ flex: 1 }}>
<Title level={4}>SQL</Title>
<CodeSyntaxHighlighter language="sql">
SELECT id, name FROM users;
</CodeSyntaxHighlighter>
</div>
<div style={{ flex: 1 }}>
<Title level={4}>JSON</Title>
<CodeSyntaxHighlighter language="json">
{`{"users": [{"id": 1, "name": "John"}]}`}
</CodeSyntaxHighlighter>
</div>
</Flex>
</div>
</Space>
);
// Theme testing helper
export const ThemeShowcase = () => (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Paragraph>
<Text strong>Theme Testing:</Text> Switch between light and dark themes in
Storybook to see automatic adaptation.
</Paragraph>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{languages.map(language => (
<div key={language}>
<Title level={4} style={{ textTransform: 'uppercase' }}>
{language}
</Title>
<CodeSyntaxHighlighter language={language}>
{sampleCode[language].split('\n').slice(0, 5).join('\n')}
</CodeSyntaxHighlighter>
</div>
))}
</Space>
</Space>
);

View File

@@ -0,0 +1,156 @@
/**
* 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 } from '../../spec';
import CodeSyntaxHighlighter from './index';
// Simple mock that just returns the content
jest.mock(
'react-syntax-highlighter/dist/cjs/light',
() =>
function MockSyntaxHighlighter({ children, ...props }: any) {
return (
<pre data-testid="syntax-highlighter" data-language={props.language}>
{children}
</pre>
);
},
);
// Mock the language modules
jest.mock(
'react-syntax-highlighter/dist/cjs/languages/hljs/sql',
() => 'sql-mock',
);
jest.mock(
'react-syntax-highlighter/dist/cjs/languages/hljs/json',
() => 'json-mock',
);
jest.mock(
'react-syntax-highlighter/dist/cjs/languages/hljs/htmlbars',
() => 'html-mock',
);
jest.mock(
'react-syntax-highlighter/dist/cjs/languages/hljs/markdown',
() => 'md-mock',
);
// Mock the styles
jest.mock('react-syntax-highlighter/dist/cjs/styles/hljs/github', () => ({}));
jest.mock(
'react-syntax-highlighter/dist/cjs/styles/hljs/atom-one-dark',
() => ({}),
);
describe('CodeSyntaxHighlighter', () => {
it('renders code content', () => {
render(<CodeSyntaxHighlighter>SELECT * FROM users;</CodeSyntaxHighlighter>);
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
});
it('renders with default SQL language', () => {
render(<CodeSyntaxHighlighter>SELECT * FROM users;</CodeSyntaxHighlighter>);
// Should show content (the important thing is content is visible)
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
});
it('renders with specified language', () => {
render(
<CodeSyntaxHighlighter language="json">
{`{ "key": "value" }`}
</CodeSyntaxHighlighter>,
);
// Should show content regardless of which element renders it
expect(screen.getByText('{ "key": "value" }')).toBeInTheDocument();
});
it('supports all expected languages', () => {
const languages = ['sql', 'json', 'htmlbars', 'markdown'] as const;
languages.forEach(language => {
const { unmount } = render(
<CodeSyntaxHighlighter language={language}>
{`Test content for ${language}`}
</CodeSyntaxHighlighter>,
);
// Should render the content (either in fallback or syntax highlighter)
expect(
screen.getByText(`Test content for ${language}`),
).toBeInTheDocument();
unmount();
});
});
it('renders fallback pre element initially', () => {
render(
<CodeSyntaxHighlighter language="sql">
SELECT COUNT(*) FROM table;
</CodeSyntaxHighlighter>,
);
// Should render the content in some form
expect(screen.getByText('SELECT COUNT(*) FROM table;')).toBeInTheDocument();
});
it('handles special characters', () => {
const specialContent = "SELECT * FROM `users` WHERE name = 'O\\'Brien';";
render(
<CodeSyntaxHighlighter language="sql">
{specialContent}
</CodeSyntaxHighlighter>,
);
expect(screen.getByText(specialContent)).toBeInTheDocument();
});
it('accepts custom styles', () => {
render(
<CodeSyntaxHighlighter language="sql" customStyle={{ fontSize: '16px' }}>
SELECT * FROM users;
</CodeSyntaxHighlighter>,
);
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
});
it('accepts showLineNumbers prop', () => {
render(
<CodeSyntaxHighlighter language="sql" showLineNumbers>
SELECT * FROM users;
</CodeSyntaxHighlighter>,
);
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
});
it('accepts wrapLines prop', () => {
render(
<CodeSyntaxHighlighter language="sql" wrapLines={false}>
SELECT * FROM users;
</CodeSyntaxHighlighter>,
);
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,149 @@
/**
* 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 { useEffect, useState } from 'react';
import SyntaxHighlighterBase from 'react-syntax-highlighter/dist/cjs/light';
import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github';
import tomorrow from 'react-syntax-highlighter/dist/cjs/styles/hljs/tomorrow-night';
import { themeObject } from '@superset-ui/core';
export type SupportedLanguage = 'sql' | 'htmlbars' | 'markdown' | 'json';
export interface CodeSyntaxHighlighterProps {
children: string;
language?: SupportedLanguage;
customStyle?: React.CSSProperties;
showLineNumbers?: boolean;
wrapLines?: boolean;
style?: any; // Override theme style if needed
}
// Track which languages have been registered to avoid duplicate registrations
const registeredLanguages = new Set<SupportedLanguage>();
// Language import functions - these will be called lazily
const languageImporters = {
sql: () => import('react-syntax-highlighter/dist/cjs/languages/hljs/sql'),
htmlbars: () =>
import('react-syntax-highlighter/dist/cjs/languages/hljs/htmlbars'),
markdown: () =>
import('react-syntax-highlighter/dist/cjs/languages/hljs/markdown'),
json: () => import('react-syntax-highlighter/dist/cjs/languages/hljs/json'),
};
/**
* Lazily register a language for syntax highlighting
*/
const registerLanguage = async (language: SupportedLanguage): Promise<void> => {
if (registeredLanguages.has(language)) {
return; // Already registered
}
try {
const languageModule = await languageImporters[language]();
SyntaxHighlighterBase.registerLanguage(language, languageModule.default);
registeredLanguages.add(language);
} catch (error) {
console.warn(`Failed to load language ${language}:`, error);
}
};
/**
* A themed syntax highlighter component that automatically adapts to Superset's current theme.
* Supports light/dark mode switching and provides consistent styling across the application.
* Languages are loaded lazily to improve initial page load performance.
* Uses ultra-neutral themes for professional, consistent appearance.
*/
export const CodeSyntaxHighlighter: React.FC<CodeSyntaxHighlighterProps> = ({
children,
language = 'sql',
customStyle = {},
showLineNumbers = false,
wrapLines = true,
style: overrideStyle,
}) => {
const [isLanguageReady, setIsLanguageReady] = useState(
registeredLanguages.has(language),
);
useEffect(() => {
const loadLanguage = async () => {
if (!registeredLanguages.has(language)) {
await registerLanguage(language);
setIsLanguageReady(true);
}
};
loadLanguage();
}, [language]);
const isDark = themeObject.isThemeDark();
const themeStyle = overrideStyle || (isDark ? tomorrow : github);
const defaultCustomStyle: React.CSSProperties = {
background: themeObject.theme.colorBgElevated,
padding: themeObject.theme.sizeUnit * 4,
border: 0,
borderRadius: themeObject.theme.borderRadius,
...customStyle,
};
// Show a simple pre-formatted text while language is loading
if (!isLanguageReady) {
return (
<pre
style={{
...defaultCustomStyle,
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
margin: 0,
}}
>
{children}
</pre>
);
}
return (
<SyntaxHighlighterBase
language={language}
style={themeStyle}
customStyle={defaultCustomStyle}
showLineNumbers={showLineNumbers}
wrapLines={wrapLines}
>
{children}
</SyntaxHighlighterBase>
);
};
/**
* Utility function to preload specific languages if needed
* This can be called strategically in components that know they'll need certain languages
*/
export const preloadLanguages = async (
languages: SupportedLanguage[],
): Promise<void> => {
const promises = languages
.filter(lang => !registeredLanguages.has(lang))
.map(registerLanguage);
await Promise.all(promises);
};
export default CodeSyntaxHighlighter;

View File

@@ -38,7 +38,7 @@ const StyledCollapse = styled((props: CollapseProps) => (
${({ expandIconPosition }) =>
expandIconPosition &&
expandIconPosition === 'right' &&
expandIconPosition === 'end' &&
`
.anticon.anticon-right.ant-collapse-arrow > svg {
transform: rotate(90deg) !important;
@@ -72,7 +72,7 @@ const StyledCollapse = styled((props: CollapseProps) => (
.ant-collapse-header {
${({ expandIconPosition }) =>
expandIconPosition &&
expandIconPosition === 'right' &&
expandIconPosition === 'end' &&
`
.anticon.anticon-right.ant-collapse-arrow > svg {
transform: rotate(-90deg) !important;

View File

@@ -62,12 +62,12 @@ test('Calling "onHide"', async () => {
expect(props.onConfirm).toHaveBeenCalledTimes(0);
// type "del" in the input
await userEvent.type(screen.getByTestId('delete-modal-input'), 'del');
userEvent.type(screen.getByTestId('delete-modal-input'), 'del');
expect(screen.getByTestId('delete-modal-input')).toHaveValue('del');
// close the modal
expect(screen.getByText('×')).toBeInTheDocument();
await userEvent.click(screen.getByText('×'));
expect(screen.getByTestId('close-modal-btn')).toBeInTheDocument();
userEvent.click(screen.getByTestId('close-modal-btn'));
expect(props.onHide).toHaveBeenCalledTimes(1);
expect(props.onConfirm).toHaveBeenCalledTimes(0);

View File

@@ -89,7 +89,7 @@ export const MenuDotsDropdown = ({
iconOrientation = IconOrientation.Vertical,
...rest
}: MenuDotsDropdownProps) => (
<AntdDropdown dropdownRender={() => overlay} {...rest}>
<AntdDropdown popupRender={() => overlay} {...rest}>
<MenuDotsWrapper data-test="dropdown-trigger">
{RenderIcon(iconOrientation)}
</MenuDotsWrapper>

View File

@@ -23,7 +23,7 @@ import { Tooltip } from '../Tooltip';
import type { DropdownButtonProps } from './types';
export const DropdownButton = ({
dropdownRender,
popupRender,
tooltip,
tooltipPlacement,
children,
@@ -51,7 +51,7 @@ export const DropdownButton = ({
`;
const button = (
<Dropdown.Button
dropdownRender={dropdownRender}
popupRender={popupRender}
{...rest}
css={[
defaultBtnCss,

View File

@@ -17,8 +17,10 @@
* under the License.
*/
import {
CSSProperties,
cloneElement,
forwardRef,
ReactElement,
RefObject,
useEffect,
useImperativeHandle,
@@ -26,17 +28,85 @@ import {
useMemo,
useState,
useRef,
ReactNode,
useCallback,
} from 'react';
import { Global } from '@emotion/react';
import { css, t, useTheme, usePrevious } from '@superset-ui/core';
import { useResizeDetector } from 'react-resize-detector';
import { Badge, Icons, Button, Tooltip, Popover } from '..';
import type {
DropdownContainerProps,
DropdownItem,
DropdownRef,
} from './types';
/**
* Container item.
*/
export interface DropdownItem {
/**
* String that uniquely identifies the item.
*/
id: string;
/**
* The element to be rendered.
*/
element: ReactElement;
}
/**
* Horizontal container that displays overflowed items in a dropdown.
* It shows an indicator of how many items are currently overflowing.
*/
export interface DropdownContainerProps {
/**
* Array of items. The id property is used to uniquely identify
* the elements when rendering or dealing with event handlers.
*/
items: DropdownItem[];
/**
* Event handler called every time an element moves between
* main container and dropdown.
*/
onOverflowingStateChange?: (overflowingState: {
notOverflowed: string[];
overflowed: string[];
}) => void;
/**
* Option to customize the content of the dropdown.
*/
dropdownContent?: (overflowedItems: DropdownItem[]) => ReactElement;
/**
* Dropdown ref.
*/
dropdownRef?: RefObject<HTMLDivElement>;
/**
* Dropdown additional style properties.
*/
dropdownStyle?: CSSProperties;
/**
* Displayed count in the dropdown trigger.
*/
dropdownTriggerCount?: number;
/**
* Icon of the dropdown trigger.
*/
dropdownTriggerIcon?: ReactElement;
/**
* Text of the dropdown trigger.
*/
dropdownTriggerText?: string;
/**
* Text of the dropdown trigger tooltip
*/
dropdownTriggerTooltip?: ReactNode | null;
/**
* Main container additional style properties.
*/
style?: CSSProperties;
/**
* Force render popover content before it's first opened
*/
forceRender?: boolean;
}
export type DropdownRef = HTMLDivElement & { open: () => void };
const MAX_HEIGHT = 500;
@@ -73,6 +143,37 @@ export const DropdownContainer = forwardRef(
const [showOverflow, setShowOverflow] = useState(false);
// callback to update item widths so that the useLayoutEffect runs whenever
// width of any of the child changes
const recalculateItemWidths = useCallback(() => {
const mainItemsContainerNode = current?.children.item(0);
if (mainItemsContainerNode) {
const visibleChildrenElements = Array.from(
mainItemsContainerNode.children,
);
setItemsWidth(prevGlobalWidths => {
if (prevGlobalWidths.length !== items.length) {
return prevGlobalWidths;
}
const newGlobalWidths = [...prevGlobalWidths];
let changed = false;
visibleChildrenElements.forEach((child, indexInVisible) => {
const originalItemIndex = indexInVisible;
if (originalItemIndex < newGlobalWidths.length) {
const newWidth = child.getBoundingClientRect().width;
if (newGlobalWidths[originalItemIndex] !== newWidth) {
newGlobalWidths[originalItemIndex] = newWidth;
changed = true;
}
}
});
return changed ? newGlobalWidths : prevGlobalWidths;
});
}
}, [current?.children, items.length]);
const reduceItems = (items: DropdownItem[]): [DropdownItem[], string[]] =>
items.reduce(
([items, ids], item) => {
@@ -122,24 +223,7 @@ export const DropdownContainer = forwardRef(
childrenArray.map(child => resizeObserver.unobserve(child));
resizeObserver.disconnect();
};
}, [items.length]);
// callback to update item widths so that the useLayoutEffect runs whenever
// width of any of the child changes
const recalculateItemWidths = () => {
const container = current?.children.item(0);
if (container) {
const { children } = container;
const childrenArray = Array.from(children);
const currentWidths = childrenArray.map(
child => child.getBoundingClientRect().width,
);
// Update state with new widths
setItemsWidth(currentWidths);
}
};
}, [items.length, current, recalculateItemWidths]);
useLayoutEffect(() => {
if (popoverVisible) {
@@ -206,6 +290,7 @@ export const DropdownContainer = forwardRef(
overflowedItems.length,
previousWidth,
width,
popoverVisible,
]);
useEffect(() => {
@@ -376,5 +461,3 @@ export const DropdownContainer = forwardRef(
);
},
);
export type { DropdownItem, DropdownRef };

View File

@@ -64,7 +64,7 @@ export const FaveStar = ({
<Icons.StarFilled
aria-label="starred"
iconSize="l"
iconColor={theme.colors.warning.base}
iconColor={theme.colorWarning}
name="favorite-selected"
/>
) : (

View File

@@ -25,7 +25,7 @@ import { BaseIconComponent } from './BaseIcon';
const AsyncIcon = (props: IconType) => {
const [, setLoaded] = useState(false);
const ImportedSVG = useRef<FC<SVGProps<SVGSVGElement>>>();
const { fileName } = props;
const { fileName, ...restProps } = props;
useEffect(() => {
let cancelled = false;
@@ -46,7 +46,7 @@ const AsyncIcon = (props: IconType) => {
return (
<BaseIconComponent
component={ImportedSVG.current || TransparentIcon}
{...props}
{...restProps}
/>
);
};

View File

@@ -43,11 +43,12 @@ export const BaseIconComponent: React.FC<
iconSize,
viewBox,
customIcons,
fileName,
...rest
}) => {
const theme = useTheme();
const whatRole = rest?.onClick ? 'button' : 'img';
const ariaLabel = genAriaLabel(rest.fileName || '');
const ariaLabel = genAriaLabel(fileName || '');
const style = {
color: iconColor,
fontSize: iconSize

View File

@@ -37,7 +37,7 @@ export const DatasetTypeLabel: React.FC<DatasetTypeLabelProps> = ({
datasetType === 'physical' ? (
<Icons.InsertRowAboveOutlined
iconSize={SIZE}
iconColor={theme.colors.primary.dark1}
iconColor={theme.colorPrimary}
/>
) : (
<Icons.ConsoleSqlOutlined iconSize={SIZE} />

View File

@@ -18,7 +18,7 @@
*/
import { isValidElement, cloneElement, useMemo, useRef, useState } from 'react';
import { isNil } from 'lodash';
import { css, styled, t } from '@superset-ui/core';
import { css, styled, t, useTheme } from '@superset-ui/core';
import { Modal as AntdModal, ModalProps as AntdModalProps } from 'antd';
import { Resizable } from 're-resizable';
import Draggable, {
@@ -26,6 +26,7 @@ import Draggable, {
DraggableData,
DraggableEvent,
} from 'react-draggable';
import { Icons } from '../Icons';
import { Button } from '../Button';
import type { ModalProps, StyledModalProps } from './types';
@@ -45,8 +46,16 @@ export const BaseModal = (props: AntdModalProps) => (
);
export const StyledModal = styled(BaseModal)<StyledModalProps>`
${({ theme, responsive, maxWidth }) =>
responsive &&
${({
theme,
responsive,
maxWidth,
resizable,
height,
draggable,
hideFooter,
}) => css`
${responsive &&
css`
max-width: ${maxWidth ?? '900px'};
padding-left: ${theme.sizeUnit * 3}px;
@@ -55,120 +64,120 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
top: 0;
`}
.ant-modal-content {
background-color: ${({ theme }) => theme.colorBgContainer};
display: flex;
flex-direction: column;
max-height: ${({ theme }) => `calc(100vh - ${theme.sizeUnit * 8}px)`};
margin-bottom: ${({ theme }) => theme.sizeUnit * 4}px;
margin-top: ${({ theme }) => theme.sizeUnit * 4}px;
padding: 0;
}
.ant-modal-header {
flex: 0 0 auto;
border-radius: ${({ theme }) => theme.borderRadius}px
${({ theme }) => theme.borderRadius}px 0 0;
padding: ${({ theme }) => theme.sizeUnit * 4}px
${({ theme }) => theme.sizeUnit * 6}px;
.ant-modal-title {
font-weight: ${({ theme }) => theme.fontWeightStrong};
}
.ant-modal-title h4 {
.ant-modal-content {
background-color: ${theme.colorBgContainer};
display: flex;
margin: 0;
align-items: center;
}
}
.ant-modal-close {
width: ${({ theme }) => theme.sizeUnit * 14}px;
height: ${({ theme }) => theme.sizeUnit * 14}px;
top: 0;
right: 0;
}
.ant-modal-close:hover {
background: transparent;
}
.ant-modal-close-x {
display: flex;
align-items: center;
.close {
flex: 1 1 auto;
margin-bottom: ${({ theme }) => theme.sizeUnit}px;
color: ${({ theme }) => theme.colorPrimaryText};
font-size: 32px;
font-weight: ${({ theme }) => theme.fontWeightLight};
}
}
.ant-modal-body {
flex: 0 1 auto;
padding: ${({ theme }) => theme.sizeUnit * 4}px;
overflow: auto;
${({ resizable, height }) => !resizable && height && `height: ${height};`}
}
.ant-modal-footer {
flex: 0 0 1;
border-top: ${({ theme }) => theme.sizeUnit / 4}px solid
${({ theme }) => theme.colorSplit};
padding: ${({ theme }) => theme.sizeUnit * 4}px;
margin-top: 0;
.btn {
font-size: 12px;
}
.btn + .btn {
margin-left: ${({ theme }) => theme.sizeUnit * 2}px;
}
}
&.no-content-padding .ant-modal-body {
padding: 0;
}
${({ draggable, theme }) =>
draggable &&
`
.ant-modal-header {
flex-direction: column;
max-height: calc(100vh - ${theme.sizeUnit * 8}px);
margin-bottom: ${theme.sizeUnit * 4}px;
margin-top: ${theme.sizeUnit * 4}px;
padding: 0;
.draggable-trigger {
}
.ant-modal-header {
flex: 0 0 auto;
border-radius: ${theme.borderRadius}px ${theme.borderRadius}px 0 0;
padding: ${theme.sizeUnit * 4}px ${theme.sizeUnit * 6}px;
.ant-modal-title {
font-weight: ${theme.fontWeightStrong};
}
.ant-modal-title h4 {
display: flex;
margin: 0;
align-items: center;
}
}
.ant-modal-close {
width: ${theme.sizeUnit * 14}px;
height: ${theme.sizeUnit * 14}px;
padding: ${theme.sizeUnit * 6}px ${theme.sizeUnit * 4}px
${theme.sizeUnit * 4}px;
top: 0;
right: 0;
display: flex;
justify-content: center;
}
.ant-modal-close:hover {
background: transparent;
}
.ant-modal-close-x {
display: flex;
align-items: center;
[data-test='close-modal-btn'] {
justify-content: center;
}
.close {
flex: 1 1 auto;
margin-bottom: ${theme.sizeUnit}px;
color: ${theme.colorPrimaryText};
font-weight: ${theme.fontWeightLight};
}
}
.ant-modal-body {
flex: 0 1 auto;
padding: ${theme.sizeUnit * 4}px;
overflow: auto;
${!resizable && height && `height: ${height};`}
}
.ant-modal-footer {
flex: 0 0 1;
border-top: ${theme.sizeUnit / 4}px solid ${theme.colorSplit};
padding: ${theme.sizeUnit * 4}px;
margin-top: 0;
.btn {
font-size: 12px;
}
.btn + .btn {
margin-left: ${theme.sizeUnit * 2}px;
}
}
&.no-content-padding .ant-modal-body {
padding: 0;
}
${draggable &&
css`
.ant-modal-header {
padding: 0;
.draggable-trigger {
cursor: move;
padding: ${theme.sizeUnit * 4}px;
width: 100%;
}
}
`};
${({ resizable, hideFooter }) =>
resizable &&
`
.resizable {
pointer-events: all;
.resizable-wrapper {
height: 100%;
}
`}
.ant-modal-content {
height: 100%;
${resizable &&
css`
.resizable {
pointer-events: all;
.ant-modal-body {
/* 100% - header height - footer height */
height: ${
hideFooter
? `calc(100% - ${MODAL_HEADER_HEIGHT}px);`
: `calc(100% - ${MODAL_HEADER_HEIGHT}px - ${MODAL_FOOTER_HEIGHT}px);`
.resizable-wrapper {
height: 100%;
}
.ant-modal-content {
height: 100%;
.ant-modal-body {
height: ${hideFooter
? `calc(100% - ${MODAL_HEADER_HEIGHT}px)`
: `calc(100% - ${MODAL_HEADER_HEIGHT}px - ${MODAL_FOOTER_HEIGHT}px)`};
}
}
}
}
`}
`}
`;
@@ -214,13 +223,14 @@ const CustomModal = ({
resizable = false,
resizableConfig = defaultResizableConfig(hideFooter),
draggableConfig,
destroyOnClose,
destroyOnHidden,
openerRef,
...rest
}: ModalProps) => {
const draggableRef = useRef<HTMLDivElement>(null);
const [bounds, setBounds] = useState<DraggableBounds>();
const [dragDisabled, setDragDisabled] = useState<boolean>(true);
const theme = useTheme();
const handleOnHide = () => {
openerRef?.current?.focus();
@@ -228,13 +238,16 @@ const CustomModal = ({
};
let FooterComponent;
if (isValidElement(footer)) {
// This safely avoids injecting "closeModal" into native elements like <div> or <span>
if (isValidElement(footer) && typeof footer.type === 'function')
// If a footer component is provided inject a closeModal function
// so the footer can provide a "close" button if desired
FooterComponent = cloneElement(footer, {
closeModal: handleOnHide,
} as Partial<unknown>);
}
else FooterComponent = footer;
const modalFooter = isNil(FooterComponent)
? [
<Button
@@ -309,9 +322,13 @@ const CustomModal = ({
open={show}
title={<ModalTitle />}
closeIcon={
<span className="close" aria-hidden="true">
×
</span>
<Icons.CloseOutlined
iconColor={theme.colorText}
iconSize="l"
data-test="close-modal-btn"
className="close"
aria-hidden="true"
/>
}
footer={!hideFooter ? modalFooter : null}
hideFooter={hideFooter}
@@ -341,7 +358,7 @@ const CustomModal = ({
mask={shouldShowMask}
draggable={draggable}
resizable={resizable}
destroyOnClose={destroyOnClose}
destroyOnHidden={destroyOnHidden}
{...rest}
>
{children}

View File

@@ -34,7 +34,7 @@ export interface ModalProps {
show: boolean;
name?: string;
title: ReactNode;
width?: string;
width?: string | number;
maxWidth?: string;
responsive?: boolean;
hideFooter?: boolean;
@@ -47,7 +47,7 @@ export interface ModalProps {
resizableConfig?: ResizableProps;
draggable?: boolean;
draggableConfig?: DraggableProps;
destroyOnClose?: boolean;
destroyOnHidden?: boolean;
maskClosable?: boolean;
zIndex?: number;
bodyStyle?: CSSProperties;

View File

@@ -39,7 +39,7 @@ export interface ModalTriggerProps {
resizableConfig?: any;
draggable?: boolean;
draggableConfig?: any;
destroyOnClose?: boolean;
destroyOnHidden?: boolean;
}
export interface ModalTriggerRef {
@@ -63,7 +63,7 @@ export const ModalTrigger = forwardRef(
tooltip,
modalFooter,
triggerNode,
destroyOnClose = true,
destroyOnHidden = true,
modalBody,
draggableConfig = {},
resizableConfig = {},
@@ -120,7 +120,7 @@ export const ModalTrigger = forwardRef(
resizableConfig={resizableConfig}
draggable={draggable}
draggableConfig={draggableConfig}
destroyOnClose={destroyOnClose}
destroyOnHidden={destroyOnHidden}
>
{modalBody}
</Modal>

View File

@@ -34,7 +34,7 @@ export const menuTriggerStyles = (theme: SupersetTheme) => css`
width: ${theme.sizeUnit * 8}px;
height: ${theme.sizeUnit * 8}px;
padding: 0;
border: 1px solid ${theme.colors.primary.dark2};
border: 1px solid ${theme.colorPrimary};
&.ant-btn > span.anticon {
line-height: 0;
@@ -151,7 +151,7 @@ export const PageHeaderWithActions = ({
{showMenuDropdown && (
<Dropdown
trigger={['click']}
dropdownRender={() => additionalActionsMenu}
popupRender={() => additionalActionsMenu}
{...menuDropdownProps}
>
<Button
@@ -163,7 +163,7 @@ export const PageHeaderWithActions = ({
data-test="actions-trigger"
>
<Icons.EllipsisOutlined
iconColor={theme.colors.primary.dark2}
iconColor={theme.colorPrimary}
iconSize="l"
/>
</Button>

View File

@@ -45,7 +45,7 @@ interface HandleSelectProps {
}
const menuItemStyles = (theme: any) => css`
&.antd5-menu-item {
&.ant-menu-item {
height: auto;
line-height: 1.4;

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import {
Radio as Antd5Radio,
Radio as AntRadio,
type CheckboxOptionType,
type RadioGroupProps,
} from 'antd';
@@ -40,19 +40,19 @@ const RadioGroup = ({
...props
}: RadioGroupWrapperProps) => {
const content = options.map((option: CheckboxOptionType) => (
<Antd5Radio key={option.value} value={option.value}>
<AntRadio key={option.value} value={option.value}>
{option.label}
</Antd5Radio>
</AntRadio>
));
return (
<Antd5Radio.Group {...props}>
<AntRadio.Group {...props}>
{spaceConfig ? <Space {...spaceConfig}>{content}</Space> : content}
</Antd5Radio.Group>
</AntRadio.Group>
);
};
export const Radio = Object.assign(Antd5Radio, {
export const Radio = Object.assign(AntRadio, {
GroupWrapper: RadioGroup,
Button: Antd5Radio.Button,
Button: AntRadio.Button,
});
export type {
RadioChangeEvent,

View File

@@ -129,7 +129,7 @@ const AsyncSelect = forwardRef(
onError,
onChange,
onClear,
onDropdownVisibleChange,
onOpenChange,
onDeselect,
onSearch,
onSelect,
@@ -441,12 +441,12 @@ const AsyncSelect = forwardRef(
}
}
if (onDropdownVisibleChange) {
onDropdownVisibleChange(isDropdownVisible);
if (onOpenChange) {
onOpenChange(isDropdownVisible);
}
};
const dropdownRender = (
const popupRender = (
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
) =>
dropDownRenderHelper(
@@ -605,7 +605,7 @@ const AsyncSelect = forwardRef(
}
data-test={ariaLabel || name}
autoClearSearchValue={autoClearSearchValue}
dropdownRender={dropdownRender}
popupRender={popupRender}
filterOption={handleFilterOption}
filterSort={sortComparatorWithSearch}
getPopupContainer={
@@ -618,7 +618,7 @@ const AsyncSelect = forwardRef(
notFoundContent={isLoading ? t('Loading...') : notFoundContent}
onBlur={handleOnBlur}
onDeselect={handleOnDeselect}
onDropdownVisibleChange={handleOnDropdownVisibleChange}
onOpenChange={handleOnDropdownVisibleChange}
// @ts-ignore
onPaste={onPaste}
onPopupScroll={handlePagination}

View File

@@ -17,6 +17,7 @@
* under the License.
*/
import { StoryObj } from '@storybook/react';
import { noop } from 'lodash';
import { SelectOptionsType, SelectProps } from './types';
import { Select } from '.';
@@ -93,23 +94,26 @@ export const InteractiveSelect: StoryObj = {
options,
optionsCount,
...args
}: SelectProps & { header: string; optionsCount: number }) => (
<div
style={{
width: DEFAULT_WIDTH,
}}
>
<Select
{...args}
options={
Array.isArray(options)
? generateOptions(options, optionsCount)
: options
}
mode="multiple"
/>
</div>
),
}: SelectProps & { header: string; optionsCount: number }) => {
noop(header);
return (
<div
style={{
width: DEFAULT_WIDTH,
}}
>
<Select
{...args}
options={
Array.isArray(options)
? generateOptions(options, optionsCount)
: options
}
mode="multiple"
/>
</div>
);
},
args: {
autoFocus: true,
allowNewOptions: false,

View File

@@ -104,7 +104,7 @@ const Select = forwardRef(
onBlur,
onChange,
onClear,
onDropdownVisibleChange,
onOpenChange,
onDeselect,
onSearch,
onSelect,
@@ -398,8 +398,8 @@ const Select = forwardRef(
if (!isDropdownVisible) {
setSelectOptions(initialOptionsSorted);
}
if (onDropdownVisibleChange) {
onDropdownVisibleChange(isDropdownVisible);
if (onOpenChange) {
onOpenChange(isDropdownVisible);
}
};
@@ -492,7 +492,7 @@ const Select = forwardRef(
],
);
const dropdownRender = (
const popupRender = (
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
) =>
dropDownRenderHelper(
@@ -694,7 +694,7 @@ const Select = forwardRef(
}
data-test={ariaLabel || name}
autoClearSearchValue={autoClearSearchValue}
dropdownRender={dropdownRender}
popupRender={popupRender}
filterOption={handleFilterOption}
filterSort={sortComparatorWithSearch}
getPopupContainer={
@@ -708,7 +708,7 @@ const Select = forwardRef(
notFoundContent={isLoading ? t('Loading...') : notFoundContent}
onBlur={handleOnBlur}
onDeselect={handleOnDeselect}
onDropdownVisibleChange={handleOnDropdownVisibleChange}
onOpenChange={handleOnDropdownVisibleChange}
// @ts-ignore
onPaste={onPaste}
onPopupScroll={undefined}

View File

@@ -45,7 +45,7 @@ export const StyledSelect = styled(Select, {
})<{ headerPosition?: string; oneLine?: boolean }>`
${({ theme, headerPosition, oneLine }) => `
.ant-select-item-option-active:not(.ant-select-item-option-disabled) {
outline: 2px solid ${theme.colors.primary.base};
outline: 2px solid ${theme.colorPrimary};
outline-offset: -2px;
}
flex: ${headerPosition === 'left' ? 1 : 0};

View File

@@ -62,7 +62,7 @@ export type AntdExposedProps = Pick<
| 'onBlur'
| 'onPopupScroll'
| 'onSearch'
| 'onDropdownVisibleChange'
| 'onOpenChange'
| 'optionRender'
| 'placeholder'
| 'showArrow'
@@ -95,7 +95,7 @@ export interface BaseSelectProps extends AntdExposedProps {
/**
* Renders the dropdown
*/
dropdownRender?: (
popupRender?: (
menu: ReactElement<any, string | JSXElementConstructor<any>>,
) => ReactElement<any, string | JSXElementConstructor<any>>;
/**

View File

@@ -19,28 +19,28 @@
import { Tooltip } from 'antd';
import { Dropdown, Icons } from '@superset-ui/core/components';
import { t } from '@superset-ui/core';
import { ThemeMode } from '../../theme/types';
import { ThemeAlgorithm, ThemeMode } from '../../theme/types';
export interface ThemeSelectProps {
changeThemeMode: (newMode: ThemeMode) => void;
setThemeMode: (newMode: ThemeMode) => void;
tooltipTitle?: string;
themeMode: ThemeMode;
}
const ThemeSelect: React.FC<ThemeSelectProps> = ({
changeThemeMode,
setThemeMode,
tooltipTitle = 'Select theme',
themeMode,
}) => {
const handleSelect = (mode: ThemeMode) => {
changeThemeMode(mode);
setThemeMode(mode);
};
const themeIconMap: Record<ThemeMode, React.ReactNode> = {
[ThemeMode.LIGHT]: <Icons.SunOutlined />,
[ThemeMode.DARK]: <Icons.MoonOutlined />,
const themeIconMap: Record<ThemeAlgorithm | ThemeMode, React.ReactNode> = {
[ThemeAlgorithm.DEFAULT]: <Icons.SunOutlined />,
[ThemeAlgorithm.DARK]: <Icons.MoonOutlined />,
[ThemeMode.SYSTEM]: <Icons.FormatPainterOutlined />,
[ThemeMode.COMPACT]: <Icons.CompressOutlined />,
[ThemeAlgorithm.COMPACT]: <Icons.CompressOutlined />,
};
return (
@@ -49,9 +49,9 @@ const ThemeSelect: React.FC<ThemeSelectProps> = ({
menu={{
items: [
{
key: ThemeMode.LIGHT,
key: ThemeMode.DEFAULT,
label: t('Light'),
onClick: () => handleSelect(ThemeMode.LIGHT),
onClick: () => handleSelect(ThemeMode.DEFAULT),
icon: <Icons.SunOutlined />,
},
{

View File

@@ -0,0 +1,47 @@
/**
* 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 { ReactElement } from 'react';
import { UnsavedChangesModal, type UnsavedChangesModalProps } from '.';
export default {
title: 'Components/UnsavedChangesModal',
component: UnsavedChangesModal,
};
export const InteractiveUnsavedChangesModal = (
props: UnsavedChangesModalProps,
): ReactElement => (
<UnsavedChangesModal {...props}>
If you don't save, changes will be lost.
</UnsavedChangesModal>
);
InteractiveUnsavedChangesModal.args = {
showModal: true,
onHide: () => {},
handleSave: () => {},
onConfirmNavigation: () => {},
title: 'Unsaved Changes',
};
InteractiveUnsavedChangesModal.argTypes = {
onHide: { action: 'onHide' },
handleSave: { action: 'handleSave' },
onConfirmNavigation: { action: 'onConfirmNavigation' },
};

View File

@@ -0,0 +1,96 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { render, screen, userEvent } from '@superset-ui/core/spec';
import { UnsavedChangesModal } from '.';
test('should render nothing if showModal is false', () => {
const { queryByRole } = render(
<UnsavedChangesModal
showModal={false}
onHide={() => {}}
handleSave={() => {}}
onConfirmNavigation={() => {}}
/>,
);
expect(queryByRole('dialog')).not.toBeInTheDocument();
});
test('should render the UnsavedChangesModal component if showModal is true', async () => {
const { queryByRole } = render(
<UnsavedChangesModal
showModal
onHide={() => {}}
handleSave={() => {}}
onConfirmNavigation={() => {}}
/>,
);
expect(queryByRole('dialog')).toBeInTheDocument();
});
test('should only call onConfirmNavigation when clicking the Discard button', async () => {
const mockOnHide = jest.fn();
const mockHandleSave = jest.fn();
const mockOnConfirmNavigation = jest.fn();
render(
<UnsavedChangesModal
showModal
onHide={mockOnHide}
handleSave={mockHandleSave}
onConfirmNavigation={mockOnConfirmNavigation}
/>,
);
const discardButton: HTMLElement = await screen.findByRole('button', {
name: /discard/i,
});
userEvent.click(discardButton);
expect(mockOnConfirmNavigation).toHaveBeenCalled();
expect(mockHandleSave).not.toHaveBeenCalled();
expect(mockOnHide).not.toHaveBeenCalled();
});
test('should only call handleSave when clicking the Save button', async () => {
const mockOnHide = jest.fn();
const mockHandleSave = jest.fn();
const mockOnConfirmNavigation = jest.fn();
render(
<UnsavedChangesModal
showModal
onHide={mockOnHide}
handleSave={mockHandleSave}
onConfirmNavigation={mockOnConfirmNavigation}
/>,
);
const saveButton: HTMLElement = await screen.findByRole('button', {
name: /save/i,
});
userEvent.click(saveButton);
expect(mockHandleSave).toHaveBeenCalled();
expect(mockOnHide).not.toHaveBeenCalled();
expect(mockOnConfirmNavigation).not.toHaveBeenCalled();
});

View File

@@ -0,0 +1,129 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { t, styled, css } from '@superset-ui/core';
import { Icons, Modal, Typography } from '@superset-ui/core/components';
import { Button } from '@superset-ui/core/components/Button';
import type { FC, ReactElement } from 'react';
const StyledModalTitle = styled(Typography.Title)`
&& {
font-weight: 600;
margin: 0;
}
`;
const StyledModalBody = styled(Typography.Text)`
${({ theme }) => css`
padding: 0 ${theme.sizeUnit * 2}px;
&& {
margin: 0;
}
`}
`;
const StyledDiscardBtn = styled(Button)`
${({ theme }) => css`
min-width: ${theme.sizeUnit * 22}px;
height: ${theme.sizeUnit * 8}px;
`}
`;
const StyledSaveBtn = styled(Button)`
${({ theme }) => css`
min-width: ${theme.sizeUnit * 17}px;
height: ${theme.sizeUnit * 8}px;
span > :first-of-type {
margin-right: 0;
}
`}
`;
const StyledWarningIcon = styled(Icons.WarningOutlined)`
${({ theme }) => css`
color: ${theme.colorWarning};
margin-right: ${theme.sizeUnit * 4}px;
`}
`;
export type UnsavedChangesModalProps = {
showModal: boolean;
onHide: () => void;
handleSave: () => void;
onConfirmNavigation: () => void;
title?: string;
body?: string;
};
export const UnsavedChangesModal: FC<UnsavedChangesModalProps> = ({
showModal,
onHide,
handleSave,
onConfirmNavigation,
title = 'Unsaved Changes',
body = "If you don't save, changes will be lost.",
}: UnsavedChangesModalProps): ReactElement => (
<Modal
centered
responsive
onHide={onHide}
show={showModal}
width="444px"
title={
<div
css={css`
align-items: center;
display: flex;
`}
>
<StyledWarningIcon iconSize="xl" />
<StyledModalTitle type="secondary" level={5}>
{title}
</StyledModalTitle>
</div>
}
footer={
<div
css={css`
display: flex;
justify-content: flex-end;
width: 100%;
`}
>
<StyledDiscardBtn
htmlType="button"
buttonSize="small"
onClick={onConfirmNavigation}
>
{t('Discard')}
</StyledDiscardBtn>
<StyledSaveBtn
htmlType="button"
buttonSize="small"
buttonStyle="primary"
onClick={handleSave}
>
{t('Save')}
</StyledSaveBtn>
</div>
}
>
<StyledModalBody type="secondary">{body}</StyledModalBody>
</Modal>
);

View File

@@ -163,5 +163,6 @@ export * from './Steps';
export * from './Table';
export * from './TableView';
export * from './Tag';
export * from './UnsavedChangesModal';
export * from './constants';
export * from './Result';

View File

@@ -21,6 +21,8 @@ import { ExtensibleFunction } from '../models';
import { getNumberFormatter, NumberFormats } from '../number-format';
import { Currency } from '../query';
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
interface CurrencyFormatterConfig {
d3Format?: string;
currency: Currency;

View File

@@ -40,7 +40,7 @@ export const buildCustomFormatters = (
const actualCurrencyFormat = currencyFormat?.symbol
? currencyFormat
: savedCurrencyFormats[metric];
return actualCurrencyFormat
return actualCurrencyFormat?.symbol
? {
...acc,
[metric]: new CurrencyFormatter({

View File

@@ -20,6 +20,8 @@ import { ExtensibleFunction } from '../models';
import { isRequired } from '../utils';
import { NumberFormatFunction } from './types';
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
export const PREVIEW_VALUE = 12345.432;
export interface NumberFormatterConfig {

View File

@@ -58,17 +58,6 @@ export const GlobalStyles = () => {
display: none !important;
}
.ant-dropdown,
.ant-dropdown,
.ant-select-dropdown,
.ant-modal-wrap,
.ant-modal-mask,
.ant-picker-dropdown,
.ant-popover,
.ant-popover {
z-index: ${theme.zIndexPopupBase} !important;
}
.no-wrap {
white-space: nowrap;
}

View File

@@ -18,7 +18,7 @@
*/
import { theme as antdThemeImport } from 'antd';
import { Theme } from './Theme';
import { AnyThemeConfig } from './types';
import { AnyThemeConfig, ThemeAlgorithm } from './types';
// Mock emotion's cache to avoid actual DOM operations
jest.mock('@emotion/cache', () => ({
@@ -44,7 +44,7 @@ describe('Theme', () => {
const parsedJson = JSON.parse(jsonString);
expect(parsedJson.token?.colorPrimary).toBe('#ff0000');
expect(parsedJson.algorithm).toBe('dark');
expect(parsedJson.algorithm).toBe(ThemeAlgorithm.DARK);
});
});
@@ -91,7 +91,7 @@ describe('Theme', () => {
// Verify dark mode by using the serialized config from the public method
const serialized = theme.toSerializedConfig();
expect(serialized.algorithm).toBe('dark');
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
});
});
@@ -137,7 +137,7 @@ describe('Theme', () => {
// Verify the algorithm was updated
const serialized = theme.toSerializedConfig();
expect(serialized.algorithm).toBe('dark');
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
});
});
@@ -150,7 +150,7 @@ describe('Theme', () => {
// Verify dark algorithm is used
const serialized = theme.toSerializedConfig();
expect(serialized.algorithm).toBe('dark');
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
});
it('switches to default algorithm when toggling dark mode off', () => {
@@ -164,7 +164,7 @@ describe('Theme', () => {
// Verify default algorithm is used
const serialized = theme.toSerializedConfig();
expect(serialized.algorithm).toBe('default');
expect(serialized.algorithm).toBe(ThemeAlgorithm.DEFAULT);
});
it('preserves other algorithms when toggling dark mode', () => {
@@ -181,10 +181,11 @@ describe('Theme', () => {
// Verify default algorithm replaces dark but compact is preserved
const serialized = theme.toSerializedConfig();
expect(Array.isArray(serialized.algorithm)).toBe(true);
expect(serialized.algorithm).toContain('default');
expect(serialized.algorithm).toContain('compact');
expect(serialized.algorithm).not.toContain('dark');
expect(serialized.algorithm).toContain(ThemeAlgorithm.DEFAULT);
expect(serialized.algorithm).toContain(ThemeAlgorithm.COMPACT);
expect(serialized.algorithm).not.toContain(ThemeAlgorithm.DARK);
});
});
@@ -218,7 +219,7 @@ describe('Theme', () => {
const serialized = theme.toSerializedConfig();
expect(serialized.token?.colorPrimary).toBe('#ff0000');
expect(serialized.algorithm).toBe('dark');
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
});
});
});

View File

@@ -22,12 +22,20 @@ import React from 'react';
import { theme as antdThemeImport, ConfigProvider } from 'antd';
import tinycolor from 'tinycolor2';
// @fontsource/* v5.1+ doesn't play nice with eslint-import plugin v2.31+
/* eslint-disable import/extensions */
import '@fontsource/inter/200.css';
/* eslint-disable import/extensions */
import '@fontsource/inter/400.css';
/* eslint-disable import/extensions */
import '@fontsource/inter/500.css';
/* eslint-disable import/extensions */
import '@fontsource/inter/600.css';
/* eslint-disable import/extensions */
import '@fontsource/fira-code/400.css';
/* eslint-disable import/extensions */
import '@fontsource/fira-code/500.css';
/* eslint-disable import/extensions */
import '@fontsource/fira-code/600.css';
import {
@@ -35,6 +43,7 @@ import {
CacheProvider as EmotionCacheProvider,
} from '@emotion/react';
import createCache from '@emotion/cache';
import { noop } from 'lodash';
import { GlobalStyles } from './GlobalStyles';
import {
@@ -285,6 +294,7 @@ export class Theme {
antdConfig: AntdThemeConfig,
emotionCache: any,
): void {
noop(theme, antdConfig, emotionCache);
// Overridden at runtime by SupersetThemeProvider using setThemeState
}

View File

@@ -17,7 +17,7 @@
* under the License.
*/
/* eslint-disable theme-colors/no-literal-colors */
import { SerializableThemeConfig } from './types';
import { type SerializableThemeConfig, ThemeAlgorithm } from './types';
const exampleThemes: Record<string, SerializableThemeConfig> = {
superset: {
@@ -27,11 +27,11 @@ const exampleThemes: Record<string, SerializableThemeConfig> = {
},
supersetDark: {
token: {},
algorithm: 'dark',
algorithm: ThemeAlgorithm.DARK,
},
supersetCompact: {
token: {},
algorithm: 'compact',
algorithm: ThemeAlgorithm.COMPACT,
},
funky: {
token: {
@@ -43,7 +43,7 @@ const exampleThemes: Record<string, SerializableThemeConfig> = {
borderRadius: 12,
fontFamily: 'Comic Sans MS, cursive',
},
algorithm: 'default',
algorithm: ThemeAlgorithm.DEFAULT,
},
funkyDark: {
token: {
@@ -55,7 +55,7 @@ const exampleThemes: Record<string, SerializableThemeConfig> = {
borderRadius: 12,
fontFamily: 'Comic Sans MS, cursive',
},
algorithm: 'dark',
algorithm: ThemeAlgorithm.DARK,
},
};
export default exampleThemes;

View File

@@ -16,17 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
import emotionStyled from '@emotion/styled';
import emotionStyled, { CreateStyled } from '@emotion/styled';
import { useTheme as useThemeBasic } from '@emotion/react';
// import { theme as antdThemeImport } from 'antd';
import { Theme } from './Theme';
import type {
SupersetTheme,
SerializableThemeConfig,
AnyThemeConfig,
ThemeStorage,
ThemeControllerOptions,
ThemeContextType,
import {
type SupersetTheme,
type SerializableThemeConfig,
type AnyThemeConfig,
type ThemeStorage,
type ThemeControllerOptions,
type ThemeContextType,
ThemeAlgorithm,
} from './types';
export {
@@ -56,10 +56,12 @@ export function useTheme() {
return theme;
}
const styled = emotionStyled;
const styled: CreateStyled = emotionStyled;
// launching in in dark mode for now while iterating
const themeObject = Theme.fromConfig({ algorithm: 'default' });
const themeObject: Theme = Theme.fromConfig({
algorithm: ThemeAlgorithm.DEFAULT,
});
const { theme } = themeObject;
const supersetTheme = theme;

View File

@@ -33,6 +33,41 @@ import { Theme } from '.';
export type AntdTokens = ReturnType<typeof antdThemeImport.getDesignToken>;
export type AntdThemeConfig = ThemeConfig;
/**
* Theme algorithms supported by Antd.
* They can be used individually or in combination.
* - DEFAULT: Default light theme
* - DARK: Dark theme
* - COMPACT: Compact theme (smaller spacing)
*/
export enum ThemeAlgorithm {
DEFAULT = 'default',
DARK = 'dark',
COMPACT = 'compact',
}
/**
* Represents the current theme mode of the app.
* It can be one of the following:
* - DEFAULT: Light theme
* - DARK: Dark theme
* - SYSTEM: System theme (auto-detects based on system settings)
*/
export enum ThemeMode {
DEFAULT = 'default',
DARK = 'dark',
SYSTEM = 'system',
}
/**
* All valid algorithm values that can be used in theme config.
*/
export type ThemeAlgorithmOption =
| ThemeAlgorithm.DEFAULT
| ThemeAlgorithm.DARK
| ThemeAlgorithm.COMPACT
| ThemeAlgorithm[];
/**
* A serializable version of Ant Design's ThemeConfig
* Compatible with theme editor exports
@@ -40,11 +75,7 @@ export type AntdThemeConfig = ThemeConfig;
export type SerializableThemeConfig = {
token?: Record<string, any>;
components?: Record<string, any>;
algorithm?:
| 'default'
| 'dark'
| 'compact'
| ('default' | 'dark' | 'compact')[];
algorithm?: ThemeAlgorithmOption;
hashed?: boolean;
inherit?: boolean;
};
@@ -358,13 +389,6 @@ export type AllowedAntdTokenKeys = Extract<
keyof AntdTokens
>;
export enum ThemeMode {
LIGHT = 'light',
DARK = 'dark',
SYSTEM = 'system',
COMPACT = 'compact',
}
export type SharedAntdTokens = Pick<AntdTokens, AllowedAntdTokenKeys>;
/** The final shape for our custom theme object, combining old theme + shared antd + superset specifics. */
@@ -379,7 +403,7 @@ export interface ThemeStorage {
}
export interface ThemeControllerOptions {
themeObject: Theme;
themeObject?: Theme;
storage?: ThemeStorage;
storageKey?: string;
modeStorageKey?: string;
@@ -393,6 +417,6 @@ export interface ThemeContextType {
theme: Theme;
themeMode: ThemeMode;
setTheme: (config: AnyThemeConfig) => void;
changeThemeMode: (newMode: ThemeMode) => void;
setThemeMode: (newMode: ThemeMode) => void;
resetTheme: () => void;
}

View File

@@ -28,9 +28,10 @@ import {
genDeprecatedColorVariations,
} from './utils';
import {
AnyThemeConfig,
SerializableThemeConfig,
AntdThemeConfig,
type AnyThemeConfig,
type SerializableThemeConfig,
type AntdThemeConfig,
ThemeAlgorithm,
} from './types';
// Mock tinycolor2 for consistent testing
@@ -50,22 +51,25 @@ describe('Theme utilities', () => {
const config: AnyThemeConfig = {
token: { colorPrimary: '#ff0000' },
};
expect(isSerializableConfig(config)).toBe(true);
});
it('returns true when algorithm is a string', () => {
const config: AnyThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: 'dark',
algorithm: ThemeAlgorithm.DARK,
};
expect(isSerializableConfig(config)).toBe(true);
});
it('returns true when algorithm is an array of strings', () => {
const config: AnyThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: ['dark', 'compact'],
algorithm: [ThemeAlgorithm.DARK, ThemeAlgorithm.COMPACT],
};
expect(isSerializableConfig(config)).toBe(true);
});
@@ -74,15 +78,19 @@ describe('Theme utilities', () => {
token: { colorPrimary: '#ff0000' },
algorithm: antdThemeImport.darkAlgorithm,
};
expect(isSerializableConfig(config)).toBe(false);
});
it('returns false when algorithm is an array containing a function', () => {
const config: AnyThemeConfig = {
token: { colorPrimary: '#ff0000' },
// @ts-ignore
algorithm: [antdThemeImport.darkAlgorithm, 'compact'],
algorithm: [
antdThemeImport.darkAlgorithm,
antdThemeImport.compactAlgorithm,
],
};
expect(isSerializableConfig(config)).toBe(false);
});
});
@@ -91,18 +99,22 @@ describe('Theme utilities', () => {
it('converts string algorithm to function reference', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: 'dark',
algorithm: ThemeAlgorithm.DARK,
};
const result = deserializeThemeConfig(config);
expect(result.algorithm).toBe(antdThemeImport.darkAlgorithm);
});
it('converts array of string algorithms to function references', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: ['dark', 'compact'],
algorithm: [ThemeAlgorithm.DARK, ThemeAlgorithm.COMPACT],
};
const result = deserializeThemeConfig(config);
expect(Array.isArray(result.algorithm)).toBe(true);
expect(result.algorithm).toContain(antdThemeImport.darkAlgorithm);
expect(result.algorithm).toContain(antdThemeImport.compactAlgorithm);
@@ -111,10 +123,12 @@ describe('Theme utilities', () => {
it('preserves other configuration properties', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: 'dark',
algorithm: ThemeAlgorithm.DARK,
hashed: true,
};
const result = deserializeThemeConfig(config);
expect(result.token).toEqual({ colorPrimary: '#ff0000' });
expect(result.hashed).toBe(true);
});
@@ -123,25 +137,31 @@ describe('Theme utilities', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
};
const result = deserializeThemeConfig(config);
expect(result.algorithm).toBeUndefined();
expect(result.algorithm).toBe(antdThemeImport.defaultAlgorithm);
});
it('converts default algorithm string to function reference', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: 'default',
algorithm: ThemeAlgorithm.DEFAULT,
};
const result = deserializeThemeConfig(config);
expect(result.algorithm).toBe(antdThemeImport.defaultAlgorithm);
});
it('converts compact algorithm string to function reference', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: 'compact',
algorithm: ThemeAlgorithm.COMPACT,
};
const result = deserializeThemeConfig(config);
expect(result.algorithm).toBe(antdThemeImport.compactAlgorithm);
});
});
@@ -152,8 +172,10 @@ describe('Theme utilities', () => {
token: { colorPrimary: '#ff0000' },
algorithm: antdThemeImport.darkAlgorithm,
};
const result = serializeThemeConfig(config);
expect(result.algorithm).toBe('dark');
expect(result.algorithm).toBe(ThemeAlgorithm.DARK);
});
it('converts array of function algorithms to strings', () => {
@@ -164,10 +186,12 @@ describe('Theme utilities', () => {
antdThemeImport.compactAlgorithm,
],
};
const result = serializeThemeConfig(config);
expect(Array.isArray(result.algorithm)).toBe(true);
expect(result.algorithm).toContain('dark');
expect(result.algorithm).toContain('compact');
expect(result.algorithm).toContain(ThemeAlgorithm.DARK);
expect(result.algorithm).toContain(ThemeAlgorithm.COMPACT);
});
it('preserves other configuration properties', () => {
@@ -176,7 +200,9 @@ describe('Theme utilities', () => {
algorithm: antdThemeImport.darkAlgorithm,
hashed: true,
};
const result = serializeThemeConfig(config);
expect(result.token).toEqual({ colorPrimary: '#ff0000' });
expect(result.hashed).toBe(true);
});
@@ -185,7 +211,9 @@ describe('Theme utilities', () => {
const config: AntdThemeConfig = {
token: { colorPrimary: '#ff0000' },
};
const result = serializeThemeConfig(config);
expect(result.algorithm).toBeUndefined();
});
@@ -196,8 +224,10 @@ describe('Theme utilities', () => {
// @ts-ignore
algorithm: unknownAlgorithm,
};
const result = serializeThemeConfig(config);
expect(result.algorithm).toBe('default');
expect(result.algorithm).toBe(ThemeAlgorithm.DEFAULT);
});
it('converts default algorithm function to string', () => {
@@ -205,8 +235,10 @@ describe('Theme utilities', () => {
token: { colorPrimary: '#ff0000' },
algorithm: antdThemeImport.defaultAlgorithm,
};
const result = serializeThemeConfig(config);
expect(result.algorithm).toBe('default');
expect(result.algorithm).toBe(ThemeAlgorithm.DEFAULT);
});
it('converts compact algorithm function to string', () => {
@@ -214,8 +246,10 @@ describe('Theme utilities', () => {
token: { colorPrimary: '#ff0000' },
algorithm: antdThemeImport.compactAlgorithm,
};
const result = serializeThemeConfig(config);
expect(result.algorithm).toBe('compact');
expect(result.algorithm).toBe(ThemeAlgorithm.COMPACT);
});
it('defaults each unknown algorithm in array to "default"', () => {
@@ -225,9 +259,14 @@ describe('Theme utilities', () => {
// @ts-ignore
algorithm: [antdThemeImport.darkAlgorithm, unknownAlgorithm],
};
const result = serializeThemeConfig(config);
expect(Array.isArray(result.algorithm)).toBe(true);
expect(result.algorithm).toEqual(['dark', 'default']);
expect(result.algorithm).toEqual([
ThemeAlgorithm.DARK,
ThemeAlgorithm.DEFAULT,
]);
});
it('handles mixed known and unknown algorithms in array', () => {
@@ -244,13 +283,15 @@ describe('Theme utilities', () => {
unknownAlgorithm2,
],
};
const result = serializeThemeConfig(config);
expect(Array.isArray(result.algorithm)).toBe(true);
expect(result.algorithm).toEqual([
'dark',
'default',
'compact',
'default',
ThemeAlgorithm.DARK,
ThemeAlgorithm.DEFAULT,
ThemeAlgorithm.COMPACT,
ThemeAlgorithm.DEFAULT,
]);
});
});
@@ -261,16 +302,20 @@ describe('Theme utilities', () => {
token: { colorPrimary: '#ff0000' },
algorithm: antdThemeImport.darkAlgorithm,
};
const result = normalizeThemeConfig(config);
expect(result).toBe(config);
});
it('deserializes serializable configs', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: 'dark',
algorithm: ThemeAlgorithm.DARK,
};
const result = normalizeThemeConfig(config);
expect(result.algorithm).toBe(antdThemeImport.darkAlgorithm);
});
});
@@ -278,14 +323,18 @@ describe('Theme utilities', () => {
describe('getAntdConfig', () => {
it('returns config with default algorithm for light mode', () => {
const seed = { colorPrimary: '#ff0000' };
const result = getAntdConfig(seed, false);
expect(result.token).toBe(seed);
expect(result.algorithm).toBe(antdThemeImport.defaultAlgorithm);
});
it('returns config with dark algorithm for dark mode', () => {
const seed = { colorPrimary: '#ff0000' };
const result = getAntdConfig(seed, true);
expect(result.token).toBe(seed);
expect(result.algorithm).toBe(antdThemeImport.darkAlgorithm);
});
@@ -301,7 +350,9 @@ describe('Theme utilities', () => {
colorInfo: '#info',
otherToken: 'ignore-me',
};
const result = getSystemColors(tokens);
expect(result).toEqual({
colorPrimary: '#primary',
colorError: '#error',
@@ -315,6 +366,7 @@ describe('Theme utilities', () => {
describe('genDeprecatedColorVariations', () => {
it('generates color variations for light mode', () => {
const result = genDeprecatedColorVariations('#base-color', false);
expect(result.base).toBe('#base-color');
expect(result.light1).toBe('#mixed-color');
expect(result.dark1).toBe('#mixed-color');
@@ -322,6 +374,7 @@ describe('Theme utilities', () => {
it('generates color variations for dark mode', () => {
const result = genDeprecatedColorVariations('#base-color', true);
expect(result.base).toBe('#base-color');
expect(result.light1).toBe('#mixed-color');
expect(result.dark1).toBe('#mixed-color');
@@ -337,7 +390,9 @@ describe('Theme utilities', () => {
colorSuccess: '#success',
colorInfo: '#info',
};
const result = getDeprecatedColors(systemColors, false);
expect(result.primary.base).toBe('#primary');
expect(result.error.base).toBe('#error');
expect(result.warning.base).toBe('#warning');

View File

@@ -19,13 +19,14 @@
import { theme as antdThemeImport } from 'antd';
import tinycolor from 'tinycolor2';
import {
AntdThemeConfig,
AnyThemeConfig,
SerializableThemeConfig,
DeprecatedColorVariations,
DeprecatedThemeColors,
SystemColors,
SupersetTheme,
type AntdThemeConfig,
type AnyThemeConfig,
type SerializableThemeConfig,
type DeprecatedColorVariations,
type DeprecatedThemeColors,
type SystemColors,
type SupersetTheme,
ThemeAlgorithm,
} from './types';
/**
@@ -38,9 +39,8 @@ export function isSerializableConfig(
if (algorithm === undefined) return true;
if (Array.isArray(algorithm)) {
if (Array.isArray(algorithm))
return (algorithm as unknown[]).every(alg => typeof alg === 'string');
}
return typeof algorithm === 'string';
}
@@ -60,9 +60,21 @@ export function deserializeThemeConfig(
let resolvedAlgorithm;
if (Array.isArray(algorithm)) {
resolvedAlgorithm = algorithm.map(alg => algorithmMap[alg]);
} else if (algorithm) {
const validAlgorithms = algorithm
.map((alg: ThemeAlgorithm) => algorithmMap[alg])
.filter(Boolean);
// If we have valid algorithms, use them; otherwise fallback to default
if (validAlgorithms.length > 0) {
resolvedAlgorithm =
validAlgorithms.length === 1 ? validAlgorithms[0] : validAlgorithms;
} else {
resolvedAlgorithm = antdThemeImport.defaultAlgorithm;
}
} else if (algorithm && algorithmMap[algorithm]) {
resolvedAlgorithm = algorithmMap[algorithm];
} else {
resolvedAlgorithm = antdThemeImport.defaultAlgorithm;
}
return {
@@ -83,19 +95,21 @@ export function serializeThemeConfig(
if (Array.isArray(algorithm)) {
serializedAlgorithm = algorithm.map(alg => {
if (alg === antdThemeImport.defaultAlgorithm) return 'default';
if (alg === antdThemeImport.darkAlgorithm) return 'dark';
if (alg === antdThemeImport.compactAlgorithm) return 'compact';
return 'default'; // Fallback
}) as ('default' | 'dark' | 'compact')[];
if (alg === antdThemeImport.defaultAlgorithm)
return ThemeAlgorithm.DEFAULT;
if (alg === antdThemeImport.darkAlgorithm) return ThemeAlgorithm.DARK;
if (alg === antdThemeImport.compactAlgorithm)
return ThemeAlgorithm.COMPACT;
return ThemeAlgorithm.DEFAULT; // Fallback
}) as ThemeAlgorithm[];
} else if (algorithm) {
if (algorithm === antdThemeImport.defaultAlgorithm)
serializedAlgorithm = 'default';
serializedAlgorithm = ThemeAlgorithm.DEFAULT;
else if (algorithm === antdThemeImport.darkAlgorithm)
serializedAlgorithm = 'dark';
serializedAlgorithm = ThemeAlgorithm.DARK;
else if (algorithm === antdThemeImport.compactAlgorithm)
serializedAlgorithm = 'compact';
else serializedAlgorithm = 'default'; // Fallback
serializedAlgorithm = ThemeAlgorithm.COMPACT;
else serializedAlgorithm = ThemeAlgorithm.DEFAULT; // Fallback
}
return {
@@ -109,9 +123,8 @@ export function serializeThemeConfig(
* This automatically detects and converts serializable configs
*/
export function normalizeThemeConfig(config: AnyThemeConfig): AntdThemeConfig {
if (isSerializableConfig(config)) {
return deserializeThemeConfig(config);
}
if (isSerializableConfig(config)) return deserializeThemeConfig(config);
return config as AntdThemeConfig;
}
@@ -164,7 +177,7 @@ export function getDeprecatedColors(
systemColors: SystemColors,
isDark: boolean,
): DeprecatedThemeColors {
const sc = systemColors;
const sc: SystemColors = systemColors;
return {
primary: genDeprecatedColorVariations(sc.colorPrimary, isDark),
error: genDeprecatedColorVariations(sc.colorError, isDark),

View File

@@ -21,6 +21,8 @@ import { isRequired } from '../utils';
import { TimeFormatFunction } from './types';
import stringifyTimeInput from './utils/stringifyTimeInput';
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
export const PREVIEW_TIME = new Date(Date.UTC(2017, 1, 14, 11, 22, 33));
// Use type augmentation to indicate that

View File

@@ -19,6 +19,8 @@
import { ExtensibleFunction } from '../models';
import { TimeRangeFormatFunction } from './types';
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
// Use type augmentation to indicate that
// an instance of TimeFormatter is also a function
interface TimeRangeFormatter {

View File

@@ -86,20 +86,33 @@ export default class Translator {
}
translate(input: string, ...args: unknown[]): string {
return this.i18n.translate(input).fetch(...args);
try {
return this.i18n.translate(input).fetch(...args);
} catch (err) {
logging.warn(`Translation failed for key "${input}" with args:`, args);
return input;
}
}
translateWithNumber(key: string, ...args: unknown[]): string {
const [plural, num, ...rest] = args;
if (typeof plural === 'number') {
try {
const [plural, num, ...rest] = args;
if (typeof plural === 'number') {
return this.i18n
.translate(key)
.ifPlural(plural, key)
.fetch(plural, num, ...rest);
}
return this.i18n
.translate(key)
.ifPlural(plural, key)
.fetch(plural, num, ...args);
.ifPlural(num as number, plural as string)
.fetch(...rest);
} catch (err) {
logging.warn(
`Plural translation failed for key "${key}" with args:`,
args,
);
}
return this.i18n
.translate(key)
.ifPlural(num as number, plural as string)
.fetch(...rest);
return key;
}
}

View File

@@ -45,6 +45,7 @@ export enum FeatureFlag {
EnableTemplateProcessing = 'ENABLE_TEMPLATE_PROCESSING',
EscapeMarkdownHtml = 'ESCAPE_MARKDOWN_HTML',
EstimateQueryCost = 'ESTIMATE_QUERY_COST',
FilterBarClosedByDefault = 'FILTERBAR_CLOSED_BY_DEFAULT',
GlobalAsyncQueries = 'GLOBAL_ASYNC_QUERIES',
ListviewsDefaultCardView = 'LISTVIEWS_DEFAULT_CARD_VIEW',
ScheduledQueries = 'SCHEDULED_QUERIES',
@@ -60,6 +61,8 @@ export enum FeatureFlag {
SlackEnableAvatars = 'SLACK_ENABLE_AVATARS',
EnableDashboardScreenshotEndpoints = 'ENABLE_DASHBOARD_SCREENSHOT_ENDPOINTS',
EnableDashboardDownloadWebDriverScreenshot = 'ENABLE_DASHBOARD_DOWNLOAD_WEBDRIVER_SCREENSHOT',
TableV2TimeComparisonEnabled = 'TABLE_V2_TIME_COMPARISON_ENABLED',
AgGridTableEnabled = 'AG_GRID_TABLE_ENABLED',
}
export type ScheduleQueriesProps = {

View File

@@ -35,6 +35,13 @@ const xssFilter = new FilterXSS({
'width',
'muted',
],
table: ['width', 'border', 'align', 'valign', 'style'],
tr: ['rowspan', 'align', 'valign', 'style'],
td: ['width', 'rowspan', 'colspan', 'align', 'valign', 'style'],
th: ['width', 'rowspan', 'colspan', 'align', 'valign', 'style'],
tbody: ['align', 'valign', 'style'],
thead: ['align', 'valign', 'style'],
tfoot: ['align', 'valign', 'style'],
},
stripIgnoreTag: true,
css: false,

View File

@@ -51,8 +51,8 @@ export const TestComponent = ({
style={{
width,
height,
backgroundColor: theme.colors.primary.light5,
color: theme.colors.grayscale.light3,
backgroundColor: theme.colorPrimaryBg,
color: theme.colorTextSecondary,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
@@ -66,7 +66,7 @@ export const TestComponent = ({
{[width, height].join('x')}
</div>
<div className="formData" style={{ padding: 10 }}>
<code style={{ color: theme.colors.primary.light2 }}>
<code style={{ color: theme.colorTextSecondary }}>
{JSON.stringify(formData)}
</code>
</div>

View File

@@ -19,6 +19,7 @@
import {
buildCustomFormatters,
Currency,
CurrencyFormatter,
getCustomFormatter,
getNumberFormatter,
@@ -116,6 +117,19 @@ test('buildCustomFormatters uses dataset d3 format if not provided in control pa
);
});
test('buildCustomFormatters returns NumberFormatter for a d3format with currency set to {}', () => {
const customFormatters: Record<string, ValueFormatter> =
buildCustomFormatters(
['count'],
{ count: {} as Currency },
{ count: ',.2%' },
undefined,
undefined,
);
expect(customFormatters.count).toBeInstanceOf(NumberFormatter);
});
test('getCustomFormatter', () => {
const customFormatters = {
sum__num: new CurrencyFormatter({

View File

@@ -17,6 +17,8 @@
* under the License.
*/
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
import { ExtensibleFunction } from '@superset-ui/core';
describe('ExtensibleFunction', () => {

View File

@@ -150,3 +150,32 @@ test('should sanitize HTML input', () => {
expect(html).toMatch(expectedHtml);
});
test('should preserve table styling after sanitization (fixes ECharts tooltip formatting)', () => {
const tableWithStyles = `
<table>
<tr style="opacity: 0.8;">
<td style="text-align: left; padding-left: 0px;">Label</td>
<td style="text-align: right; padding-left: 16px;">Value</td>
</tr>
</table>
`;
const sanitized = sanitizeHtml(tableWithStyles);
expect(sanitized).toContain('style="opacity: 0.8;"');
expect(sanitized).toContain('style="text-align: left; padding-left: 0px;"');
expect(sanitized).toContain('style="text-align: right; padding-left: 16px;"');
const data = [
['Metric', 'Value'],
['Sales', '$1,234'],
];
const html = tooltipHtml(data, 'Test Tooltip');
expect(html).toContain('style="opacity: 0.8;"');
expect(html).toContain('text-align: left');
expect(html).toContain('text-align: right');
expect(html).toContain('padding-left: 0px');
expect(html).toContain('padding-left: 16px');
expect(html).toContain('max-width: 300px');
});

View File

@@ -33,10 +33,10 @@
"dependencies": {
"@emotion/cache": "^11.14.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mihkeleidast/storybook-addon-source": "^1.0.1",
"@react-icons/all-files": "^4.1.0",
"@storybook/addon-actions": "8.1.11",
"@storybook/addon-actions": "9.0.8",
"@storybook/addon-controls": "8.1.11",
"@storybook/addon-links": "8.1.11",
"@storybook/react": "8.1.11",
@@ -58,7 +58,7 @@
"@babel/preset-typescript": "^7.23.3",
"@storybook/react-webpack5": "8.2.9",
"babel-loader": "^10.0.0",
"fork-ts-checker-webpack-plugin": "^9.0.2",
"fork-ts-checker-webpack-plugin": "^9.1.0",
"ts-loader": "^9.5.2",
"typescript": "^5.7.2"
},

View File

@@ -28,9 +28,7 @@ const colorTypes = [
'grayscale',
];
const AntDFunctionalColors = ({ antdTheme }) => {
const { antd } = supersetTheme;
const AntDFunctionalColors = () => {
// Define color types and variants dynamically
const variants = [
'active',
@@ -44,7 +42,6 @@ const AntDFunctionalColors = ({ antdTheme }) => {
'bg',
];
const { colors } = supersetTheme;
return (
<table
style={{ borderCollapse: 'collapse', width: '100%', textAlign: 'left' }}
@@ -63,32 +60,29 @@ const AntDFunctionalColors = ({ antdTheme }) => {
</tr>
</thead>
<tbody>
{colorTypes.map(type => {
const typeKey = `color${type}`;
return (
<tr key={type}>
<td style={{ border: '1px solid #ddd', padding: '8px' }}>
<strong>{type}</strong>
</td>
{variants.map(variant => {
const color = themeObject.getColorVariants(type)[variant];
return (
<td
key={variant}
style={{
border: '1px solid #ddd',
padding: '8px',
backgroundColor: color || 'transparent',
color: [`color${type}${variant}`],
}}
>
{color ? <code>{color}</code> : '-'}
</td>
);
})}
</tr>
);
})}
{colorTypes.map(type => (
<tr key={type}>
<td style={{ border: '1px solid #ddd', padding: '8px' }}>
<strong>{type}</strong>
</td>
{variants.map(variant => {
const color = themeObject.getColorVariants(type)[variant];
return (
<td
key={variant}
style={{
border: '1px solid #ddd',
padding: '8px',
backgroundColor: color || 'transparent',
color: `color${type}${variant}`,
}}
>
{color ? <code>{color}</code> : '-'}
</td>
);
})}
</tr>
))}
</tbody>
</table>
);
@@ -159,7 +153,7 @@ export const ThemeColors = () => {
</table>
<h2>Ant Design Theme Colors</h2>
<h3>Functional Colors</h3>
<AntDFunctionalColors antdTheme={supersetTheme.antd} />
<AntDFunctionalColors />
<h2>The supersetTheme object</h2>
<pre>
<code>{JSON.stringify(supersetTheme, null, 2)}</code>

View File

@@ -31,7 +31,7 @@
"dependencies": {
"d3": "^3.5.17",
"prop-types": "^15.8.1",
"react": "^17.0.2"
"react": "^19.1.0"
},
"peerDependencies": {
"@superset-ui/chart-controls": "*",

View File

@@ -31,13 +31,14 @@
"@mapbox/geojson-extent": "^1.0.1",
"@math.gl/web-mercator": "^4.1.0",
"@types/d3-array": "^2.0.0",
"@types/geojson": "^7946.0.15",
"@types/geojson": "^7946.0.16",
"bootstrap-slider": "^11.0.2",
"d3-array": "^1.2.4",
"d3-color": "^1.4.1",
"d3-scale": "^3.0.0",
"lodash": "^4.17.21",
"mousetrap": "^1.6.5",
"ngeohash": "^0.6.3",
"prop-types": "^15.8.1",
"underscore": "^1.13.7",
"urijs": "^1.19.11",
@@ -45,6 +46,7 @@
},
"devDependencies": {
"@types/mapbox__geojson-extent": "^1.0.3",
"@types/ngeohash": "^0.6.8",
"@types/underscore": "^1.13.0",
"@types/urijs": "^1.19.25"
},

View File

@@ -28,24 +28,28 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react';
import {
CategoricalColorNamespace,
Datasource,
FilterState,
HandlerFunction,
JsonObject,
JsonValue,
QueryFormData,
SetDataMaskHook,
} from '@superset-ui/core';
import type { Layer } from '@deck.gl/core';
import Legend from './components/Legend';
import { hexToRGB } from './utils/colors';
import sandboxedEval from './utils/sandbox';
// eslint-disable-next-line import/extensions
import fitViewport, { Viewport } from './utils/fitViewport';
import {
DeckGLContainerHandle,
DeckGLContainerStyledWrapper,
} from './DeckGLContainer';
import { Point } from './types';
import { getLayerType } from './factory';
import { GetLayerType } from './factory';
import { ColorBreakpointType, ColorType, Point } from './types';
import { TooltipProps } from './components/Tooltip';
import { COLOR_SCHEME_TYPES, ColorSchemeType } from './utilities/utils';
import { getColorBreakpointsBuckets } from './utils';
import { DEFAULT_DECKGL_COLOR } from './utilities/Shared_DeckGL';
const { getScale } = CategoricalColorNamespace;
@@ -54,18 +58,24 @@ function getCategories(fd: QueryFormData, data: JsonObject[]) {
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
const appliedScheme = fd.color_scheme;
const colorFn = getScale(appliedScheme);
const categories: Record<any, { color: any; enabled: boolean }> = {};
data.forEach(d => {
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
let color;
if (fd.dimension) {
color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
} else {
color = fixedColor;
let categories: Record<any, { color: any; enabled: boolean }> = {};
const colorSchemeType = fd.color_scheme_type;
if (colorSchemeType === COLOR_SCHEME_TYPES.color_breakpoints) {
categories = getColorBreakpointsBuckets(fd.color_breakpoints);
} else {
data.forEach(d => {
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
let color;
if (fd.dimension) {
color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
} else {
color = fixedColor;
}
categories[d.cat_color] = { color, enabled: true };
}
categories[d.cat_color] = { color, enabled: true };
}
});
});
}
return categories;
}
@@ -78,10 +88,14 @@ export type CategoricalDeckGLContainerProps = {
height: number;
width: number;
viewport: Viewport;
getLayer: getLayerType<unknown>;
getLayer: GetLayerType<unknown>;
payload: JsonObject;
onAddFilter?: HandlerFunction;
setControlValue: (control: string, value: JsonValue) => void;
filterState: FilterState;
setDataMask: SetDataMaskHook;
onContextMenu: HandlerFunction;
emitCrossFilters: boolean;
};
const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
@@ -128,29 +142,91 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
}
}, []);
const addColor = useCallback((data: JsonObject[], fd: QueryFormData) => {
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
const appliedScheme = fd.color_scheme;
const colorFn = getScale(appliedScheme);
const addColor = useCallback(
(
data: JsonObject[],
fd: QueryFormData,
selectedColorScheme: ColorSchemeType,
) => {
const appliedScheme = fd.color_scheme;
const colorFn = getScale(appliedScheme);
let color: ColorType;
return data.map(d => {
let color;
if (fd.dimension) {
color = hexToRGB(colorFn(d.cat_color, fd.slice_id), c.a * 255);
switch (selectedColorScheme) {
case COLOR_SCHEME_TYPES.fixed_color: {
color = fd.color_picker || { r: 0, g: 0, b: 0, a: 100 };
return { ...d, color };
return data.map(d => ({
...d,
color: [color.r, color.g, color.b, color.a * 255],
}));
}
case COLOR_SCHEME_TYPES.categorical_palette: {
return data.map(d => ({
...d,
color: hexToRGB(colorFn(d.cat_color, fd.slice_id)),
}));
}
case COLOR_SCHEME_TYPES.color_breakpoints: {
const defaultBreakpointColor = fd.deafult_breakpoint_color
? [
fd.deafult_breakpoint_color.r,
fd.deafult_breakpoint_color.g,
fd.deafult_breakpoint_color.b,
fd.deafult_breakpoint_color.a * 255,
]
: [
DEFAULT_DECKGL_COLOR.r,
DEFAULT_DECKGL_COLOR.g,
DEFAULT_DECKGL_COLOR.b,
DEFAULT_DECKGL_COLOR.a * 255,
];
return data.map(d => {
const breakpointForPoint: ColorBreakpointType =
fd.color_breakpoints?.find(
(breakpoint: ColorBreakpointType) =>
d.metric >= breakpoint.minValue &&
d.metric <= breakpoint.maxValue,
);
return {
...d,
color: breakpointForPoint
? [
breakpointForPoint?.color.r,
breakpointForPoint?.color.g,
breakpointForPoint?.color.b,
breakpointForPoint?.color.a * 255,
]
: defaultBreakpointColor,
};
});
}
default: {
return [];
}
}
return d;
});
}, []);
},
[],
);
const getLayers = useCallback(() => {
const { getLayer, payload, formData: fd, onAddFilter } = props;
const {
getLayer,
payload,
formData: fd,
onAddFilter,
onContextMenu,
filterState,
setDataMask,
emitCrossFilters,
} = props;
let features = payload.data.features ? [...payload.data.features] : [];
const selectedColorScheme = fd.color_scheme_type;
// Add colors from categories or fixed color
features = addColor(features, fd);
features = addColor(features, fd, selectedColorScheme);
// Apply user defined data mutator if defined
if (fd.js_data_mutator) {
@@ -169,13 +245,17 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
};
return [
getLayer(
fd,
filteredPayload,
getLayer({
formData: fd,
payload: filteredPayload,
onAddFilter,
setTooltip,
props.datasource,
) as Layer,
datasource: props.datasource,
onContextMenu,
filterState,
setDataMask,
emitCrossFilters,
}) as Layer,
];
}, [addColor, categories, props, setTooltip]);

View File

@@ -24,10 +24,12 @@ import {
forwardRef,
memo,
ReactNode,
MouseEvent,
useCallback,
useEffect,
useImperativeHandle,
useState,
useRef,
} from 'react';
import { isEqual } from 'lodash';
import { StaticMap } from 'react-map-gl';
@@ -58,6 +60,14 @@ export const DeckGLContainer = memo(
const [lastUpdate, setLastUpdate] = useState<number | null>(null);
const [viewState, setViewState] = useState(props.viewport);
const prevViewport = usePrevious(props.viewport);
const glContextRef = useRef<WebGL2RenderingContext | null>(null);
useEffect(
() => () => {
glContextRef.current?.getExtension('WEBGL_lose_context')?.loseContext();
},
[],
);
useImperativeHandle(ref, () => ({ setTooltip }), []);
@@ -106,7 +116,13 @@ export const DeckGLContainer = memo(
return (
<>
<div style={{ position: 'relative', width, height }}>
<div
style={{ position: 'relative', width, height }}
onContextMenu={(e: MouseEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
}}
>
<DeckGL
controller
width={width}
@@ -114,6 +130,9 @@ export const DeckGLContainer = memo(
layers={layers()}
viewState={viewState}
onViewStateChange={onViewStateChange}
onAfterRender={context => {
glContextRef.current = context.gl;
}}
>
<StaticMap
preserveDrawingBuffer

View File

@@ -22,11 +22,15 @@
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { isEqual } from 'lodash';
import {
AdhocFilter,
Datasource,
ensureIsArray,
HandlerFunction,
isDefined,
JsonObject,
JsonValue,
QueryFormData,
QueryObjectFilterClause,
SupersetClient,
usePrevious,
} from '@superset-ui/core';
@@ -107,70 +111,175 @@ const DeckMulti = (props: DeckMultiProps) => {
}
}, []);
const getLayerIndex = useCallback(
(sliceId: number, payloadIndex: number, deckSlices?: number[]): number =>
deckSlices ? deckSlices.indexOf(sliceId) : payloadIndex,
[],
);
const processLayerFilters = useCallback(
(
subslice: JsonObject,
formData: QueryFormData,
layerIndex: number,
): {
extraFilters: (AdhocFilter | QueryObjectFilterClause)[];
adhocFilters: AdhocFilter[];
} => {
const layerFilterScope = formData.layer_filter_scope;
const extraFilters: (AdhocFilter | QueryObjectFilterClause)[] = [
...(subslice.form_data.extra_filters || []),
...(formData.extra_filters || []),
];
const adhocFilters: AdhocFilter[] = [
...(subslice.form_data?.adhoc_filters || []),
];
if (layerFilterScope) {
const filterDataMapping = formData.filter_data_mapping || {};
let shouldAddDashboardAdhocFilters = false;
Object.entries(layerFilterScope).forEach(
([filterId, filterScope]: [string, number[]]) => {
const shouldApplyFilter =
ensureIsArray(filterScope).includes(layerIndex);
if (shouldApplyFilter) {
shouldAddDashboardAdhocFilters = true;
const filtersFromThisFilter = filterDataMapping[filterId] || [];
extraFilters.push(...filtersFromThisFilter);
}
},
);
if (shouldAddDashboardAdhocFilters) {
const dashboardAdhocFilters = formData.adhoc_filters || [];
adhocFilters.push(...dashboardAdhocFilters);
}
} else {
const originalExtraFormDataFilters =
formData.extra_form_data?.filters || [];
extraFilters.push(...originalExtraFormDataFilters);
const dashboardAdhocFilters = formData.adhoc_filters || [];
adhocFilters.push(...dashboardAdhocFilters);
}
return { extraFilters, adhocFilters };
},
[],
);
const createLayerFromData = useCallback(
(subslice: JsonObject, json: JsonObject): Layer =>
// @ts-ignore TODO(hainenber): define proper type for `form_data.viz_type` and call signature for functions in layerGenerators.
layerGenerators[subslice.form_data.viz_type](
subslice.form_data,
json,
props.onAddFilter,
setTooltip,
props.datasource,
[],
props.onSelect,
),
[props.onAddFilter, props.onSelect, props.datasource, setTooltip],
);
const loadSingleLayer = useCallback(
(
subslice: JsonObject,
formData: QueryFormData,
payloadIndex: number,
): void => {
const layerIndex = getLayerIndex(
subslice.slice_id,
payloadIndex,
formData.deck_slices,
);
let extraFilters: (AdhocFilter | QueryObjectFilterClause)[] = [];
let adhocFilters: AdhocFilter[] = [];
const isExplore = (window.location.href || '').includes('explore');
if (isExplore) {
// in explore all the filters are in the adhoc_filters
const adhocFiltersFromFormData = formData.adhoc_filters || [];
const finalAdhocFilters = adhocFiltersFromFormData
.map((filter: AdhocFilter & { layerFilterScope?: number[] }) => {
if (!isDefined(filter?.layerFilterScope)) {
return filter;
}
if (
Array.isArray(filter.layerFilterScope) &&
filter.layerFilterScope.length > 0
) {
if (filter.layerFilterScope.includes(-1)) {
return filter;
}
if (filter.layerFilterScope.includes(layerIndex)) {
return filter;
}
}
return undefined;
})
.filter(filter => isDefined(filter));
adhocFilters = finalAdhocFilters as AdhocFilter[];
} else {
const {
extraFilters: processLayerFiltersResultExtraFilters,
adhocFilters: processLayerFiltersResultAdhocFilters,
} = processLayerFilters(subslice, formData, layerIndex);
extraFilters = processLayerFiltersResultExtraFilters;
adhocFilters = processLayerFiltersResultAdhocFilters;
}
const subsliceCopy = {
...subslice,
form_data: {
...subslice.form_data,
extra_filters: extraFilters,
adhoc_filters: adhocFilters,
},
} as any as JsonObject & { slice_id: number };
const url = getExploreLongUrl(subsliceCopy.form_data, 'json');
if (url) {
SupersetClient.get({ endpoint: url })
.then(({ json }) => {
const layer = createLayerFromData(subsliceCopy, json);
setSubSlicesLayers(subSlicesLayers => ({
...subSlicesLayers,
[subsliceCopy.slice_id]: layer,
}));
})
.catch(error => {
console.error(
`Error loading layer for slice ${subsliceCopy.slice_id}:`,
error,
);
});
}
},
[getLayerIndex, processLayerFilters, createLayerFromData],
);
const loadLayers = useCallback(
(formData: QueryFormData, payload: JsonObject, viewport?: Viewport) => {
(
formData: QueryFormData,
payload: JsonObject,
viewport?: Viewport,
): void => {
setViewport(getAdjustedViewport());
setSubSlicesLayers({});
payload.data.slices.forEach(
(subslice: { slice_id: number } & JsonObject) => {
// Filters applied to multi_deck are passed down to underlying charts
// note that dashboard contextual information (filter_immune_slices and such) aren't
// taken into consideration here
const extra_filters = [
...(subslice.form_data.extra_filters || []),
...(formData.extra_filters || []),
...(formData.extra_form_data?.filters || []),
];
const adhoc_filters = [
...(formData.adhoc_filters || []),
...(subslice.formData?.adhoc_filters || []),
...(formData.extra_form_data?.adhoc_filters || []),
];
const subsliceCopy = {
...subslice,
form_data: {
...subslice.form_data,
extra_filters,
adhoc_filters,
},
};
const url = getExploreLongUrl(subsliceCopy.form_data, 'json');
if (url) {
SupersetClient.get({
endpoint: url,
})
.then(({ json }) => {
// @ts-ignore TODO(hainenber): define proper type for `form_data.viz_type` and call signature for functions in layerGenerators.
const layer = layerGenerators[subsliceCopy.form_data.viz_type](
subsliceCopy.form_data,
json,
props.onAddFilter,
setTooltip,
props.datasource,
[],
props.onSelect,
);
setSubSlicesLayers(subSlicesLayers => ({
...subSlicesLayers,
[subsliceCopy.slice_id]: layer,
}));
})
.catch(() => {});
}
(subslice: { slice_id: number } & JsonObject, payloadIndex: number) => {
loadSingleLayer(subslice, formData, payloadIndex);
},
);
},
[
props.datasource,
props.onAddFilter,
props.onSelect,
setTooltip,
getAdjustedViewport,
],
[getAdjustedViewport, loadSingleLayer],
);
const prevDeckSlices = usePrevious(props.formData.deck_slices);

View File

@@ -21,6 +21,7 @@
*/
import { memo } from 'react';
import { formatNumber, styled } from '@superset-ui/core';
import { Color } from '@deck.gl/core';
const StyledLegend = styled.div`
${({ theme }) => `
@@ -59,7 +60,7 @@ export type LegendProps = {
format: string | null;
forceCategorical?: boolean;
position?: null | 'tl' | 'tr' | 'bl' | 'br';
categories: Record<string, { enabled: boolean; color: number[] | undefined }>;
categories: Record<string, { enabled: boolean; color: Color | undefined }>;
toggleCategory?: (key: string) => void;
showSingleCategory?: (key: string) => void;
};

View File

@@ -25,6 +25,11 @@ import {
JsonObject,
HandlerFunction,
usePrevious,
SetDataMaskHook,
DataMask,
FilterState,
JsonValue,
ContextMenuFilters,
} from '@superset-ui/core';
import {
@@ -35,38 +40,59 @@ import CategoricalDeckGLContainer from './CategoricalDeckGLContainer';
import fitViewport, { Viewport } from './utils/fitViewport';
import { Point } from './types';
import { TooltipProps } from './components/Tooltip';
import { getColorBreakpointsBuckets } from './utils';
import Legend from './components/Legend';
type deckGLComponentProps = {
type DeckGLComponentProps = {
datasource: Datasource;
formData: QueryFormData;
height: number;
onAddFilter: HandlerFunction;
onContextMenu: HandlerFunction;
payload: JsonObject;
setControlValue: () => void;
viewport: Viewport;
width: number;
filterState: FilterState;
setDataMask: SetDataMaskHook;
emitCrossFilters: boolean;
};
export interface getLayerType<T> {
(
formData: QueryFormData,
payload: JsonObject,
onAddFilter: HandlerFunction | undefined,
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
datasource?: Datasource,
): T;
export interface GetLayerTypeParams {
formData: QueryFormData;
payload: JsonObject;
onAddFilter?: HandlerFunction;
setTooltip: (tooltip: TooltipProps['tooltip']) => void;
setDataMask?: (dataMask: DataMask) => void;
onContextMenu?: (
clientX: number,
clientY: number,
filters?: ContextMenuFilters,
) => void;
datasource?: Datasource;
filterState?: FilterState;
selected?: JsonObject[];
onSelect?: (value: JsonValue) => void;
emitCrossFilters?: boolean;
}
interface getPointsType {
export interface GetLayerType<T> {
(params: GetLayerTypeParams): T;
}
interface GetPointsType {
(data: JsonObject[]): Point[];
}
export function createDeckGLComponent(
getLayer: getLayerType<unknown>,
getPoints: getPointsType,
getLayer: GetLayerType<unknown>,
getPoints: GetPointsType,
) {
// Higher order component
return memo((props: deckGLComponentProps) => {
return memo((props: DeckGLComponentProps) => {
const containerRef = useRef<DeckGLContainerHandle>();
const prevFormData = usePrevious(props.formData);
const prevFilterState = usePrevious(props.filterState);
const prevPayload = usePrevious(props.payload);
const getAdjustedViewport = () => {
const { width, height, formData } = props;
@@ -79,6 +105,9 @@ export function createDeckGLComponent(
}
return props.viewport;
};
const [categories, setCategories] = useState<JsonObject>(
getColorBreakpointsBuckets(props.formData.color_breakpoints) || [],
);
const [viewport, setViewport] = useState(getAdjustedViewport());
@@ -90,48 +119,89 @@ export function createDeckGLComponent(
}, []);
const computeLayer = useCallback(
(props: deckGLComponentProps) => {
const { formData, payload, onAddFilter } = props;
(props: DeckGLComponentProps) => {
const {
formData,
payload,
onAddFilter,
filterState,
setDataMask,
onContextMenu,
emitCrossFilters,
} = props;
return getLayer(formData, payload, onAddFilter, setTooltip) as Layer;
return getLayer({
formData,
payload,
onAddFilter,
setTooltip,
setDataMask,
onContextMenu,
filterState,
emitCrossFilters,
}) as Layer;
},
[setTooltip],
);
useEffect(() => {
const categories = getColorBreakpointsBuckets(
props.formData.color_breakpoints,
);
setCategories(categories);
}, [props]);
const [layer, setLayer] = useState(computeLayer(props));
useEffect(() => {
// Only recompute the layer if anything BUT the viewport has changed
const prevFdNoVP = { ...prevFormData, viewport: null };
const currFdNoVP = { ...props.formData, viewport: null };
const prevFdNoVP = {
...prevFormData,
...prevFilterState,
viewport: null,
};
const currFdNoVP = {
...props.formData,
...props.filterState,
viewport: null,
};
if (!isEqual(prevFdNoVP, currFdNoVP) || prevPayload !== props.payload) {
setLayer(computeLayer(props));
}
}, [computeLayer, prevFormData, prevPayload, props]);
}, [computeLayer, prevFormData, prevFilterState, prevPayload, props]);
const { formData, payload, setControlValue, height, width } = props;
return (
<DeckGLContainerStyledWrapper
ref={containerRef}
mapboxApiAccessToken={payload.data.mapboxApiKey}
viewport={viewport}
layers={[layer]}
mapStyle={formData.mapbox_style}
setControlValue={setControlValue}
width={width}
height={height}
onViewportChange={setViewport}
/>
<div style={{ position: 'relative' }}>
<DeckGLContainerStyledWrapper
ref={containerRef}
mapboxApiAccessToken={payload.data.mapboxApiKey}
viewport={viewport}
layers={[layer]}
mapStyle={formData.mapbox_style}
setControlValue={setControlValue}
width={width}
height={height}
onViewportChange={setViewport}
/>
<Legend
forceCategorical
categories={categories}
format={props.formData.legend_format}
position={props.formData.legend_position}
/>
</div>
);
});
}
export function createCategoricalDeckGLComponent(
getLayer: getLayerType<Layer>,
getPoints: getPointsType,
getLayer: GetLayerType<Layer>,
getPoints: GetPointsType,
) {
return function Component(props: deckGLComponentProps) {
return function Component(props: DeckGLComponentProps) {
const {
datasource,
formData,
@@ -140,6 +210,10 @@ export function createCategoricalDeckGLComponent(
setControlValue,
viewport,
width,
setDataMask,
filterState,
onContextMenu,
emitCrossFilters,
} = props;
return (
@@ -154,6 +228,10 @@ export function createCategoricalDeckGLComponent(
getPoints={getPoints}
width={width}
height={height}
setDataMask={setDataMask}
onContextMenu={onContextMenu}
filterState={filterState}
emitCrossFilters={emitCrossFilters}
/>
);
};

View File

@@ -17,16 +17,11 @@
* under the License.
*/
import { ArcLayer } from '@deck.gl/layers';
import {
HandlerFunction,
JsonObject,
QueryFormData,
t,
} from '@superset-ui/core';
import { JsonObject, QueryFormData, t } from '@superset-ui/core';
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
import { commonLayerProps } from '../common';
import { createCategoricalDeckGLComponent } from '../../factory';
import { GetLayerType, createCategoricalDeckGLComponent } from '../../factory';
import TooltipRow from '../../TooltipRow';
import { TooltipProps } from '../../components/Tooltip';
import { Point } from '../../types';
export function getPoints(data: JsonObject[]) {
@@ -60,26 +55,50 @@ function setTooltipContent(formData: QueryFormData) {
);
}
export function getLayer(
fd: QueryFormData,
payload: JsonObject,
onAddFilter: HandlerFunction,
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
) {
export const getLayer: GetLayerType<ArcLayer> = function ({
formData,
payload,
setTooltip,
filterState,
setDataMask,
onContextMenu,
emitCrossFilters,
}) {
const fd = formData;
const data = payload.data.features;
const sc = fd.color_picker;
const tc = fd.target_color_picker;
const colorSchemeType = fd.color_scheme_type;
return new ArcLayer({
data,
getSourceColor: (d: any) =>
d.sourceColor || d.color || [sc.r, sc.g, sc.b, 255 * sc.a],
getTargetColor: (d: any) =>
d.targetColor || d.color || [tc.r, tc.g, tc.b, 255 * tc.a],
getSourceColor: (d: any) => {
if (colorSchemeType === COLOR_SCHEME_TYPES.fixed_color) {
return [sc.r, sc.g, sc.b, 255 * sc.a];
}
return d.targetColor || d.color;
},
getTargetColor: (d: any) => {
if (colorSchemeType === COLOR_SCHEME_TYPES.fixed_color) {
return [tc.r, tc.g, tc.b, 255 * tc.a];
}
return d.targetColor || d.color;
},
id: `path-layer-${fd.slice_id}` as const,
getWidth: fd.stroke_width ? fd.stroke_width : 3,
...commonLayerProps(fd, setTooltip, setTooltipContent(fd)),
...commonLayerProps({
formData: fd,
setTooltip,
setTooltipContent: setTooltipContent(fd),
onContextMenu,
setDataMask,
filterState,
emitCrossFilters,
}),
});
}
};
export default createCategoricalDeckGLComponent(getLayer, getPoints);

View File

@@ -22,11 +22,14 @@ import timeGrainSqlaAnimationOverrides, {
columnChoices,
PRIMARY_COLOR,
} from '../../utilities/controls';
import { formatSelectOptions } from '../../utilities/utils';
import {
COLOR_SCHEME_TYPES,
formatSelectOptions,
isColorSchemeTypeVisible,
} from '../../utilities/utils';
import {
filterNulls,
autozoom,
dimension,
jsColumns,
jsDataMutator,
jsTooltip,
@@ -35,6 +38,9 @@ import {
legendPosition,
viewport,
mapboxStyle,
deckGLCategoricalColor,
deckGLCategoricalColorSchemeSelect,
deckGLCategoricalColorSchemeTypeSelect,
} from '../../utilities/Shared_DeckGL';
const config: ControlPanelConfig = {
@@ -81,7 +87,37 @@ const config: ControlPanelConfig = {
label: t('Arc'),
controlSetRows: [
[
'color_picker',
{
name: 'color_scheme_type',
config: {
...deckGLCategoricalColorSchemeTypeSelect.config,
choices: [
[COLOR_SCHEME_TYPES.fixed_color, t('Fixed color')],
[
COLOR_SCHEME_TYPES.categorical_palette,
t('Categorical palette'),
],
],
default: COLOR_SCHEME_TYPES.fixed_color,
},
},
],
[
{
name: 'color_picker',
config: {
label: t('Source Color'),
description: t('Color of the source location'),
type: 'ColorPickerControl',
default: PRIMARY_COLOR,
renderTrigger: true,
visibility: ({ controls }) =>
isColorSchemeTypeVisible(
controls,
COLOR_SCHEME_TYPES.fixed_color,
),
},
},
{
name: 'target_color_picker',
config: {
@@ -90,22 +126,16 @@ const config: ControlPanelConfig = {
type: 'ColorPickerControl',
default: PRIMARY_COLOR,
renderTrigger: true,
visibility: ({ controls }) =>
isColorSchemeTypeVisible(
controls,
COLOR_SCHEME_TYPES.fixed_color,
),
},
},
],
[
{
name: dimension.name,
config: {
...dimension.config,
label: t('Categorical Color'),
description: t(
'Pick a dimension from which categorical colors are defined',
),
},
},
'color_scheme',
],
[deckGLCategoricalColor],
[deckGLCategoricalColorSchemeSelect],
[
{
name: 'stroke_width',
@@ -119,9 +149,9 @@ const config: ControlPanelConfig = {
choices: formatSelectOptions([1, 2, 3, 4, 5]),
},
},
legendPosition,
],
[legendFormat, null],
[legendPosition],
[legendFormat],
],
},
{

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