Compare commits

...

43 Commits

Author SHA1 Message Date
Beto Dealmeida
0a2850a33c Fix build 2025-12-18 21:30:08 -05:00
Beto Dealmeida
ff17daa424 Small fix 2025-12-18 19:10:21 -05:00
Beto Dealmeida
89c5c55dcb Small fix 2025-12-18 18:59:18 -05:00
Beto Dealmeida
db201285e5 Testing 2025-12-18 18:51:57 -05:00
Beto Dealmeida
1910a5c607 feat(frontend): extract default catalog/schema from API responses
Update useCatalogs and useSchemas hooks to:
- Parse the new `default` field from API responses
- Expose `defaultCatalog` and `defaultSchema` from the hooks
- Maintain backwards compatibility with the existing API

The internal response types are updated to include both the list of
options and the default value, while the external hook interface
continues to return the options as `data` for compatibility.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 18:15:44 -05:00
Beto Dealmeida
b377ce564b test(api): add tests for default catalog/schema in API responses
Add and update tests for the catalogs and schemas endpoints to verify:
- Default catalog/schema is returned when accessible
- Default is null when not in user's accessible list
- Default is null when retrieval fails (error handling)
- Default works correctly with upload_allowed filter
- Default is null when not in upload-allowed schemas list

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 18:11:08 -05:00
Beto Dealmeida
4c6df01353 feat(api): add default field to catalogs and schemas API responses
Add a `default` field to the `/api/v1/database/{id}/catalogs/` and
`/api/v1/database/{id}/schemas/` endpoints. This field contains the
default catalog/schema for the database, or null if it cannot be
determined or is not accessible to the user.

The default is retrieved from the database engine spec via
`database.get_default_catalog()` and `database.get_default_schema()`.
Error handling ensures the API never fails if default retrieval is slow
or fails - it simply returns null.

RBAC is respected: the default is only returned if the user has access
to it. For the upload_allowed filter, the default is checked against
the allowed schemas list.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 18:07:22 -05:00
Evan Rusackas
b8f31124d0 chore(frontend): migrate 13 JS/JSX files to TypeScript (#36720)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 13:54:49 -08:00
Evan Rusackas
da8e077a44 chore(frontend): migrate utility JS files to TypeScript (#36721)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 13:54:40 -08:00
Evan Rusackas
32435bc3e9 feat(docs): enhance Matomo analytics tracking (#36743)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 13:52:08 -08:00
Evan Rusackas
2cf0d7936e chore(pre-commit): exclude logos from end-of-file-fixer (#36744)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 13:51:50 -08:00
Evan Rusackas
0830a57fa6 feat(docs): add llms.txt for LLM-friendly documentation index (#36730)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 13:48:28 -08:00
Brandon Sovran
0f56e3b9ae fix: Implement SIP-40 error styles for GAQ (#36596) 2025-12-18 12:22:17 -08:00
Evan Rusackas
ee45b26ad7 fix(tests): optimize DatasourceEditorCurrency tests for CI reliability (#36723)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 09:37:53 -08:00
Kamil Gabryjelski
f3407d7a56 chore: Close playwright browser gracefully (#36537) 2025-12-18 17:30:22 +01:00
Joe Li
f51f7f3307 fix(tests): resolve flakey selectOption helper race condition (#36719)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 21:27:17 -08:00
Evan Rusackas
2f4f64dfe8 chore(frontend): migrate easy JS/JSX files to TypeScript (#36713)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 17:12:02 -08:00
Evan Rusackas
ae584c8886 chore: remove INTHEWILD.md after migration to YAML (#36718)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 12:59:19 -08:00
Đỗ Trọng Hải
b1e004e122 build(dev-deps): remove stub type definition packages (#36706)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2025-12-17 12:57:44 -08:00
Vitor Avila
737a5162e4 fix: Use is_active for guest users (#36716) 2025-12-17 17:23:22 -03:00
Evan Rusackas
b800412eda fix(docs): add retry logic and concurrency handling for badge downloads (#36715)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 12:19:39 -08:00
Michael S. Molina
24a4f8510d docs: Add SQL Lab Export to Google Sheets to community extensions registry (#36714) 2025-12-17 16:53:10 -03:00
Yousuf Ansari
33a425bbbc fix(echarts): use scroll legend for horizontal layouts to prevent overlap (#36306) 2025-12-17 11:16:35 -08:00
Catherine Qu
5ce4c52cfa feat(docs): In the Wild page with YAML data and AntD components (#36386)
Co-authored-by: Catherine Qu <catherine.qu@mail.utoronto.ca>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 09:42:29 -08:00
Luis Sánchez
c9ec173647 fix(SearchFilter): prevent unintended autocomplete on search input (#36209) 2025-12-17 20:41:07 +03:00
Michael S. Molina
71f9dcff5a chore: Bump core packages (0.0.1rc3, 0.0.1-rc6) (#36707) 2025-12-17 09:12:26 -08:00
dependabot[bot]
479b7a3fba chore(deps-dev): bump @pmmmwh/react-refresh-webpack-plugin from 0.5.17 to 0.6.2 in /superset-frontend (#36691)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 09:01:02 -08:00
dependabot[bot]
594ea972ca chore(deps-dev): bump @types/node from 25.0.2 to 25.0.3 in /superset-websocket (#36692)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 08:46:34 -08:00
dependabot[bot]
d77f7b6d20 chore(deps): bump nanoid from 5.0.9 to 5.1.6 in /superset-frontend (#36586)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 08:46:17 -08:00
dependabot[bot]
f4ded02e0d chore(deps-dev): bump typescript-eslint from 8.49.0 to 8.50.0 in /docs (#36650)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 08:46:01 -08:00
dependabot[bot]
f97fa08477 chore(deps-dev): bump baseline-browser-mapping from 2.9.7 to 2.9.8 in /superset-frontend (#36690)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 08:45:47 -08:00
dependabot[bot]
789be78166 chore(deps-dev): bump webpack from 5.103.0 to 5.104.0 in /docs (#36695)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 21:42:48 +07:00
dependabot[bot]
ea3d247017 chore(deps-dev): bump webpack-bundle-analyzer from 4.10.2 to 5.1.0 in /superset-frontend (#36610)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 16:06:20 -08:00
dependabot[bot]
6456f4c516 chore(deps): bump googleapis from 168.0.0 to 169.0.0 in /superset-frontend (#36646)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 15:48:07 -08:00
dependabot[bot]
e9bbf06938 chore(deps): bump re-resizable from 6.10.3 to 6.11.2 in /superset-frontend (#36647)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 15:47:53 -08:00
dependabot[bot]
ebee35ea5a chore(deps-dev): bump typescript-eslint from 8.49.0 to 8.50.0 in /superset-websocket (#36649)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 15:47:13 -08:00
SBIN2010
d0fb77cbc8 fix: removed dashboard from main page in "All" tab, refreshes dashboard list (#35945) 2025-12-16 15:45:58 -08:00
Evan Rusackas
46659c2bd1 fix(tests): resolve flaky ExploreChartHeader export menu tests (#36642)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 15:44:30 -08:00
dependabot[bot]
8407e9cf3b chore(deps): bump antd from 6.1.0 to 6.1.1 in /docs (#36655)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 15:43:53 -08:00
dependabot[bot]
5eeba2e734 chore(deps-dev): bump @typescript-eslint/parser from 8.49.0 to 8.50.0 in /docs (#36656)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 15:43:33 -08:00
dependabot[bot]
4ca8c000d1 chore(deps): update classnames requirement from ^2.2.5 to ^2.5.1 in /superset-frontend/packages/superset-ui-core (#36660)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 15:43:15 -08:00
dependabot[bot]
7108658de0 chore(deps-dev): bump @babel/runtime-corejs3 from 7.28.2 to 7.28.4 in /superset-frontend (#36664)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 15:42:58 -08:00
dependabot[bot]
42311f602e chore(deps-dev): bump npm from 11.5.2 to 11.7.0 in /superset-frontend (#36668)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 15:42:39 -08:00
105 changed files with 6324 additions and 4089 deletions

View File

@@ -54,7 +54,7 @@ repos:
exclude: ^helm/superset/templates/
- id: debug-statements
- id: end-of-file-fixer
exclude: .*/lerna\.json$
exclude: .*/lerna\.json$|^docs/static/img/logos/
- id: trailing-whitespace
exclude: ^.*\.(snap)
args: ["--markdown-linebreak-ext=md"]

View File

@@ -76,6 +76,9 @@ snowflake.svg
ydb.svg
loading.svg
# docs third-party logos, i.e. docs/static/img/logos/*
logos/*
# docs-related
erd.puml
erd.svg
@@ -83,6 +86,7 @@ intro_header.txt
# for LLMs
llm-context.md
llms.txt
AGENTS.md
LLMS.md
CLAUDE.md

View File

@@ -55,7 +55,7 @@ A modern, enterprise-ready business intelligence web application.
[**Get Involved**](#get-involved) |
[**Contributor Guide**](#contributor-guide) |
[**Resources**](#resources) |
[**Organizations Using Superset**](https://github.com/apache/superset/blob/master/RESOURCES/INTHEWILD.md)
[**Organizations Using Superset**](https://superset.apache.org/inTheWild)
## Why Superset?
@@ -171,7 +171,7 @@ how to set up a development environment.
## Resources
- [Superset "In the Wild"](https://github.com/apache/superset/blob/master/RESOURCES/INTHEWILD.md) - open a PR to add your org to the list!
- [Superset "In the Wild"](https://superset.apache.org/inTheWild) - see who's using Superset, and [add your organization](https://github.com/apache/superset/edit/master/RESOURCES/INTHEWILD.yaml) to the list!
- [Feature Flags](https://github.com/apache/superset/blob/master/RESOURCES/FEATURE_FLAGS.md) - the status of Superset's Feature Flags.
- [Standard Roles](https://github.com/apache/superset/blob/master/RESOURCES/STANDARD_ROLES.md) - How RBAC permissions map to roles.
- [Superset Wiki](https://github.com/apache/superset/wiki) - Tons of additional community resources: best practices, community content and other information.

View File

@@ -1,226 +0,0 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
## Superset Users in the Wild
Here's a list of organizations, broken down into broad industry categories, that have taken the time to send a PR to let
the world know they are using Apache Superset. If you are a user and want to be recognized,
all you have to do is file a simple PR [like this one](https://github.com/apache/superset/pull/10122) — [just click here](https://github.com/apache/superset/edit/master/RESOURCES/INTHEWILD.md) to do so. If you think
the categorization is inaccurate, please file a PR with your correction as well.
Join our growing community!
### Sharing Economy
- [Airbnb](https://github.com/airbnb)
- [Faasos](https://faasos.com/) [@shashanksingh]
- [Free2Move](https://www.free2move.com/) [@PaoloTerzi]
- [Hostnfly](https://www.hostnfly.com/) [@alexisrosuel]
- [Lime](https://www.li.me/) [@cxmcc]
- [Lyft](https://www.lyft.com/)
- [Ontruck](https://www.ontruck.com/)
### Financial Services
- [Aktia Bank plc](https://www.aktia.com)
- [American Express](https://www.americanexpress.com) [@TheLastSultan]
- [bumper](https://www.bumper.co/) [@vasu-ram, @JamiePercival]
- [Cape Crypto](https://capecrypto.com)
- [Capital Service S.A.](https://capitalservice.pl) [@pkonarzewski]
- [Clark.de](https://clark.de/)
- [Europace](https://europace.de)
- [KarrotPay](https://www.daangnpay.com/)
- [Remita](https://remita.net) [@mujibishola]
- [Taveo](https://www.taveo.com) [@codek]
- [Unit](https://www.unit.co/about-us) [@amitmiran137]
- [Wise](https://wise.com) [@koszti]
- [Xendit](https://xendit.co/) [@LieAlbertTriAdrian]
- [Cover Genius](https://covergenius.com/)
### Gaming
- [Popoko VM Games Studio](https://popoko.live)
### E-Commerce
- [AiHello](https://www.aihello.com) [@ganeshkrishnan1]
- [Bazaar Technologies](https://www.bazaartech.com) [@umair-abro]
- [Dragonpass](https://www.dragonpass.com.cn/) [@zhxjdwh]
- [Dropit Shopping](https://www.dropit.shop/) [@dropit-dev]
- [Fanatics](https://www.fanatics.com/) [@coderfender]
- [Fordeal](https://www.fordeal.com) [@Renkai]
- [Fynd](https://www.fynd.com/) [@darpanjain07]
- [GFG - Global Fashion Group](https://global-fashion-group.com) [@ksaagariconic]
- [GoTo/Gojek](https://www.gojek.io/) [@gwthm-in]
- [HuiShouBao](https://www.huishoubao.com/) [@Yukinoshita-Yukino]
- [Now](https://www.now.vn/) [@davidkohcw]
- [Qunar](https://www.qunar.com/) [@flametest]
- [Rakuten Viki](https://www.viki.com)
- [Shopee](https://shopee.sg) [@xiaohanyu]
- [Shopkick](https://www.shopkick.com) [@LAlbertalli]
- [ShopUp](https://www.shopup.org/) [@gwthm-in]
- [Tails.com](https://tails.com/gb/) [@alanmcruickshank]
- [THE ICONIC](https://theiconic.com.au/) [@ksaagariconic]
- [Utair](https://www.utair.ru) [@utair-digital]
- [VkusVill](https://vkusvill.ru/) [@ETselikov]
- [Zalando](https://www.zalando.com) [@dmigo]
- [Zalora](https://www.zalora.com) [@ksaagariconic]
- [Zepto](https://www.zeptonow.com/) [@gwthm-in]
### Enterprise Technology
- [A3Data](https://a3data.com.br) [@neylsoncrepalde]
- [Analytics Aura](https://analyticsaura.com/) [@Analytics-Aura]
- [Apollo GraphQL](https://www.apollographql.com/) [@evans]
- [Astronomer](https://www.astronomer.io) [@ryw]
- [Avesta Technologies](https://avestatechnologies.com/) [@TheRum]
- [Caizin](https://caizin.com/) [@tejaskatariya]
- [Canonical](https://canonical.com)
- [Careem](https://www.careem.com/) [@samraHanif0340]
- [Cloudsmith](https://cloudsmith.io) [@alancarson]
- [Cyberhaven](https://www.cyberhaven.com/) [@toliver-ch]
- [Deepomatic](https://deepomatic.com/) [@Zanoellia]
- [Dial Once](https://www.dial-once.com/)
- [Dremio](https://dremio.com) [@narendrans]
- [EFinance](https://www.efinance.com.eg) [@habeeb556]
- [Elestio](https://elest.io/) [@kaiwalyakoparkar]
- [ELMO Cloud HR & Payroll](https://elmosoftware.com.au/)
- [Endress+Hauser](https://www.endress.com/) [@rumbin]
- [FBK - ICT center](https://ict.fbk.eu)
- [Formbricks](https://formbricks.com)
- [Gavagai](https://gavagai.io) [@gavagai-corp]
- [GfK Data Lab](https://www.gfk.com/home) [@mherr]
- [HPE](https://www.hpe.com/in/en/home.html) [@anmol-hpe]
- [Hydrolix](https://www.hydrolix.io/)
- [Intercom](https://www.intercom.com/) [@kate-gallo]
- [jampp](https://jampp.com/)
- [Konfío](https://konfio.mx) [@uis-rodriguez]
- [Mainstrat](https://mainstrat.com/)
- [mishmash io](https://mishmash.io/) [@mishmash-io]
- [Myra Labs](https://www.myralabs.com/) [@viksit]
- [Nielsen](https://www.nielsen.com/) [@amitNielsen]
- [Ona](https://ona.io) [@pld]
- [Orange](https://www.orange.com) [@icsu]
- [Oslandia](https://oslandia.com)
- [Oxylabs](https://oxylabs.io/) [@rytis-ulys]
- [Peak AI](https://www.peak.ai/) [@azhar22k]
- [PeopleDoc](https://www.people-doc.com) [@rodo]
- [PlaidCloud](https://www.plaidcloud.com)
- [Preset, Inc.](https://preset.io)
- [PubNub](https://pubnub.com) [@jzucker2]
- [ReadyTech](https://www.readytech.io)
- [Reward Gateway](https://www.rewardgateway.com)
- [RIADVICE](https://riadvice.tn) [@riadvice]
- [ScopeAI](https://www.getscopeai.com) [@iloveluce]
- [shipmnts](https://shipmnts.com)
- [Showmax](https://showmax.com) [@bobek]
- [SingleStore](https://www.singlestore.com/)
- [TechAudit](https://www.techaudit.info) [@ETselikov]
- [Tenable](https://www.tenable.com) [@dflionis]
- [Tentacle](https://www.linkedin.com/company/tentacle-cmi/) [@jdclarke5]
- [timbr.ai](https://timbr.ai/) [@semantiDan]
- [Tobii](https://www.tobii.com/) [@dwa]
- [Tooploox](https://www.tooploox.com/) [@jakubczaplicki]
- [Unvired](https://unvired.com) [@srinisubramanian]
- [Virtuoso QA](https://www.virtuosoqa.com)
- [Whale](https://whale.im)
- [Windsor.ai](https://www.windsor.ai/) [@octaviancorlade]
- [WinWin Network马上赢](https://brandct.cn/) [@wenbinye]
- [Zeta](https://www.zeta.tech/) [@shaikidris]
### Media & Entertainment
- [6play](https://www.6play.fr) [@CoryChaplin]
- [bilibili](https://www.bilibili.com) [@Moinheart]
- [BurdaForward](https://www.burda-forward.de/en/)
- [Douban](https://www.douban.com/) [@luchuan]
- [Kuaishou](https://www.kuaishou.com/) [@zhaoyu89730105]
- [Netflix](https://www.netflix.com/)
- [Prensa Iberica](https://www.prensaiberica.es/) [@zamar-roura]
- [TME QQMUSIC/WESING](https://www.tencentmusic.com/) [@shenyuanli,@marklaw]
- [Xite](https://xite.com/) [@shashankkoppar]
- [Zaihang](https://www.zaih.com/)
### Education
- [Aveti Learning](https://avetilearning.com/) [@TheShubhendra]
- [Brilliant.org](https://brilliant.org/)
- [Open edX](https://openedx.org/)
- [Platzi.com](https://platzi.com/)
- [Sunbird](https://www.sunbird.org/) [@eksteporg]
- [The GRAPH Network](https://thegraphnetwork.org/) [@fccoelho]
- [Udemy](https://www.udemy.com/) [@sungjuly]
- [VIPKID](https://www.vipkid.com.cn/) [@illpanda]
- [WikiMedia Foundation](https://wikimediafoundation.org) [@vg]
### Energy
- [Airboxlab](https://foobot.io) [@antoine-galataud]
- [DouroECI](https://www.douroeci.com/) [@nunohelibeires]
- [Safaricom](https://www.safaricom.co.ke/) [@mmutiso]
- [Scoot](https://scoot.co/) [@haaspt]
- [Wattbewerb](https://wattbewerb.de/) [@wattbewerb]
### Healthcare
- [Amino](https://amino.com) [@shkr]
- [Bluesquare](https://www.bluesquarehub.com/) [@madewulf]
- [Care](https://www.getcare.io/) [@alandao2021]
- [Living Goods](https://www.livinggoods.org) [@chelule]
- [Maieutical Labs](https://maieuticallabs.it) [@xrmx]
- [Medic](https://medic.org) [@1yuv]
- [REDCap Cloud](https://www.redcapcloud.com/)
- [TrustMedis](https://trustmedis.com/) [@famasya]
- [WeSure](https://www.wesure.cn/)
- [2070Health](https://2070health.com/)
### HR / Staffing
- [Swile](https://www.swile.co/) [@PaoloTerzi]
- [Symmetrics](https://www.symmetrics.fyi)
- [bluquist](https://bluquist.com/)
### Government
- [City of Ann Arbor, MI](https://www.a2gov.org/) [@sfirke]
- [RIS3 Strategy of CZ, MIT CR](https://www.ris3.cz/) [@RIS3CZ]
- [NRLM - Sarathi, India](https://pib.gov.in/PressReleasePage.aspx?PRID=1999586)
### Travel
- [Agoda](https://www.agoda.com/) [@lostseaway, @maiake, @obombayo]
- [HomeToGo](https://hometogo.com/) [@pedromartinsteenstrup]
- [Skyscanner](https://www.skyscanner.net/) [@cleslie, @stanhoucke]
### Others
- [10Web](https://10web.io/)
- [AI inside](https://inside.ai/en/)
- [Automattic](https://automattic.com/) [@Khrol, @Usiel]
- [Dropbox](https://www.dropbox.com/) [@bkyryliuk]
- [Flowbird](https://flowbird.com) [@EmmanuelCbd]
- [GEOTAB](https://www.geotab.com) [@JZ6]
- [Grassroot](https://www.grassrootinstitute.org/)
- [Increff](https://www.increff.com/) [@ishansinghania]
- [komoot](https://www.komoot.com/) [@christophlingg]
- [Let's Roam](https://www.letsroam.com/)
- [Machrent SA](https://www.machrent.com/)
- [Onebeat](https://1beat.com/) [@GuyAttia]
- [X](https://x.com/)
- [VLMedia](https://www.vlmedia.com.tr/) [@ibotheperfect]
- [Yahoo!](https://yahoo.com/)

644
RESOURCES/INTHEWILD.yaml Normal file
View File

@@ -0,0 +1,644 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# Apache Superset Users in the Wild
#
# To add your organization:
# 1. Find the appropriate category (or add a new one)
# 2. Add an entry with your organization details
# 3. Optionally add a logo file to docs/static/img/logos/
#
# Required fields:
# - name: Your organization name
# - url: Link to your organization's website
#
# Optional fields:
# - logo: Filename of logo in docs/static/img/logos/ (e.g., "mycompany.svg")
# - contributors: List of GitHub usernames who contributed (e.g., ["@username"])
categories:
Sharing Economy:
- name: Airbnb
url: https://github.com/airbnb
- name: Faasos
url: https://faasos.com/
contributors: ["@shashanksingh"]
- name: Free2Move
url: https://www.free2move.com/
contributors: ["@PaoloTerzi"]
- name: Hostnfly
url: https://www.hostnfly.com/
contributors: ["@alexisrosuel"]
- name: Lime
url: https://www.li.me/
contributors: ["@cxmcc"]
- name: Lyft
url: https://www.lyft.com/
- name: Ontruck
url: https://www.ontruck.com/
Financial Services:
- name: Aktia Bank plc
url: https://www.aktia.com
- name: American Express
url: https://www.americanexpress.com
contributors: ["@TheLastSultan"]
- name: bumper
url: https://www.bumper.co/
contributors: ["@vasu-ram", "@JamiePercival"]
- name: Cape Crypto
url: https://capecrypto.com
- name: Capital Service S.A.
url: https://capitalservice.pl
contributors: ["@pkonarzewski"]
- name: Clark.de
url: https://clark.de/
- name: Europace
url: https://europace.de
- name: KarrotPay
url: https://www.daangnpay.com/
- name: Remita
url: https://remita.net
contributors: ["@mujibishola"]
- name: Taveo
url: https://www.taveo.com
contributors: ["@codek"]
- name: Unit
url: https://www.unit.co/about-us
contributors: ["@amitmiran137"]
- name: Wise
url: https://wise.com
contributors: ["@koszti"]
- name: Xendit
url: https://xendit.co/
contributors: ["@LieAlbertTriAdrian"]
- name: Cover Genius
url: https://covergenius.com/
Gaming:
- name: Popoko VM Games Studio
url: https://popoko.live
E-Commerce:
- name: AiHello
url: https://www.aihello.com
contributors: ["@ganeshkrishnan1"]
- name: Bazaar Technologies
url: https://www.bazaartech.com
contributors: ["@umair-abro"]
- name: Dragonpass
url: https://www.dragonpass.com.cn/
contributors: ["@zhxjdwh"]
- name: Dropit Shopping
url: https://www.dropit.shop/
contributors: ["@dropit-dev"]
- name: Fanatics
url: https://www.fanatics.com/
contributors: ["@coderfender"]
- name: Fordeal
url: https://www.fordeal.com
contributors: ["@Renkai"]
- name: Fynd
url: https://www.fynd.com/
contributors: ["@darpanjain07"]
- name: GFG - Global Fashion Group
url: https://global-fashion-group.com
contributors: ["@ksaagariconic"]
- name: GoTo/Gojek
url: https://www.gojek.io/
contributors: ["@gwthm-in"]
- name: HuiShouBao
url: https://www.huishoubao.com/
contributors: ["@Yukinoshita-Yukino"]
- name: Now
url: https://www.now.vn/
contributors: ["@davidkohcw"]
- name: Qunar
url: https://www.qunar.com/
contributors: ["@flametest"]
- name: Rakuten Viki
url: https://www.viki.com
- name: Shopee
url: https://shopee.sg
contributors: ["@xiaohanyu"]
- name: Shopkick
url: https://www.shopkick.com
contributors: ["@LAlbertalli"]
- name: ShopUp
url: https://www.shopup.org/
contributors: ["@gwthm-in"]
- name: Tails.com
url: https://tails.com/gb/
contributors: ["@alanmcruickshank"]
- name: THE ICONIC
url: https://theiconic.com.au/
contributors: ["@ksaagariconic"]
- name: Utair
url: https://www.utair.ru
contributors: ["@utair-digital"]
- name: VkusVill
url: https://vkusvill.ru/
contributors: ["@ETselikov"]
- name: Zalando
url: https://www.zalando.com
contributors: ["@dmigo"]
- name: Zalora
url: https://www.zalora.com
contributors: ["@ksaagariconic"]
- name: Zepto
url: https://www.zeptonow.com/
contributors: ["@gwthm-in"]
Enterprise Technology:
- name: A3Data
url: https://a3data.com.br
contributors: ["@neylsoncrepalde"]
- name: Analytics Aura
url: https://analyticsaura.com/
contributors: ["@Analytics-Aura"]
- name: Apollo GraphQL
url: https://www.apollographql.com/
contributors: ["@evans"]
- name: Astronomer
url: https://www.astronomer.io
contributors: ["@ryw"]
- name: Avesta Technologies
url: https://avestatechnologies.com/
contributors: ["@TheRum"]
- name: Caizin
url: https://caizin.com/
contributors: ["@tejaskatariya"]
- name: Canonical
url: https://canonical.com
- name: Careem
url: https://www.careem.com/
contributors: ["@samraHanif0340"]
- name: Cloudsmith
url: https://cloudsmith.io
contributors: ["@alancarson"]
- name: Cyberhaven
url: https://www.cyberhaven.com/
contributors: ["@toliver-ch"]
- name: Deepomatic
url: https://deepomatic.com/
contributors: ["@Zanoellia"]
- name: Dial Once
url: https://www.dial-once.com/
- name: Dremio
url: https://dremio.com
contributors: ["@narendrans"]
- name: EFinance
url: https://www.efinance.com.eg
contributors: ["@habeeb556"]
- name: Elestio
url: https://elest.io/
contributors: ["@kaiwalyakoparkar"]
- name: ELMO Cloud HR & Payroll
url: https://elmosoftware.com.au/
- name: Endress+Hauser
url: https://www.endress.com/
contributors: ["@rumbin"]
- name: FBK - ICT center
url: https://ict.fbk.eu
- name: Formbricks
url: https://formbricks.com
- name: Gavagai
url: https://gavagai.io
contributors: ["@gavagai-corp"]
- name: GfK Data Lab
url: https://www.gfk.com/home
contributors: ["@mherr"]
- name: HPE
url: https://www.hpe.com/in/en/home.html
contributors: ["@anmol-hpe"]
- name: Hydrolix
url: https://www.hydrolix.io/
- name: Intercom
url: https://www.intercom.com/
contributors: ["@kate-gallo"]
- name: jampp
url: https://jampp.com/
- name: Konfío
url: https://konfio.mx
contributors: ["@uis-rodriguez"]
- name: Mainstrat
url: https://mainstrat.com/
- name: mishmash io
url: https://mishmash.io/
contributors: ["@mishmash-io"]
- name: Myra Labs
url: https://www.myralabs.com/
contributors: ["@viksit"]
- name: Nielsen
url: https://www.nielsen.com/
contributors: ["@amitNielsen"]
- name: Ona
url: https://ona.io
contributors: ["@pld"]
- name: Orange
url: https://www.orange.com
contributors: ["@icsu"]
- name: Oslandia
url: https://oslandia.com
- name: Oxylabs
url: https://oxylabs.io/
contributors: ["@rytis-ulys"]
- name: Peak AI
url: https://www.peak.ai/
contributors: ["@azhar22k"]
- name: PeopleDoc
url: https://www.people-doc.com
contributors: ["@rodo"]
- name: PlaidCloud
url: https://www.plaidcloud.com
- name: Preset, Inc.
url: https://preset.io
logo: preset.svg
contributors: ["@mistercrunch", "@betodealmeida", "@dpgaspar", "@rusackas", "@sadpandajoe", "@Vitor-Avila", "@kgabryje", "@geido", "@eschutho", "@Antonio-RiveroMartnez", "@yousoph"]
- name: PubNub
url: https://pubnub.com
contributors: ["@jzucker2"]
- name: ReadyTech
url: https://www.readytech.io
- name: Reward Gateway
url: https://www.rewardgateway.com
- name: RIADVICE
url: https://riadvice.tn
contributors: ["@riadvice"]
- name: ScopeAI
url: https://www.getscopeai.com
contributors: ["@iloveluce"]
- name: shipmnts
url: https://shipmnts.com
- name: Showmax
url: https://showmax.com
contributors: ["@bobek"]
- name: SingleStore
url: https://www.singlestore.com/
- name: TechAudit
url: https://www.techaudit.info
contributors: ["@ETselikov"]
- name: Tenable
url: https://www.tenable.com
contributors: ["@dflionis"]
- name: Tentacle
url: https://www.linkedin.com/company/tentacle-cmi/
contributors: ["@jdclarke5"]
- name: timbr.ai
url: https://timbr.ai/
contributors: ["@semantiDan"]
- name: Tobii
url: https://www.tobii.com/
contributors: ["@dwa"]
- name: Tooploox
url: https://www.tooploox.com/
contributors: ["@jakubczaplicki"]
- name: Unvired
url: https://unvired.com
contributors: ["@srinisubramanian"]
- name: Virtuoso QA
url: https://www.virtuosoqa.com
- name: Whale
url: https://whale.im
- name: Windsor.ai
url: https://www.windsor.ai/
contributors: ["@octaviancorlade"]
- name: WinWin Network马上赢
url: https://brandct.cn/
contributors: ["@wenbinye"]
- name: Zeta
url: https://www.zeta.tech/
contributors: ["@shaikidris"]
Media & Entertainment:
- name: 6play
url: https://www.6play.fr
contributors: ["@CoryChaplin"]
- name: bilibili
url: https://www.bilibili.com
contributors: ["@Moinheart"]
- name: BurdaForward
url: https://www.burda-forward.de/en/
- name: Douban
url: https://www.douban.com/
contributors: ["@luchuan"]
- name: Kuaishou
url: https://www.kuaishou.com/
contributors: ["@zhaoyu89730105"]
- name: Netflix
url: https://www.netflix.com/
- name: Prensa Iberica
url: https://www.prensaiberica.es/
contributors: ["@zamar-roura"]
- name: TME QQMUSIC/WESING
url: https://www.tencentmusic.com/
contributors: ["@shenyuanli", "@marklaw"]
- name: Xite
url: https://xite.com/
contributors: ["@shashankkoppar"]
- name: Zaihang
url: https://www.zaih.com/
Education:
- name: Aveti Learning
url: https://avetilearning.com/
contributors: ["@TheShubhendra"]
- name: Brilliant.org
url: https://brilliant.org/
- name: Open edX
url: https://openedx.org/
- name: Platzi.com
url: https://platzi.com/
- name: Sunbird
url: https://www.sunbird.org/
contributors: ["@eksteporg"]
- name: The GRAPH Network
url: https://thegraphnetwork.org/
contributors: ["@fccoelho"]
- name: Udemy
url: https://www.udemy.com/
contributors: ["@sungjuly"]
- name: VIPKID
url: https://www.vipkid.com.cn/
contributors: ["@illpanda"]
- name: WikiMedia Foundation
url: https://wikimediafoundation.org
contributors: ["@vg"]
Energy:
- name: Airboxlab
url: https://foobot.io
contributors: ["@antoine-galataud"]
- name: DouroECI
url: https://www.douroeci.com/
contributors: ["@nunohelibeires"]
- name: Safaricom
url: https://www.safaricom.co.ke/
contributors: ["@mmutiso"]
- name: Scoot
url: https://scoot.co/
contributors: ["@haaspt"]
- name: Wattbewerb
url: https://wattbewerb.de/
contributors: ["@wattbewerb"]
Healthcare:
- name: Amino
url: https://amino.com
contributors: ["@shkr"]
- name: Bluesquare
url: https://www.bluesquarehub.com/
contributors: ["@madewulf"]
- name: Care
url: https://www.getcare.io/
contributors: ["@alandao2021"]
- name: Living Goods
url: https://www.livinggoods.org
contributors: ["@chelule"]
- name: Maieutical Labs
url: https://maieuticallabs.it
contributors: ["@xrmx"]
- name: Medic
url: https://medic.org
contributors: ["@1yuv"]
- name: REDCap Cloud
url: https://www.redcapcloud.com/
- name: TrustMedis
url: https://trustmedis.com/
contributors: ["@famasya"]
- name: WeSure
url: https://www.wesure.cn/
- name: 2070Health
url: https://2070health.com/
HR / Staffing:
- name: Swile
url: https://www.swile.co/
contributors: ["@PaoloTerzi"]
- name: Symmetrics
url: https://www.symmetrics.fyi
- name: bluquist
url: https://bluquist.com/
Government:
- name: City of Ann Arbor, MI
url: https://www.a2gov.org/
contributors: ["@sfirke"]
- name: RIS3 Strategy of CZ, MIT CR
url: https://www.ris3.cz/
contributors: ["@RIS3CZ"]
- name: NRLM - Sarathi, India
url: https://pib.gov.in/PressReleasePage.aspx?PRID=1999586
Travel:
- name: Agoda
url: https://www.agoda.com/
contributors: ["@lostseaway", "@maiake", "@obombayo"]
- name: HomeToGo
url: https://hometogo.com/
contributors: ["@pedromartinsteenstrup"]
- name: Skyscanner
url: https://www.skyscanner.net/
contributors: ["@cleslie", "@stanhoucke"]
Others:
- name: 10Web
url: https://10web.io/
- name: AI inside
url: https://inside.ai/en/
- name: Automattic
url: https://automattic.com/
contributors: ["@Khrol", "@Usiel"]
- name: Dropbox
url: https://www.dropbox.com/
contributors: ["@bkyryliuk"]
- name: Flowbird
url: https://flowbird.com
contributors: ["@EmmanuelCbd"]
- name: GEOTAB
url: https://www.geotab.com
contributors: ["@JZ6"]
- name: Grassroot
url: https://www.grassrootinstitute.org/
- name: Increff
url: https://www.increff.com/
contributors: ["@ishansinghania"]
- name: komoot
url: https://www.komoot.com/
contributors: ["@christophlingg"]
- name: Let's Roam
url: https://www.letsroam.com/
- name: Machrent SA
url: https://www.machrent.com/
- name: Onebeat
url: https://1beat.com/
contributors: ["@GuyAttia"]
- name: X
url: https://x.com/
- name: VLMedia
url: https://www.vlmedia.com.tr/
contributors: ["@ibotheperfect"]
- name: Yahoo!
url: https://yahoo.com/

View File

@@ -28,10 +28,12 @@ This page serves as a registry of community-created Superset extensions. These e
## Extensions
| Name | Description | Author | Preview |
| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Extensions API Explorer](https://github.com/michael-s-molina/superset-extensions/tree/main/api_explorer) | A SQL Lab panel that demonstrates the Extensions API by providing an interactive explorer for testing commands like getTabs, getCurrentTab, and getDatabases. Useful for extension developers to understand and experiment with the available APIs. | Michael S. Molina | <a href="/img/extensions/api_explorer.png" target="_blank"><img src="/img/extensions/api_explorer.png" alt="Extensions API Explorer" width="120" /></a> |
| [SQL Query Flow Visualizer](https://github.com/msyavuz/superset-sql-visualizer) | A SQL Lab panel that transforms SQL queries into interactive flow diagrams, helping developers and analysts understand query execution paths and data relationships.| Mehmet Salih Yavuz | <a href="/img/extensions/sql_flow_visualizer.png" target="_blank"><img src="/img/extensions/sql_flow_visualizer.png" alt="SQL Flow Visualizer" width="120" /></a> |
| Name | Description | Author | Preview |
| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Extensions API Explorer](https://github.com/michael-s-molina/superset-extensions/tree/main/api_explorer) | A SQL Lab panel that demonstrates the Extensions API by providing an interactive explorer for testing commands like getTabs, getCurrentTab, and getDatabases. Useful for extension developers to understand and experiment with the available APIs. | Michael S. Molina | <a href="/img/extensions/api-explorer.png" target="_blank"><img src="/img/extensions/api-explorer.png" alt="Extensions API Explorer" width="120" /></a> |
| [SQL Query Flow Visualizer](https://github.com/msyavuz/superset-sql-visualizer) | A SQL Lab panel that transforms SQL queries into interactive flow diagrams, helping developers and analysts understand query execution paths and data relationships. | Mehmet Salih Yavuz | <a href="/img/extensions/sql-flow-visualizer.png" target="_blank"><img src="/img/extensions/sql-flow-visualizer.png" alt="SQL Flow Visualizer" width="120" /></a> |
| [SQL Lab Export to Google Sheets](https://github.com/michael-s-molina/superset-extensions/tree/main/sqllab_gsheets) | A Superset extension that allows users to export SQL Lab query results directly to Google Sheets. | Michael S. Molina | <a href="/img/extensions/gsheets-export.png" target="_blank"><img src="/img/extensions/gsheets-export.png" alt="SQL Lab Export to Google Sheets" width="120" /></a> |
## How to Add Your Extension
To add your extension to this registry, submit a pull request to the [Apache Superset repository](https://github.com/apache/superset) with the following changes:

View File

@@ -429,6 +429,14 @@ const config: Config = {
label: 'Stack Overflow',
href: 'https://stackoverflow.com/questions/tagged/apache-superset',
},
{
label: 'Community Calendar',
href: '/community#superset-community-calendar',
},
{
label: 'In the Wild',
href: '/inTheWild',
},
],
},
...dynamicNavbarItems,

View File

@@ -51,9 +51,11 @@
"@storybook/preview-api": "^8.6.11",
"@storybook/theming": "^8.6.11",
"@superset-ui/core": "^0.20.4",
"antd": "^6.1.0",
"antd": "^6.1.1",
"caniuse-lite": "^1.0.30001760",
"docusaurus-plugin-less": "^2.0.2",
"js-yaml": "^4.1.1",
"js-yaml-loader": "^1.2.2",
"json-bigint": "^1.0.0",
"less": "^4.5.1",
"less-loader": "^12.3.0",
@@ -74,9 +76,10 @@
"@docusaurus/module-type-aliases": "^3.9.1",
"@docusaurus/tsconfig": "^3.9.2",
"@eslint/js": "^9.39.2",
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^8.37.0",
"@typescript-eslint/parser": "^8.49.0",
"@typescript-eslint/parser": "^8.50.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.3",
@@ -84,8 +87,8 @@
"globals": "^16.5.0",
"prettier": "^3.7.4",
"typescript": "~5.9.3",
"typescript-eslint": "^8.49.0",
"webpack": "^5.103.0"
"typescript-eslint": "^8.50.0",
"webpack": "^5.104.0"
},
"browserslist": {
"production": [

View File

@@ -49,9 +49,23 @@ const BADGE_PATH_PATTERNS = [
// Cache for downloaded badges (persists across files in a single build)
const badgeCache = new Map();
// Track in-flight downloads to prevent duplicate concurrent requests
const inFlightDownloads = new Map();
// Track if we've already ensured the badges directory exists
let badgesDirCreated = false;
// Retry configuration
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 1000;
/**
* Sleep for a given number of milliseconds
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Generate a stable filename for a badge URL
*/
@@ -74,21 +88,61 @@ function isBadgeUrl(url) {
try {
const parsed = new URL(url);
// Check if it's from a known badge domain
if (BADGE_DOMAINS.some((domain) => parsed.hostname.includes(domain))) {
if (BADGE_DOMAINS.some(domain => parsed.hostname.includes(domain))) {
return true;
}
// Check if it matches a badge path pattern
return BADGE_PATH_PATTERNS.some((pattern) => pattern.test(url));
return BADGE_PATH_PATTERNS.some(pattern => pattern.test(url));
} catch {
return false;
}
}
/**
* Fetch a badge with retry logic
*/
async function fetchWithRetry(url, retries = MAX_RETRIES) {
let lastError;
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch(url, {
headers: {
// Some services need a user agent
'User-Agent': 'Mozilla/5.0 (compatible; DocusaurusBuild/1.0)',
Accept: 'image/svg+xml,image/*,*/*',
},
// Follow redirects
redirect: 'follow',
// Add timeout to prevent hanging
signal: AbortSignal.timeout(30000),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response;
} catch (error) {
lastError = error;
if (attempt < retries) {
const delay = RETRY_DELAY_MS * attempt; // Exponential backoff
console.log(
`[remark-localize-badges] Retry ${attempt}/${retries} for ${url} after ${delay}ms...`,
);
await sleep(delay);
}
}
}
throw lastError;
}
/**
* Download a badge and return the local path
*/
async function downloadBadge(url, staticDir) {
// Check cache first
// Check memory cache first
if (badgeCache.has(url)) {
return badgeCache.get(url);
}
@@ -105,58 +159,67 @@ async function downloadBadge(url, staticDir) {
const localPath = path.join(badgesDir, filename);
const webPath = `/badges/${filename}`;
// Check if already downloaded in a previous build
// Check if already downloaded in a previous build or by another concurrent request
if (fs.existsSync(localPath)) {
badgeCache.set(url, webPath);
return webPath;
}
console.log(`[remark-localize-badges] Downloading: ${url}`);
try {
const response = await fetch(url, {
headers: {
// Some services need a user agent
'User-Agent': 'Mozilla/5.0 (compatible; DocusaurusBuild/1.0)',
Accept: 'image/svg+xml,image/*,*/*',
},
// Follow redirects
redirect: 'follow',
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get('content-type') || '';
const content = await response.text();
// Validate it's actually an SVG or image
if (
!contentType.includes('svg') &&
!contentType.includes('image') &&
!content.trim().startsWith('<svg') &&
!content.trim().startsWith('<?xml')
) {
throw new Error(
`Invalid content type: ${contentType}. Expected SVG image.`,
);
}
// Write the badge to disk
fs.writeFileSync(localPath, content, 'utf8');
console.log(`[remark-localize-badges] Saved: ${filename}`);
badgeCache.set(url, webPath);
return webPath;
} catch (error) {
// Fail the build on badge download failure
throw new Error(
`[remark-localize-badges] Failed to download badge: ${url}\n` +
`Error: ${error.message}\n` +
`Build cannot continue with broken badges. Please fix the badge URL or remove it.`,
);
// Check if there's already an in-flight download for this URL
// This prevents duplicate concurrent downloads of the same badge
if (inFlightDownloads.has(url)) {
return inFlightDownloads.get(url);
}
// Create the download promise and store it
const downloadPromise = (async () => {
// Double-check file existence after acquiring the "lock"
if (fs.existsSync(localPath)) {
badgeCache.set(url, webPath);
return webPath;
}
console.log(`[remark-localize-badges] Downloading: ${url}`);
try {
const response = await fetchWithRetry(url);
const contentType = response.headers.get('content-type') || '';
const content = await response.text();
// Validate it's actually an SVG or image
if (
!contentType.includes('svg') &&
!contentType.includes('image') &&
!content.trim().startsWith('<svg') &&
!content.trim().startsWith('<?xml')
) {
throw new Error(
`Invalid content type: ${contentType}. Expected SVG image.`,
);
}
// Write the badge to disk
fs.writeFileSync(localPath, content, 'utf8');
console.log(`[remark-localize-badges] Saved: ${filename}`);
badgeCache.set(url, webPath);
return webPath;
} catch (error) {
// Fail the build on badge download failure
throw new Error(
`[remark-localize-badges] Failed to download badge: ${url}\n` +
`Error: ${error.message}\n` +
`Build cannot continue with broken badges. Please fix the badge URL or remove it.`,
);
} finally {
// Clean up the in-flight tracker
inFlightDownloads.delete(url);
}
})();
inFlightDownloads.set(url, downloadPromise);
return downloadPromise;
}
/**
@@ -168,15 +231,14 @@ export default function remarkLocalizeBadges(options = {}) {
const docsRoot = path.resolve(currentDir, '..');
const staticDir = options.staticDir || path.join(docsRoot, 'static');
return async function transformer(tree) {
const promises = [];
// Find all image nodes
visit(tree, 'image', (node) => {
visit(tree, 'image', node => {
if (isBadgeUrl(node.url)) {
promises.push(
downloadBadge(node.url, staticDir).then((localPath) => {
downloadBadge(node.url, staticDir).then(localPath => {
node.url = localPath;
}),
);
@@ -184,7 +246,7 @@ export default function remarkLocalizeBadges(options = {}) {
});
// Also handle HTML img tags in raw HTML or JSX
visit(tree, ['html', 'jsx'], (node) => {
visit(tree, ['html', 'jsx'], node => {
if (!node.value) return;
// Find img src attributes pointing to badge URLs
@@ -195,7 +257,7 @@ export default function remarkLocalizeBadges(options = {}) {
const url = match[1];
if (isBadgeUrl(url)) {
promises.push(
downloadBadge(url, staticDir).then((localPath) => {
downloadBadge(url, staticDir).then(localPath => {
node.value = node.value.replace(url, localPath);
}),
);
@@ -204,12 +266,12 @@ export default function remarkLocalizeBadges(options = {}) {
});
// Also handle markdown link images: [![alt](img-url)](link-url)
visit(tree, 'link', (node) => {
visit(tree, 'link', node => {
if (node.children) {
node.children.forEach((child) => {
node.children.forEach(child => {
if (child.type === 'image' && isBadgeUrl(child.url)) {
promises.push(
downloadBadge(child.url, staticDir).then((localPath) => {
downloadBadge(child.url, staticDir).then(localPath => {
child.url = localPath;
}),
);

View File

@@ -0,0 +1,165 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Layout from '@theme/Layout';
import { Avatar, Card, Col, Collapse, Row, Typography } from 'antd';
import BlurredSection from '../components/BlurredSection';
import SectionHeader from '../components/SectionHeader';
import DataSet from '../../../RESOURCES/INTHEWILD.yaml';
const { Text, Link } = Typography;
interface Organization {
name: string;
url: string;
logo?: string;
contributors?: string[];
}
interface DataSetType {
categories: Record<string, Organization[]>;
}
const typedDataSet = DataSet as DataSetType;
const ContributorAvatars = ({ contributors }: { contributors?: string[] }) => {
if (!contributors?.length) return null;
return (
<Avatar.Group size="small" max={{ count: 3 }}>
{contributors.map((handle) => {
const username = handle.replace('@', '');
return (
<a
key={username}
href={`https://github.com/${username}`}
target="_blank"
rel="noreferrer"
onClick={(e) => e.stopPropagation()}
>
<Avatar
src={`https://github.com/${username}.png?size=40`}
alt={username}
style={{ cursor: 'pointer' }}
>
{username.charAt(0).toUpperCase()}
</Avatar>
</a>
);
})}
</Avatar.Group>
);
};
export default function InTheWild() {
return (
<Layout title="In the Wild" description="Organizations using Apache Superset">
<main>
<BlurredSection>
<SectionHeader
level="h2"
title="In the Wild"
subtitle="See who's using Superset and join our growing community"
/>
<div style={{ textAlign: 'center', marginTop: 10 }}>
<Link
href="https://github.com/apache/superset/edit/master/RESOURCES/INTHEWILD.yaml"
target="_blank"
>
Add your name/org!
</Link>
</div>
</BlurredSection>
<div style={{ maxWidth: 850, margin: '70px auto 60px', padding: '0 20px' }}>
<Collapse
bordered={false}
defaultActiveKey={Object.keys(typedDataSet.categories)}
style={{
background: 'var(--ifm-background-color)',
border: '1px solid var(--ifm-border-color)',
borderRadius: 10,
}}
items={Object.entries(typedDataSet.categories).map(([category, items]) => {
const logoItems = items.filter(({ logo }) => logo?.trim());
const textItems = items.filter(({ logo }) => !logo?.trim());
return {
key: category,
label: (
<Text strong style={{ fontSize: 16, lineHeight: '22px' }}>
{category} ({items.length})
</Text>
),
children: (
<>
{logoItems.length > 0 && (
<Row gutter={[16, 16]} style={{ marginBottom: textItems.length > 0 ? 24 : 0 }}>
{logoItems.map(({ name, url, logo, contributors }) => (
<Col xs={24} sm={12} md={8} key={name}>
<a href={url} target="_blank" rel="noreferrer">
<Card
hoverable
style={{ height: 150, position: 'relative' }}
styles={{ body: { padding: 16, height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' } }}
>
<img
src={`/img/logos/${logo}`}
alt={name}
style={{ maxHeight: 80, maxWidth: '100%', objectFit: 'contain' }}
/>
{contributors?.length && (
<div style={{ position: 'absolute', bottom: 8, right: 8 }}>
<ContributorAvatars contributors={contributors} />
</div>
)}
</Card>
</a>
</Col>
))}
</Row>
)}
{textItems.length > 0 && (
<Row gutter={[8, 8]}>
{textItems.map(({ name, url, contributors }) => (
<Col xs={24} sm={12} md={8} key={name}>
<a href={url} target="_blank" rel="noreferrer">
<Card
size="small"
hoverable
styles={{ body: { padding: '8px 12px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } }}
>
<Text ellipsis style={{ flex: 1 }}>{name}</Text>
<ContributorAvatars contributors={contributors} />
</Card>
</a>
</Col>
))}
</Row>
)}
</>
),
};
})}
/>
</div>
</main>
</Layout>
);
}

View File

@@ -19,15 +19,43 @@
import { useRef, useState, useEffect, JSX } from 'react';
import Layout from '@theme/Layout';
import Link from '@docusaurus/Link';
import { Carousel } from 'antd';
import { Card, Carousel, Flex } from 'antd';
import styled from '@emotion/styled';
import GitHubButton from 'react-github-btn';
import { mq } from '../utils';
import { Databases } from '../resources/data';
import SectionHeader from '../components/SectionHeader';
import BlurredSection from '../components/BlurredSection';
import DataSet from '../../../RESOURCES/INTHEWILD.yaml';
import '../styles/main.less';
interface Organization {
name: string;
url: string;
logo?: string;
}
interface DataSetType {
categories: Record<string, Organization[]>;
}
const typedDataSet = DataSet as DataSetType;
// Extract all organizations with logos for the carousel
const companiesWithLogos = Object.values(typedDataSet.categories)
.flat()
.filter((org) => org.logo?.trim());
// Fisher-Yates shuffle for fair randomization
function shuffleArray<T>(array: T[]): T[] {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
const features = [
{
image: 'powerful-yet-easy.jpg',
@@ -452,6 +480,7 @@ export default function Home(): JSX.Element {
const slider = useRef(null);
const [slideIndex, setSlideIndex] = useState(0);
const [shuffledCompanies, setShuffledCompanies] = useState(companiesWithLogos);
const onChange = (current, next) => {
setSlideIndex(next);
@@ -479,6 +508,11 @@ export default function Home(): JSX.Element {
}
};
// Shuffle companies on mount for fair rotation
useEffect(() => {
setShuffledCompanies(shuffleArray(companiesWithLogos));
}, []);
// Set up dark <-> light navbar change
useEffect(() => {
changeToDark();
@@ -747,6 +781,74 @@ export default function Home(): JSX.Element {
</span>
</StyledIntegrations>
</BlurredSection>
{/* Only show carousel when we have enough logos (>10) for a good display */}
{companiesWithLogos.length > 10 && (
<BlurredSection>
<div style={{ padding: '0 20px' }}>
<SectionHeader
level="h2"
title="Trusted by teams everywhere"
subtitle="Join thousands of companies using Superset to explore and visualize their data"
/>
<div style={{ maxWidth: 1160, margin: '25px auto 0' }}>
<Carousel
autoplay
autoplaySpeed={2000}
slidesToShow={6}
slidesToScroll={1}
dots={false}
responsive={[
{ breakpoint: 1024, settings: { slidesToShow: 4 } },
{ breakpoint: 768, settings: { slidesToShow: 3 } },
{ breakpoint: 480, settings: { slidesToShow: 2 } },
]}
>
{shuffledCompanies.map(({ name, url, logo }) => (
<div key={name}>
<a
href={url}
target="_blank"
rel="noreferrer"
aria-label={`Visit ${name}`}
>
<Card
style={{ margin: '0 8px' }}
styles={{
body: {
height: 80,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 16,
},
}}
>
<img
src={`/img/logos/${logo}`}
alt={name}
title={name}
style={{ maxHeight: 48, maxWidth: '100%', objectFit: 'contain' }}
/>
</Card>
</a>
</div>
))}
</Carousel>
</div>
<Flex justify="center" style={{ marginTop: 30, fontSize: 17 }}>
<Link to="/inTheWild">See all companies</Link>
<span style={{ margin: '0 8px' }}>·</span>
<a
href="https://github.com/apache/superset/edit/master/RESOURCES/INTHEWILD.yaml"
target="_blank"
rel="noreferrer"
>
Add yours to the list!
</a>
</Flex>
</div>
</BlurredSection>
)}
</StyledMain>
</Layout>
);

View File

@@ -19,6 +19,17 @@
import { useEffect } from 'react';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
// File extensions to track as downloads
const DOWNLOAD_EXTENSIONS = [
'pdf', 'zip', 'tar', 'gz', 'tgz', 'bz2',
'exe', 'dmg', 'pkg', 'deb', 'rpm',
'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
'csv', 'json', 'yaml', 'yml',
];
// Scroll depth milestones to track
const SCROLL_MILESTONES = [25, 50, 75, 100];
export default function Root({ children }) {
const { siteConfig } = useDocusaurusContext();
const { customFields } = siteConfig;
@@ -27,10 +38,9 @@ export default function Root({ children }) {
const { matomoUrl, matomoSiteId } = customFields;
if (typeof window !== 'undefined') {
// Making testing easier, logging debug junk if we're in development
const devMode = window.location.hostname === 'localhost' ? true : false;
const devMode = ['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(window.location.hostname);
// Initialize the _paq array first
// Initialize the _paq array
window._paq = window._paq || [];
// Configure the tracker before loading matomo.js
@@ -39,7 +49,8 @@ export default function Root({ children }) {
window._paq.push(['setTrackerUrl', `${matomoUrl}/matomo.php`]);
window._paq.push(['setSiteId', matomoSiteId]);
// Initial page view is handled by handleRouteChange
// Track downloads with custom extensions
window._paq.push(['setDownloadExtensions', DOWNLOAD_EXTENSIONS.join('|')]);
// Now load the matomo.js script
const script = document.createElement('script');
@@ -47,19 +58,168 @@ export default function Root({ children }) {
script.src = `${matomoUrl}/matomo.js`;
document.head.appendChild(script);
// Helper to track events
const trackEvent = (category, action, name, value) => {
if (devMode) {
console.log('Matomo trackEvent:', { category, action, name, value });
}
window._paq.push(['trackEvent', category, action, name, value]);
};
// Helper to track site search
const trackSiteSearch = (keyword, category, resultsCount) => {
if (devMode) {
console.log('Matomo trackSiteSearch:', { keyword, category, resultsCount });
}
window._paq.push(['trackSiteSearch', keyword, category, resultsCount]);
};
// Track external link clicks using domain as category (vendor-agnostic)
const handleLinkClick = (event) => {
const link = event.target.closest('a');
if (!link) return;
const href = link.getAttribute('href');
if (!href) return;
try {
const url = new URL(href, window.location.origin);
// Skip internal links
if (url.hostname === window.location.hostname) return;
// Use hostname as category for vendor-agnostic tracking
trackEvent('Outbound Link', url.hostname, href);
} catch {
// Invalid URL, skip tracking
}
};
// Track Algolia search queries
const setupAlgoliaTracking = () => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const searchInput = node.querySelector?.('.DocSearch-Input') ||
(node.classList?.contains('DocSearch-Input') ? node : null);
if (searchInput) {
let debounceTimer;
searchInput.addEventListener('input', (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const query = e.target.value.trim();
if (query.length >= 3) {
const results = document.querySelectorAll('.DocSearch-Hit');
trackSiteSearch(query, 'Documentation', results.length);
}
}, 1000);
});
}
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
return observer;
};
// Track video plays
const handleVideoPlay = (event) => {
if (event.target.tagName === 'VIDEO') {
const videoSrc = event.target.currentSrc || event.target.src || 'unknown';
trackEvent('Video', 'Play', videoSrc);
}
};
// Track CTA button clicks
const handleCTAClick = (event) => {
const button = event.target.closest('.get-started-button, .default-button-theme');
if (button) {
const buttonText = button.textContent?.trim() || 'Unknown';
const href = button.getAttribute('href') || '';
trackEvent('CTA', 'Click', `${buttonText} - ${href}`);
}
};
// Track scroll depth
let scrollMilestonesReached = new Set();
const handleScroll = () => {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
if (docHeight <= 0) return;
const scrollPercent = Math.round((scrollTop / docHeight) * 100);
SCROLL_MILESTONES.forEach(milestone => {
if (scrollPercent >= milestone && !scrollMilestonesReached.has(milestone)) {
scrollMilestonesReached.add(milestone);
trackEvent('Scroll Depth', `${milestone}%`, window.location.pathname);
}
});
};
// Reset scroll tracking on route change
const resetScrollTracking = () => {
scrollMilestonesReached = new Set();
};
// Track 404 pages
const track404 = () => {
const is404 = document.querySelector('.theme-doc-404') ||
document.title.toLowerCase().includes('not found') ||
document.querySelector('h1')?.textContent?.toLowerCase().includes('not found');
if (is404) {
trackEvent('Error', '404', window.location.pathname);
if (devMode) {
console.log('Matomo: 404 page detected', window.location.pathname);
}
}
};
// Track copy-to-clipboard events on code blocks
const handleCopy = (event) => {
const codeBlock = event.target.closest('pre, code, .prism-code');
if (codeBlock) {
const codeText = window.getSelection()?.toString() || '';
const codeSnippet = codeText.substring(0, 100) + (codeText.length > 100 ? '...' : '');
trackEvent('Code', 'Copy', `${window.location.pathname}: ${codeSnippet}`);
}
};
// Track color mode preference (as event, no admin config needed)
const trackColorMode = () => {
const colorMode = document.documentElement.getAttribute('data-theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
trackEvent('User Preference', 'Color Mode', colorMode);
};
// Track docs version from URL (as event, no admin config needed)
const trackDocsVersion = () => {
const pathMatch = window.location.pathname.match(/\/docs\/([\d.]+)\//);
const version = pathMatch ? pathMatch[1] : 'latest';
trackEvent('User Preference', 'Docs Version', version);
};
// Handle route changes for SPA
const handleRouteChange = () => {
if (devMode) {
console.log('Route changed to:', window.location.pathname);
}
// Short timeout to ensure the page has fully rendered
// Reset scroll tracking for new page
resetScrollTracking();
setTimeout(() => {
// Get the current page title from the document
const currentTitle = document.title;
const currentPath = window.location.pathname;
// For testing: impersonate real domain - ONLY FOR DEVELOPMENT
// Set custom dimensions before tracking page view
trackColorMode();
trackDocsVersion();
if (devMode) {
console.log('Tracking page view:', currentPath, currentTitle);
window._paq.push(['setDomains', ['superset.apache.org']]);
@@ -74,10 +234,13 @@ export default function Root({ children }) {
window._paq.push(['setReferrerUrl', window.location.href]);
window._paq.push(['setDocumentTitle', currentTitle]);
window._paq.push(['trackPageView']);
}, 100); // Increased delay to ensure page has fully rendered
// Check for 404 after page renders
setTimeout(track404, 500);
}, 100);
};
// Try all possible Docusaurus events - they've changed between versions
// Set up Docusaurus route listeners
const possibleEvents = [
'docusaurus.routeDidUpdate',
'docusaurusRouteDidUpdate',
@@ -85,21 +248,22 @@ export default function Root({ children }) {
];
if (devMode) {
console.log('Setting up Docusaurus route listeners');
console.log('Setting up Matomo tracking with enhanced features');
}
possibleEvents.forEach(eventName => {
document.addEventListener(eventName, () => {
// Store handler references for proper cleanup
const routeHandlers = possibleEvents.map(eventName => {
const handler = () => {
if (devMode) {
console.log(`Docusaurus route update detected via ${eventName}`);
}
handleRouteChange();
});
};
document.addEventListener(eventName, handler);
return { eventName, handler };
});
// Also set up manual history tracking as fallback
if (devMode) {
console.log('Setting up manual history tracking as fallback');
}
// Manual history tracking as fallback
const originalPushState = window.history.pushState;
window.history.pushState = function () {
originalPushState.apply(this, arguments);
@@ -108,19 +272,53 @@ export default function Root({ children }) {
window.addEventListener('popstate', handleRouteChange);
// Set up event listeners
document.addEventListener('click', handleLinkClick);
document.addEventListener('click', handleCTAClick);
document.addEventListener('play', handleVideoPlay, true);
document.addEventListener('copy', handleCopy);
window.addEventListener('scroll', handleScroll, { passive: true });
// Watch for color mode changes
const colorModeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
trackEvent('User Preference', 'Color Mode Change',
document.documentElement.getAttribute('data-theme'));
}
});
});
colorModeObserver.observe(document.documentElement, { attributes: true });
// Set up Algolia tracking
const algoliaObserver = setupAlgoliaTracking();
// Initial page tracking
handleRouteChange();
// Cleanup
return () => {
// Cleanup listeners
possibleEvents.forEach(eventName => {
document.removeEventListener(eventName, handleRouteChange);
routeHandlers.forEach(({ eventName, handler }) => {
document.removeEventListener(eventName, handler);
});
if (originalPushState) {
window.history.pushState = originalPushState;
window.removeEventListener('popstate', handleRouteChange);
}
document.removeEventListener('click', handleLinkClick);
document.removeEventListener('click', handleCTAClick);
document.removeEventListener('play', handleVideoPlay, true);
document.removeEventListener('copy', handleCopy);
window.removeEventListener('scroll', handleScroll);
if (algoliaObserver) {
algoliaObserver.disconnect();
}
if (colorModeObserver) {
colorModeObserver.disconnect();
}
};
}
}, []);

28
docs/src/types/yaml.d.ts vendored Normal file
View File

@@ -0,0 +1,28 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
declare module '*.yaml' {
const content: unknown;
export default content;
}
declare module '*.yml' {
const content: unknown;
export default content;
}

View File

@@ -25,6 +25,13 @@ export default function webpackExtendPlugin(): Plugin<void> {
name: 'custom-webpack-plugin',
configureWebpack(config) {
const isDev = process.env.NODE_ENV === 'development';
// Add YAML loader rule directly to existing rules
config.module?.rules?.push({
test: /\.ya?ml$/,
use: 'js-yaml-loader',
});
return {
devtool: isDev ? 'eval-source-map' : config.devtool,
...(isDev && {

View File

Before

Width:  |  Height:  |  Size: 476 KiB

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

View File

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

19
docs/static/img/logos/preset.svg vendored Normal file
View File

@@ -0,0 +1,19 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 494.72 148.76"><defs><style>.cls-1{fill:#2fc096;}</style></defs><title>preset-logo</title><path class="cls-1" d="M91,51.77H43.11L0,145H27.81L59.3,77.14H91a12.69,12.69,0,0,0,0-25.37ZM91,0H61.28L49.51,25.37H91a39.08,39.08,0,0,1,0,78.16H74.8L63,128.84a12.1,12.1,0,0,0,1.21.06H91A64.45,64.45,0,0,0,91,0Z"/><path d="M205,38.46c-14.41,0-24.47,10.31-24.47,25.07V146a2.66,2.66,0,0,0,2.75,2.75h5.62a2.66,2.66,0,0,0,2.75-2.75V114.25a20.32,20.32,0,0,0,14.72,5.65c13.17,0,23.1-10.78,23.1-25.08V63.53C229.5,48.77,219.44,38.46,205,38.46ZM191.68,63.53c0-8.66,5.24-14.26,13.35-14.26s13.35,5.6,13.35,14.26V94.82c0,8.66-5.24,14.26-13.35,14.26s-13.35-5.6-13.35-14.26Z"/><path d="M272.15,39.52h-6.68c-14.53,0-25.08,10.35-25.08,24.62v51.49a2.66,2.66,0,0,0,2.75,2.75h5.62a2.67,2.67,0,0,0,2.76-2.75V64.14c0-8.51,5.34-13.8,14-13.8h6.68a2.67,2.67,0,0,0,2.76-2.76V42.27A2.66,2.66,0,0,0,272.15,39.52Z"/><path d="M304.2,86.64c14.17,0,24.47-9.85,24.47-23.41,0-14.5-10.07-24.63-24.47-24.63-14.64,0-24.47,10.14-24.47,25.24V94.67c0,15,9.83,25.08,24.47,25.08,14.06,0,23.84-10.06,24.32-25.08a2.66,2.66,0,0,0-2.76-2.75h-5.62a2.57,2.57,0,0,0-2.75,2.71c-.28,8.69-5.46,14.3-13.19,14.3-8.11,0-13.35-5.6-13.35-14.26v-8Zm-13.35-22.8c0-9,5-14.41,13.35-14.41,8.1,0,13.34,5.41,13.34,13.8,0,7.64-5.24,12.59-13.34,12.59H290.85Z"/><path d="M363.68,73.69c-8.11-3.42-15.12-6.37-15.12-13.5,0-6.53,5-10.92,12.44-10.92S373.13,53.94,373.58,62a2.59,2.59,0,0,0,2.76,2.75H382A2.66,2.66,0,0,0,384.7,62c-.67-14.94-9.47-23.5-24.16-23.5-13.17,0-23.11,9.34-23.11,21.73,0,14.52,11.48,19.4,21.6,23.7,8.38,3.56,15.62,6.64,15.62,14.27,0,6.94-4.54,10.92-12.44,10.92-9.28,0-14-4.68-14.41-14.26a2.58,2.58,0,0,0-2.75-2.75h-5.62a2.66,2.66,0,0,0-2.75,2.75v.05c.65,15.44,10.43,25,25.53,25,13.43,0,23.56-9.35,23.56-21.74C385.77,83,374,78.05,363.68,73.69Z"/><path d="M418.55,86.64c14.18,0,24.47-9.85,24.47-23.41C443,48.73,433,38.6,418.55,38.6c-14.64,0-24.47,10.14-24.47,25.24V94.67c0,15,9.83,25.08,24.47,25.08,14.07,0,23.84-10.06,24.32-25.08a2.66,2.66,0,0,0-2.75-2.75H434.5a2.57,2.57,0,0,0-2.75,2.71c-.29,8.69-5.47,14.3-13.2,14.3-8.11,0-13.35-5.6-13.35-14.26v-8ZM405.2,63.84c0-9,5-14.41,13.35-14.41,8.11,0,13.35,5.41,13.35,13.8,0,7.64-5.24,12.59-13.35,12.59H405.2Z"/><path d="M478.14,108.05h-2c-7.65,0-11.53-5.36-11.53-15.93V50.83h11.68a2.67,2.67,0,0,0,2.75-2.76V42.91a2.66,2.66,0,0,0-2.75-2.75H464.64V21.65a2.67,2.67,0,0,0-2.75-2.76h-5.62a2.67,2.67,0,0,0-2.75,2.76V92.12c0,16.75,8.46,26.75,22.65,26.75h2a2.65,2.65,0,0,0,2.75-2.75V110.8A2.58,2.58,0,0,0,478.14,108.05Z"/><path d="M484,27h-1.91V25.85h5.18V27h-1.91v5.17H484Z"/><path d="M493.41,29.77c0-1.07,0-2.27,0-3h0c-.3,1.28-.93,3.37-1.53,5.34h-1.16c-.46-1.72-1.11-4.1-1.38-5.36h0c.06.74.08,2,.08,3.11v2.25h-1.24V25.85h2c.49,1.64,1,3.7,1.23,4.63h0c.15-.82.84-3,1.37-4.63h2v6.28h-1.31Z"/></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

64
docs/static/llms.txt vendored Normal file
View File

@@ -0,0 +1,64 @@
# Apache Superset
> Apache Superset is a modern, enterprise-ready business intelligence web application. It provides a no-code interface for building charts, a powerful SQL editor, support for nearly any SQL database, and beautiful visualizations. Superset is open source under the Apache 2.0 license.
Superset is designed for data exploration and visualization at scale. It features a lightweight semantic layer, configurable caching, extensible security with RBAC, a REST API for programmatic access, and a cloud-native architecture.
## Getting Started
- [Introduction](https://superset.apache.org/docs/intro): Overview of Superset features, supported databases, and resources
- [Quickstart](https://superset.apache.org/docs/quickstart): Get Superset running quickly with Docker Compose
## Installation
- [Architecture](https://superset.apache.org/docs/installation/architecture): Production deployment architecture and components
- [Docker Compose](https://superset.apache.org/docs/installation/docker-compose): Install Superset using Docker Compose
- [Docker Builds](https://superset.apache.org/docs/installation/docker-builds): Building and customizing Docker images
- [Kubernetes](https://superset.apache.org/docs/installation/kubernetes): Deploy Superset on Kubernetes with Helm
- [PyPI](https://superset.apache.org/docs/installation/pypi): Install from PyPI using pip
- [Upgrading Superset](https://superset.apache.org/docs/installation/upgrading-superset): Upgrade between Superset versions
## Configuration
- [Configuring Superset](https://superset.apache.org/docs/configuration/configuring-superset): Main configuration options and superset_config.py
- [Databases](https://superset.apache.org/docs/configuration/databases): Connect to databases with SQLAlchemy connection strings
- [Caching](https://superset.apache.org/docs/configuration/cache): Configure caching with Redis or other backends
- [Async Queries & Celery](https://superset.apache.org/docs/configuration/async-queries-celery): Set up async query execution with Celery
- [Alerts & Reports](https://superset.apache.org/docs/configuration/alerts-reports): Configure email/Slack alerts and scheduled reports
- [SQL Templating](https://superset.apache.org/docs/configuration/sql-templating): Use Jinja templates in SQL queries
- [Theming](https://superset.apache.org/docs/configuration/theming): Customize Superset's appearance
- [Networking Settings](https://superset.apache.org/docs/configuration/networking-settings): CORS, proxy, and network configuration
- [Timezones](https://superset.apache.org/docs/configuration/timezones): Timezone handling configuration
- [Event Logging](https://superset.apache.org/docs/configuration/event-logging): Configure event and analytics logging
- [Importing/Exporting Datasources](https://superset.apache.org/docs/configuration/importing-exporting-datasources): Import and export datasource definitions
## Using Superset
- [Creating Your First Dashboard](https://superset.apache.org/docs/using-superset/creating-your-first-dashboard): Step-by-step tutorial for building dashboards
- [Exploring Data](https://superset.apache.org/docs/using-superset/exploring-data): Guide to data exploration features
- [Issue Codes](https://superset.apache.org/docs/using-superset/issue-codes): Reference for Superset error codes
## Security
- [Security Overview](https://superset.apache.org/docs/security/security): RBAC, row-level security, and authentication options
- [Securing Superset](https://superset.apache.org/docs/security/securing_superset): Production security hardening guide
- [CVEs](https://superset.apache.org/docs/security/cves): Security vulnerabilities and advisories
## Contributing
- [Contributing Guide](https://superset.apache.org/docs/contributing/contributing): How to contribute to Apache Superset
- [Development Environment](https://superset.apache.org/docs/contributing/development): Set up a local development environment
- [Guidelines](https://superset.apache.org/docs/contributing/guidelines): Code style, testing, and PR guidelines
- [How-Tos](https://superset.apache.org/docs/contributing/howtos): Common development tasks and recipes
- [Resources](https://superset.apache.org/docs/contributing/resources): Additional contributor resources
## Reference
- [FAQ](https://superset.apache.org/docs/faq): Frequently asked questions
- [REST API](https://superset.apache.org/docs/api): Superset REST API documentation
## Optional
- [Country Map Tools](https://superset.apache.org/docs/configuration/country-map-tools): Custom country map visualizations
- [Map Tiles](https://superset.apache.org/docs/configuration/map-tiles): Configure map tile providers for deck.gl charts
- [Miscellaneous](https://superset.apache.org/docs/contributing/misc): Additional contributor information

View File

@@ -2762,19 +2762,19 @@
"@rc-component/util" "^1.2.1"
clsx "^2.1.1"
"@rc-component/form@~1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@rc-component/form/-/form-1.4.0.tgz#bee504c182bbb768b5fb68809e82b69deef9aec0"
integrity sha512-C8MN/2wIaW9hSrCCtJmcgCkWTQNIspN7ARXLFA4F8PGr8Qxk39U5pS3kRK51/bUJNhb/fEtdFnaViLlISGKI2A==
"@rc-component/form@~1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@rc-component/form/-/form-1.5.0.tgz#67ea351fde90ff94e1866b430594c9864fb29443"
integrity sha512-clF2Ws00bImVSOfaF4e2dLr631g5QOUD7M7kqb8es6fWXkJ1YO4nmjGJTQ/7QfB7iqY6JED4yV12ftKKD5/8GQ==
dependencies:
"@rc-component/async-validator" "^5.0.3"
"@rc-component/util" "^1.3.0"
"@rc-component/util" "^1.5.0"
clsx "^2.1.1"
"@rc-component/image@~1.5.2":
version "1.5.2"
resolved "https://registry.yarnpkg.com/@rc-component/image/-/image-1.5.2.tgz#46cd467466f8b5c9a682bbc96a04f15ad3688af6"
integrity sha512-SIbYLy0IrXqyhccpKktQEvpbBti/KwgG8V/E8GJa8ycwOQmuZaCP7b/C+eQlivn4KDWpfKfoOrLKHXmVlljDgg==
"@rc-component/image@~1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@rc-component/image/-/image-1.5.3.tgz#ea163e5b55303d548e3b2946e99bdcd9e7586299"
integrity sha512-/NR7QW9uCN8Ugar+xsHZOPvzPySfEhcW2/vLcr7VPRM+THZMrllMRv7LAUgW7ikR+Z67Ab67cgPp5K5YftpJsQ==
dependencies:
"@rc-component/motion" "^1.0.0"
"@rc-component/portal" "^2.0.0"
@@ -2870,10 +2870,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/picker@~1.8.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@rc-component/picker/-/picker-1.8.0.tgz#4fe2965acd804995123d7bd81d184c2c81f92f8b"
integrity sha512-ek4efrIy+peC8WFJg6Lg7c+WNkykr+wUGQGBNoKmlF0K752aIJuaPcBj6p8CceT9vSJ9gOeeclQCBQIFWVDk1A==
"@rc-component/picker@~1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@rc-component/picker/-/picker-1.9.0.tgz#5ecb5595d2fcf0b4ec4edc9202628f42a314c4b0"
integrity sha512-OLisdk8AWVCG9goBU1dWzuH5QlBQk8jktmQ6p0/IyBFwdKGwyIZOSjnBYo8hooHiTdl0lU+wGf/OfMtVBw02KQ==
dependencies:
"@rc-component/overflow" "^1.0.0"
"@rc-component/resize-observer" "^1.0.0"
@@ -2929,10 +2929,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/select@~1.3.0", "@rc-component/select@~1.3.2":
version "1.3.4"
resolved "https://registry.yarnpkg.com/@rc-component/select/-/select-1.3.4.tgz#e1c0756290ae4ed3d6823b36de536222752b1193"
integrity sha512-NKhzahL/lXk3aKtmeH5W/jIqaPKcx9QiFXOvJxKe8eiuusIcSCW+XvJdjY3nRvCpTZCZDp7e1RaCU95gohx6Ow==
"@rc-component/select@~1.3.0", "@rc-component/select@~1.3.5":
version "1.3.5"
resolved "https://registry.yarnpkg.com/@rc-component/select/-/select-1.3.5.tgz#5306a4bbcb43fe45712544bc5d9a73a65e47ce8d"
integrity sha512-A2QVOWDfRoLgHwPHrCGx1G42dYntOk+nsT6SX4ADCoagqu4bcxceJPbYvVKkfMYSIwgtfu+tDhPk3Z5gz8944g==
dependencies:
"@rc-component/overflow" "^1.0.0"
"@rc-component/trigger" "^3.0.0"
@@ -3055,7 +3055,7 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/util@^1.0.1", "@rc-component/util@^1.1.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0":
"@rc-component/util@^1.0.1", "@rc-component/util@^1.1.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.5.0", "@rc-component/util@^1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.6.0.tgz#4f700da5417eb5fd5f9491f08edcba6d075d9454"
integrity sha512-YbjuIVAm8InCnXVoA4n6G+uh31yESTxQ6fSY2frZ2/oMSvktoB+bumFUfNN7RKh7YeOkZgOvN2suGtEDhJSX0A==
@@ -4211,6 +4211,11 @@
dependencies:
"@types/istanbul-lib-report" "*"
"@types/js-yaml@^4.0.9":
version "4.0.9"
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2"
integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==
"@types/json-bigint@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@types/json-bigint/-/json-bigint-1.0.4.tgz#250d29e593375499d8ba6efaab22d094c3199ef3"
@@ -4430,100 +4435,100 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.49.0", "@typescript-eslint/eslint-plugin@^8.37.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz#8ed8736b8415a9193989220eadb6031dbcd2260a"
integrity sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==
"@typescript-eslint/eslint-plugin@8.50.0", "@typescript-eslint/eslint-plugin@^8.37.0":
version "8.50.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz#a6ce899690542e2affa9543306d2d3935740abb7"
integrity sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==
dependencies:
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "8.49.0"
"@typescript-eslint/type-utils" "8.49.0"
"@typescript-eslint/utils" "8.49.0"
"@typescript-eslint/visitor-keys" "8.49.0"
"@typescript-eslint/scope-manager" "8.50.0"
"@typescript-eslint/type-utils" "8.50.0"
"@typescript-eslint/utils" "8.50.0"
"@typescript-eslint/visitor-keys" "8.50.0"
ignore "^7.0.0"
natural-compare "^1.4.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/parser@8.49.0", "@typescript-eslint/parser@^8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.49.0.tgz#0ede412d59e99239b770f0f08c76c42fba717fa2"
integrity sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==
"@typescript-eslint/parser@8.50.0", "@typescript-eslint/parser@^8.50.0":
version "8.50.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.50.0.tgz#c35b28f686dbe08e81b9d6208ebc08912549f4ba"
integrity sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==
dependencies:
"@typescript-eslint/scope-manager" "8.49.0"
"@typescript-eslint/types" "8.49.0"
"@typescript-eslint/typescript-estree" "8.49.0"
"@typescript-eslint/visitor-keys" "8.49.0"
"@typescript-eslint/scope-manager" "8.50.0"
"@typescript-eslint/types" "8.50.0"
"@typescript-eslint/typescript-estree" "8.50.0"
"@typescript-eslint/visitor-keys" "8.50.0"
debug "^4.3.4"
"@typescript-eslint/project-service@8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.49.0.tgz#ce220525c88cb2d23792b391c07e14cb9697651a"
integrity sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==
"@typescript-eslint/project-service@8.50.0":
version "8.50.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.50.0.tgz#1422366b7cc11fef8c6d87770884e608093423a4"
integrity sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.49.0"
"@typescript-eslint/types" "^8.49.0"
"@typescript-eslint/tsconfig-utils" "^8.50.0"
"@typescript-eslint/types" "^8.50.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz#a3496765b57fb48035d671174552e462e5bffa63"
integrity sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==
"@typescript-eslint/scope-manager@8.50.0":
version "8.50.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz#e0d6c838dc9044bc679724611b138cb34c81bddf"
integrity sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==
dependencies:
"@typescript-eslint/types" "8.49.0"
"@typescript-eslint/visitor-keys" "8.49.0"
"@typescript-eslint/types" "8.50.0"
"@typescript-eslint/visitor-keys" "8.50.0"
"@typescript-eslint/tsconfig-utils@8.49.0", "@typescript-eslint/tsconfig-utils@^8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz#857777c8e35dd1e564505833d8043f544442fbf4"
integrity sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==
"@typescript-eslint/tsconfig-utils@8.50.0", "@typescript-eslint/tsconfig-utils@^8.50.0":
version "8.50.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz#5c17537ad4c8a13bf6d7393035edaf91a1e13191"
integrity sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==
"@typescript-eslint/type-utils@8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz#d8118a0c1896a78a22f01d3c176e9945409b085b"
integrity sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==
"@typescript-eslint/type-utils@8.50.0":
version "8.50.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz#feb6f54f876980a258b14f1cb033f54fc545d37b"
integrity sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==
dependencies:
"@typescript-eslint/types" "8.49.0"
"@typescript-eslint/typescript-estree" "8.49.0"
"@typescript-eslint/utils" "8.49.0"
"@typescript-eslint/types" "8.50.0"
"@typescript-eslint/typescript-estree" "8.50.0"
"@typescript-eslint/utils" "8.50.0"
debug "^4.3.4"
ts-api-utils "^2.1.0"
"@typescript-eslint/types@8.49.0", "@typescript-eslint/types@^8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.49.0.tgz#c1bd3ebf956d9e5216396349ca23c58d74f06aee"
integrity sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==
"@typescript-eslint/types@8.50.0", "@typescript-eslint/types@^8.50.0":
version "8.50.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.50.0.tgz#ad8f1ad88ae0096f548c9cdf60da9b92832db96e"
integrity sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==
"@typescript-eslint/typescript-estree@8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz#99c5a53275197ccb4e849786dad68344e9924135"
integrity sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==
"@typescript-eslint/typescript-estree@8.50.0":
version "8.50.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz#2871d36617f81a127db905fa91b16d1a0251411b"
integrity sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==
dependencies:
"@typescript-eslint/project-service" "8.49.0"
"@typescript-eslint/tsconfig-utils" "8.49.0"
"@typescript-eslint/types" "8.49.0"
"@typescript-eslint/visitor-keys" "8.49.0"
"@typescript-eslint/project-service" "8.50.0"
"@typescript-eslint/tsconfig-utils" "8.50.0"
"@typescript-eslint/types" "8.50.0"
"@typescript-eslint/visitor-keys" "8.50.0"
debug "^4.3.4"
minimatch "^9.0.4"
semver "^7.6.0"
tinyglobby "^0.2.15"
ts-api-utils "^2.1.0"
"@typescript-eslint/utils@8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.49.0.tgz#43b3b91d30afd6f6114532cf0b228f1790f43aff"
integrity sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==
"@typescript-eslint/utils@8.50.0":
version "8.50.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.50.0.tgz#107f20a5747eab5db988c5f6ad462b59851cdd1f"
integrity sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==
dependencies:
"@eslint-community/eslint-utils" "^4.7.0"
"@typescript-eslint/scope-manager" "8.49.0"
"@typescript-eslint/types" "8.49.0"
"@typescript-eslint/typescript-estree" "8.49.0"
"@typescript-eslint/scope-manager" "8.50.0"
"@typescript-eslint/types" "8.50.0"
"@typescript-eslint/typescript-estree" "8.50.0"
"@typescript-eslint/visitor-keys@8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz#8e450cc502c0d285cad9e84d400cf349a85ced6c"
integrity sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==
"@typescript-eslint/visitor-keys@8.50.0":
version "8.50.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz#79d1c95474e08f844dbe13370715cfb9b7e21363"
integrity sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==
dependencies:
"@typescript-eslint/types" "8.49.0"
"@typescript-eslint/types" "8.50.0"
eslint-visitor-keys "^4.2.1"
"@ungap/structured-clone@^1.0.0":
@@ -4860,10 +4865,10 @@ ansi-styles@^6.1.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
antd@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.1.0.tgz#2924f50e37bf1fe9b4c494057eca1c1b7ccfe47a"
integrity sha512-RIe4W5saaL9SWgvqCcvz6LZta/KwT50B0YF7xYiWVZh0Gqfw2rJAsOMcp202Hxgm+YiyoSp4QqqvexKhuGGarw==
antd@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.1.1.tgz#e16c234ce4b69d09486ab17fdb5960a0508164b9"
integrity sha512-GBVxq3ShcYN/lEALvLPQDFN0bWjp2gQaFvRIXu4cwvXQcbV/D2FhShhTiNWhXxUk6nVkODBi4Uzo9SfLbDGsng==
dependencies:
"@ant-design/colors" "^8.0.0"
"@ant-design/cssinjs" "^2.0.1"
@@ -4879,8 +4884,8 @@ antd@^6.1.0:
"@rc-component/dialog" "~1.5.1"
"@rc-component/drawer" "~1.3.0"
"@rc-component/dropdown" "~1.0.2"
"@rc-component/form" "~1.4.0"
"@rc-component/image" "~1.5.2"
"@rc-component/form" "~1.5.0"
"@rc-component/image" "~1.5.3"
"@rc-component/input" "~1.1.2"
"@rc-component/input-number" "~1.6.2"
"@rc-component/mentions" "~1.6.0"
@@ -4889,13 +4894,13 @@ antd@^6.1.0:
"@rc-component/mutate-observer" "^2.0.1"
"@rc-component/notification" "~1.2.0"
"@rc-component/pagination" "~1.2.0"
"@rc-component/picker" "~1.8.0"
"@rc-component/picker" "~1.9.0"
"@rc-component/progress" "~1.0.2"
"@rc-component/qrcode" "~1.1.1"
"@rc-component/rate" "~1.0.1"
"@rc-component/resize-observer" "^1.0.1"
"@rc-component/segmented" "~1.2.3"
"@rc-component/select" "~1.3.2"
"@rc-component/select" "~1.3.5"
"@rc-component/slider" "~1.0.1"
"@rc-component/steps" "~1.2.2"
"@rc-component/switch" "~1.0.3"
@@ -4908,7 +4913,7 @@ antd@^6.1.0:
"@rc-component/tree-select" "~1.4.0"
"@rc-component/trigger" "^3.7.1"
"@rc-component/upload" "~1.1.0"
"@rc-component/util" "^1.4.0"
"@rc-component/util" "^1.6.0"
clsx "^2.1.1"
dayjs "^1.11.11"
scroll-into-view-if-needed "^3.1.0"
@@ -5157,10 +5162,10 @@ base64-js@^1.3.1, base64-js@^1.5.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
baseline-browser-mapping@^2.8.9:
version "2.8.10"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.10.tgz#32eb5e253d633fa3fa3ffb1685fabf41680d9e8a"
integrity sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==
baseline-browser-mapping@^2.9.0:
version "2.9.8"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.8.tgz#04fb5c10ff9c7a1b04ac08cfdfc3b10942a8ac72"
integrity sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA==
batch@0.6.1:
version "0.6.1"
@@ -5275,16 +5280,16 @@ browser-assert@^1.2.1:
resolved "https://registry.yarnpkg.com/browser-assert/-/browser-assert-1.2.1.tgz#9aaa5a2a8c74685c2ae05bfe46efd606f068c200"
integrity sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.25.0, browserslist@^4.25.3, browserslist@^4.26.3:
version "4.26.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.26.3.tgz#40fbfe2d1cd420281ce5b1caa8840049c79afb56"
integrity sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.25.0, browserslist@^4.25.3, browserslist@^4.28.1:
version "4.28.1"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95"
integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==
dependencies:
baseline-browser-mapping "^2.8.9"
caniuse-lite "^1.0.30001746"
electron-to-chromium "^1.5.227"
node-releases "^2.0.21"
update-browserslist-db "^1.1.3"
baseline-browser-mapping "^2.9.0"
caniuse-lite "^1.0.30001759"
electron-to-chromium "^1.5.263"
node-releases "^2.0.27"
update-browserslist-db "^1.2.0"
buffer-from@^1.0.0:
version "1.1.2"
@@ -5393,7 +5398,7 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001746, caniuse-lite@^1.0.30001760:
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001760:
version "1.0.30001760"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz#bdd1960fafedf8d5f04ff16e81460506ff9b798f"
integrity sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==
@@ -6716,10 +6721,10 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.5.227:
version "1.5.228"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.228.tgz#38b849bc8714bd21fb64f5ad56bf8cfd8638e1e9"
integrity sha512-nxkiyuqAn4MJ1QbobwqJILiDtu/jk14hEAWaMiJmNPh1Z+jqoFlBFZjdXwLWGeVSeu9hGLg6+2G9yJaW8rBIFA==
electron-to-chromium@^1.5.263:
version "1.5.267"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz#5d84f2df8cdb6bfe7e873706bb21bd4bfb574dc7"
integrity sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==
emoji-regex@^8.0.0:
version "8.0.0"
@@ -6756,10 +6761,10 @@ encodeurl@~2.0.0:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.3:
version "5.18.3"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz#9b5f4c5c076b8787c78fe540392ce76a88855b44"
integrity sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==
enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.4:
version "5.18.4"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz#c22d33055f3952035ce6a144ce092447c525f828"
integrity sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
@@ -6885,10 +6890,10 @@ es-iterator-helpers@^1.2.1:
iterator.prototype "^1.1.4"
safe-array-concat "^1.1.3"
es-module-lexer@^1.2.1:
version "1.7.0"
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a"
integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==
es-module-lexer@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.0.0.tgz#f657cd7a9448dcdda9c070a3cb75e5dc1e85f5b1"
integrity sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
version "1.1.1"
@@ -8756,7 +8761,16 @@ js-file-download@^0.4.12:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@=4.1.1, js-yaml@^4.1.0:
js-yaml-loader@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/js-yaml-loader/-/js-yaml-loader-1.2.2.tgz#2c15f93915617acd19676d648945fa3003f8629b"
integrity sha512-H+NeuNrG6uOs/WMjna2SjkaCw13rMWiT/D7l9+9x5n8aq88BDsh2sRmdfxckWPIHtViYHWRG6XiCKYvS1dfyLg==
dependencies:
js-yaml "^3.13.1"
loader-utils "^1.2.3"
un-eval "^1.2.0"
js-yaml@=4.1.1, js-yaml@^4.1.0, js-yaml@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
@@ -8835,6 +8849,13 @@ json2mq@^0.2.0:
dependencies:
string-convert "^0.2.0"
json5@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
dependencies:
minimist "^1.2.0"
json5@^2.1.2, json5@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
@@ -8984,6 +9005,15 @@ loader-runner@^4.3.1:
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.1.tgz#6c76ed29b0ccce9af379208299f07f876de737e3"
integrity sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==
loader-utils@^1.2.3:
version "1.4.2"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3"
integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==
dependencies:
big.js "^5.2.2"
emojis-list "^3.0.0"
json5 "^1.0.1"
loader-utils@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c"
@@ -10306,10 +10336,10 @@ node-gyp-build@^4.8.0, node-gyp-build@^4.8.2, node-gyp-build@^4.8.4:
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8"
integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==
node-releases@^2.0.21:
version "2.0.21"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.21.tgz#f59b018bc0048044be2d4c4c04e4c8b18160894c"
integrity sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==
node-releases@^2.0.27:
version "2.0.27"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e"
integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
@@ -13119,10 +13149,10 @@ tapable@^2.0.0, tapable@^2.2.0, tapable@^2.2.1, tapable@^2.3.0:
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6"
integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==
terser-webpack-plugin@^5.3.11, terser-webpack-plugin@^5.3.9:
version "5.3.14"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz#9031d48e57ab27567f02ace85c7d690db66c3e06"
integrity sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==
terser-webpack-plugin@^5.3.16, terser-webpack-plugin@^5.3.9:
version "5.3.16"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz#741e448cc3f93d8026ebe4f7ef9e4afacfd56330"
integrity sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==
dependencies:
"@jridgewell/trace-mapping" "^0.3.25"
jest-worker "^27.4.5"
@@ -13417,15 +13447,15 @@ types-ramda@^0.30.1:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.49.0:
version "8.49.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.49.0.tgz#4a8b608ae48c0db876c8fb2a2724839fc5a7147c"
integrity sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==
typescript-eslint@^8.50.0:
version "8.50.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.50.0.tgz#b91e73eea65edf46e10425dbeb0dc1ddb0d7fea5"
integrity sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==
dependencies:
"@typescript-eslint/eslint-plugin" "8.49.0"
"@typescript-eslint/parser" "8.49.0"
"@typescript-eslint/typescript-estree" "8.49.0"
"@typescript-eslint/utils" "8.49.0"
"@typescript-eslint/eslint-plugin" "8.50.0"
"@typescript-eslint/parser" "8.50.0"
"@typescript-eslint/typescript-estree" "8.50.0"
"@typescript-eslint/utils" "8.50.0"
typescript@~5.9.3:
version "5.9.3"
@@ -13437,6 +13467,11 @@ ufo@^1.5.4:
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b"
integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==
un-eval@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/un-eval/-/un-eval-1.2.0.tgz#22a95c650334d59d21697efae32612218ecad65f"
integrity sha512-Wlj/pum6dQtGTPD/lclDtoVPkSfpjPfy1dwnnKw/sZP5DpBH9fLhBgQfsqNhe5/gS1D+vkZUuB771NRMUPA5CA==
unbox-primitive@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2"
@@ -13651,10 +13686,10 @@ unraw@^3.0.0:
resolved "https://registry.yarnpkg.com/unraw/-/unraw-3.0.0.tgz#73443ed70d2ab09ccbac2b00525602d5991fbbe3"
integrity sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==
update-browserslist-db@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420"
integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==
update-browserslist-db@^1.2.0:
version "1.2.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d"
integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==
dependencies:
escalade "^3.2.0"
picocolors "^1.1.1"
@@ -13973,10 +14008,10 @@ webpack-virtual-modules@^0.6.2:
resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
webpack@^5.103.0, webpack@^5.88.1, webpack@^5.95.0:
version "5.103.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.103.0.tgz#17a7c5a5020d5a3a37c118d002eade5ee2c6f3da"
integrity sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==
webpack@^5.104.0, webpack@^5.88.1, webpack@^5.95.0:
version "5.104.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.104.0.tgz#2b919a4f2526cdc42731142ae295019264fcfb76"
integrity sha512-5DeICTX8BVgNp6afSPYXAFjskIgWGlygQH58bcozPOXgo2r/6xx39Y1+cULZ3gTxUYQP88jmwLj2anu4Xaq84g==
dependencies:
"@types/eslint-scope" "^3.7.7"
"@types/estree" "^1.0.8"
@@ -13986,10 +14021,10 @@ webpack@^5.103.0, webpack@^5.88.1, webpack@^5.95.0:
"@webassemblyjs/wasm-parser" "^1.14.1"
acorn "^8.15.0"
acorn-import-phases "^1.0.3"
browserslist "^4.26.3"
browserslist "^4.28.1"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.17.3"
es-module-lexer "^1.2.1"
enhanced-resolve "^5.17.4"
es-module-lexer "^2.0.0"
eslint-scope "5.1.1"
events "^3.2.0"
glob-to-regexp "^0.4.1"
@@ -14000,7 +14035,7 @@ webpack@^5.103.0, webpack@^5.88.1, webpack@^5.95.0:
neo-async "^2.6.2"
schema-utils "^4.3.3"
tapable "^2.3.0"
terser-webpack-plugin "^5.3.11"
terser-webpack-plugin "^5.3.16"
watchpack "^2.4.4"
webpack-sources "^3.3.3"

View File

@@ -18,7 +18,7 @@
[project]
name = "apache-superset-core"
version = "0.0.1rc2"
version = "0.0.1rc3"
description = "Core Python package for building Apache Superset backend extensions and integrations"
readme = "README.md"
authors = [

File diff suppressed because it is too large Load Diff

View File

@@ -160,7 +160,7 @@
"geostyler-openlayers-parser": "^4.3.0",
"geostyler-style": "7.5.0",
"geostyler-wfs-parser": "^2.0.3",
"googleapis": "^168.0.0",
"googleapis": "^169.0.0",
"immer": "^11.0.1",
"interweave": "^13.1.1",
"jquery": "^3.7.1",
@@ -175,10 +175,10 @@
"memoize-one": "^5.2.1",
"mousetrap": "^1.6.5",
"mustache": "^4.2.0",
"nanoid": "^5.0.9",
"nanoid": "^5.1.6",
"ol": "^7.5.2",
"prop-types": "^15.8.1",
"re-resizable": "^6.10.1",
"re-resizable": "^6.11.2",
"react": "^17.0.2",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^3.4.0",
@@ -234,7 +234,7 @@
"@babel/preset-typescript": "^7.26.0",
"@babel/register": "^7.23.7",
"@babel/runtime": "^7.28.4",
"@babel/runtime-corejs3": "^7.28.2",
"@babel/runtime-corejs3": "^7.28.4",
"@babel/types": "^7.26.9",
"@cypress/react": "^8.0.2",
"@emotion/babel-plugin": "^11.13.5",
@@ -243,7 +243,7 @@
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@mihkeleidast/storybook-addon-source": "^1.0.1",
"@playwright/test": "^1.56.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.17",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-actions": "8.6.14",
"@storybook/addon-controls": "8.6.14",
"@storybook/addon-essentials": "8.6.14",
@@ -269,18 +269,15 @@
"@types/jest": "^30.0.0",
"@types/js-levenshtein": "^1.1.3",
"@types/json-bigint": "^1.0.4",
"@types/math-expression-evaluator": "^2.0.0",
"@types/mousetrap": "^1.6.15",
"@types/node": "^25.0.2",
"@types/react": "^17.0.83",
"@types/react-dom": "^17.0.26",
"@types/react-json-tree": "^0.13.0",
"@types/react-loadable": "^5.5.11",
"@types/react-redux": "^7.1.10",
"@types/react-resizable": "^3.0.8",
"@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "^4.4.12",
"@types/react-virtualized-auto-sizer": "^1.0.8",
"@types/react-window": "^1.8.8",
"@types/redux-localstorage": "^1.0.8",
"@types/redux-mock-store": "^1.0.6",
@@ -295,7 +292,7 @@
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-typescript-to-proptypes": "^2.0.0",
"baseline-browser-mapping": "^2.9.7",
"baseline-browser-mapping": "^2.9.8",
"cheerio": "1.1.0",
"concurrently": "^9.2.1",
"copy-webpack-plugin": "^13.0.1",
@@ -363,7 +360,7 @@
"vm-browserify": "^1.1.2",
"wait-on": "^9.0.3",
"webpack": "^5.103.0",
"webpack-bundle-analyzer": "^4.10.1",
"webpack-bundle-analyzer": "^5.1.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.2",
"webpack-manifest-plugin": "^5.0.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@apache-superset/core",
"version": "0.0.1-rc5",
"version": "0.0.1-rc6",
"description": "This package contains UI elements, APIs, and utility functions used by Superset.",
"sideEffects": false,
"main": "lib/index.js",
@@ -17,7 +17,7 @@
"@babel/preset-react": "^7.26.3",
"@babel/preset-typescript": "^7.26.0",
"install": "^0.13.0",
"npm": "^11.1.0",
"npm": "^11.7.0",
"typescript": "^5.0.0",
"@emotion/styled": "^11.14.1",
"@types/lodash": "^4.17.21",

View File

@@ -32,7 +32,7 @@
"ag-grid-community": "34.3.1",
"ag-grid-react": "34.3.1",
"brace": "^0.11.1",
"classnames": "^2.2.5",
"classnames": "^2.5.1",
"csstype": "^3.1.3",
"core-js": "^3.38.1",
"d3-format": "^1.3.2",
@@ -78,7 +78,6 @@
"@types/react-syntax-highlighter": "^15.5.13",
"@types/jquery": "^3.5.33",
"@types/lodash": "^4.17.21",
"@types/math-expression-evaluator": "^2.0.0",
"@types/node": "^25.0.2",
"@types/prop-types": "^15.7.15",
"@types/rison": "0.1.0",

View File

@@ -45,7 +45,6 @@
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "*",
"@types/classnames": "*",
"@types/react": "*",
"match-sorter": "^6.3.3",
"react": "^17.0.2",

View File

@@ -437,15 +437,21 @@ export function getLegendProps(
zoomable = false,
legendState?: LegendState,
padding?: LegendPaddingType,
): LegendComponentOption | LegendComponentOption[] {
const legend: LegendComponentOption | LegendComponentOption[] = {
): LegendComponentOption {
const isHorizontal =
orientation === LegendOrientation.Top ||
orientation === LegendOrientation.Bottom;
const effectiveType =
type === LegendType.Scroll || !isHorizontal ? type : LegendType.Scroll;
const legend: LegendComponentOption = {
orient: [LegendOrientation.Top, LegendOrientation.Bottom].includes(
orientation,
)
? 'horizontal'
: 'vertical',
show,
type,
type: effectiveType,
selected: legendState,
selector: ['all', 'inverse'],
selectorLabel: {

View File

@@ -139,7 +139,6 @@ describe('Gantt transformProps', () => {
legend: expect.objectContaining({
show: true,
type: 'scroll',
selector: ['all', 'inverse'],
}),
tooltip: {
formatter: expect.anything(),

View File

@@ -891,14 +891,27 @@ describe('getLegendProps', () => {
});
});
it('should return the correct props for plain type with bottom orientation', () => {
it('should default plain legends to scroll for bottom orientation', () => {
expect(
getLegendProps(LegendType.Plain, LegendOrientation.Bottom, false, theme),
).toEqual({
show: false,
bottom: 0,
orient: 'horizontal',
type: 'plain',
type: 'scroll',
...expectedThemeProps,
});
});
it('should default plain legends to scroll for top orientation', () => {
expect(
getLegendProps(LegendType.Plain, LegendOrientation.Top, false, theme),
).toEqual({
show: false,
top: 0,
right: 0,
orient: 'horizontal',
type: 'scroll',
...expectedThemeProps,
});
});

View File

@@ -45,7 +45,6 @@
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "*",
"@types/classnames": "*",
"@types/react": "*",
"match-sorter": "^6.3.3",
"react": "^17.0.2",

View File

@@ -135,7 +135,8 @@ export { customRender as render };
export { default as userEvent } from '@testing-library/user-event';
export async function selectOption(option: string, selectName?: string) {
const select = screen.getByRole(
// Use findByRole (async) to wait for element to be ready, preventing race conditions on slow CI
const select = await screen.findByRole(
'combobox',
selectName ? { name: selectName } : {},
);

View File

@@ -73,11 +73,14 @@ beforeEach(() => {
dbId: expectDbId,
forceRefresh: false,
},
fakeSchemaApiResult.map(value => ({
value,
label: value,
title: value,
})),
{
schemas: fakeSchemaApiResult.map(value => ({
value,
label: value,
title: value,
})),
defaultSchema: null,
},
),
);
store.dispatch(
@@ -307,11 +310,14 @@ test('returns long keywords with docText', async () => {
dbId: expectLongKeywordDbId,
forceRefresh: false,
},
['short', longKeyword].map(value => ({
value,
label: value,
title: value,
})),
{
schemas: ['short', longKeyword].map(value => ({
value,
label: value,
title: value,
})),
defaultSchema: null,
},
),
);
});

View File

@@ -78,7 +78,7 @@ export function useKeywords(
// skipFetch is used to prevent re-evaluating memoized keywords
// due to updated api results by skip flag
const skipFetch = hasFetchedKeywords && skip;
const { currentData: schemaOptions } = useSchemasQueryState(
const { currentData: schemaData } = useSchemasQueryState(
{
dbId,
catalog: catalog || undefined,
@@ -86,6 +86,7 @@ export function useKeywords(
},
{ skip: skipFetch || !dbId },
);
const schemaOptions = schemaData?.schemas;
const { currentData: tableData } = useTablesQueryState(
{
dbId,

View File

@@ -17,21 +17,21 @@
* under the License.
*/
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { bindActionCreators, Dispatch, AnyAction } from 'redux';
import * as actions from './chartAction';
import { logEvent } from '../../logger/actions';
import Chart from './Chart';
import { updateDataMask } from '../../dataMask/actions';
function mapDispatchToProps(dispatch) {
function mapDispatchToProps(dispatch: Dispatch<AnyAction>) {
return {
actions: bindActionCreators(
{
...actions,
updateDataMask,
logEvent,
},
} as any,
dispatch,
),
};

View File

@@ -163,11 +163,13 @@ const fakeDatabaseApiResultInReverseOrder = {
const fakeSchemaApiResult = {
count: 2,
result: ['information_schema', 'public'],
default: 'public',
};
const fakeCatalogApiResult = {
count: 0,
result: [],
default: null,
};
const fakeFunctionNamesApiResult = {
@@ -369,10 +371,11 @@ test('Sends the correct schema when changing the schema', async () => {
});
await waitFor(() => expect(fetchMock.calls(databaseApiRoute).length).toBe(1));
rerender(<DatabaseSelector {...props} />);
expect(props.onSchemaChange).toHaveBeenCalledTimes(0);
const select = screen.getByRole('combobox', {
// Wait for schema data to load
const select = await screen.findByRole('combobox', {
name: 'Select schema or type to search schemas: public',
});
expect(props.onSchemaChange).toHaveBeenCalledTimes(0);
expect(select).toBeInTheDocument();
await userEvent.click(select);
const schemaOption = await screen.findByText('information_schema');
@@ -382,3 +385,82 @@ test('Sends the correct schema when changing the schema', async () => {
);
expect(props.onSchemaChange).toHaveBeenCalledTimes(1);
});
test('Auto-selects default schema on first load when no schema is provided', async () => {
fetchMock.get(
schemaApiRoute,
{
result: ['information_schema', 'public', 'other_schema'],
default: 'public',
},
{ overwriteRoutes: true },
);
const props = {
...createProps(),
schema: undefined,
};
render(<DatabaseSelector {...props} />, { useRedux: true, store });
// Wait for schemas to load and default to be applied
await waitFor(() => {
expect(props.onSchemaChange).toHaveBeenCalledWith('public');
});
});
test('Does not auto-select default schema when schema is already provided', async () => {
fetchMock.get(
schemaApiRoute,
{
result: ['information_schema', 'public', 'other_schema'],
default: 'public',
},
{ overwriteRoutes: true },
);
const props = {
...createProps(),
schema: 'information_schema',
};
render(<DatabaseSelector {...props} />, { useRedux: true, store });
// Wait for schemas to load
await waitFor(() => {
expect(fetchMock.calls(schemaApiRoute).length).toBe(1);
});
// Should not call onSchemaChange since schema is already set
expect(props.onSchemaChange).not.toHaveBeenCalled();
});
test('Auto-selects default catalog on first load for multi-catalog database', async () => {
fetchMock.get(
catalogApiRoute,
{
result: ['catalog_a', 'catalog_b', 'catalog_c'],
default: 'catalog_b',
},
{ overwriteRoutes: true },
);
const props = {
...createProps(),
db: {
id: 1,
database_name: 'test-multicatalog',
backend: 'test-postgresql',
allow_multi_catalog: true,
},
catalog: undefined,
onCatalogChange: jest.fn(),
};
render(<DatabaseSelector {...props} />, { useRedux: true, store });
// Wait for catalogs to load and default to be applied
await waitFor(() => {
expect(props.onCatalogChange).toHaveBeenCalledWith('catalog_b');
});
});

View File

@@ -144,6 +144,9 @@ export function DatabaseSelector({
);
const schemaRef = useRef(schema);
schemaRef.current = schema;
// Track if we've applied defaults to avoid re-applying after user clears selection
const appliedCatalogDefaultRef = useRef<string | null>(null);
const appliedSchemaDefaultRef = useRef<string | null>(null);
const { addSuccessToast } = useToasts();
const sortComparator = useCallback(
(itemA: AntdLabeledValueWithOrder, itemB: AntdLabeledValueWithOrder) =>
@@ -240,22 +243,14 @@ export function DatabaseSelector({
}
const {
currentData: schemaData,
data: schemaData,
isFetching: loadingSchemas,
refetch: refetchSchemas,
defaultSchema,
} = useSchemas({
dbId: currentDb?.value,
catalog: currentCatalog?.value,
onSuccess: (schemas, isFetched) => {
setErrorPayload(null);
if (schemas.length === 1) {
changeSchema(schemas[0]);
} else if (
!schemas.find(schemaOption => schemaRef.current === schemaOption.value)
) {
changeSchema(undefined);
}
if (isFetched) {
addSuccessToast('List refreshed');
}
@@ -271,9 +266,41 @@ export function DatabaseSelector({
const schemaOptions = schemaData || EMPTY_SCHEMA_OPTIONS;
// Handle schema auto-selection when data changes
useEffect(() => {
if (!schemaData || loadingSchemas) return;
setErrorPayload(null);
if (schemaData.length === 1) {
changeSchema(schemaData[0]);
} else if (
!schemaData.find(schemaOption => schemaRef.current === schemaOption.value)
) {
// Current selection not in list - try to apply default on first load
if (
defaultSchema &&
appliedSchemaDefaultRef.current !== defaultSchema
) {
const defaultOption = schemaData.find(s => s.value === defaultSchema);
if (defaultOption) {
appliedSchemaDefaultRef.current = defaultSchema;
changeSchema(defaultOption);
} else {
changeSchema(undefined);
}
} else {
changeSchema(undefined);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [schemaData, defaultSchema, loadingSchemas]);
function changeCatalog(catalog: CatalogOption | null | undefined) {
setCurrentCatalog(catalog);
setCurrentSchema(undefined);
// Reset schema default ref so default can be applied for the new catalog
appliedSchemaDefaultRef.current = null;
if (onCatalogChange && catalog?.value !== catalogRef.current) {
onCatalogChange(catalog?.value);
}
@@ -283,22 +310,10 @@ export function DatabaseSelector({
data: catalogData,
isFetching: loadingCatalogs,
refetch: refetchCatalogs,
defaultCatalog,
} = useCatalogs({
dbId: showCatalogSelector ? currentDb?.value : undefined,
onSuccess: (catalogs, isFetched) => {
setErrorPayload(null);
if (!showCatalogSelector) {
changeCatalog(null);
} else if (catalogs.length === 1) {
changeCatalog(catalogs[0]);
} else if (
!catalogs.find(
catalogOption => catalogRef.current === catalogOption.value,
)
) {
changeCatalog(undefined);
}
if (showCatalogSelector && isFetched) {
addSuccessToast('List refreshed');
}
@@ -316,6 +331,49 @@ export function DatabaseSelector({
const catalogOptions = catalogData || EMPTY_CATALOG_OPTIONS;
// Handle catalog auto-selection when data changes
useEffect(() => {
if (loadingCatalogs) return;
setErrorPayload(null);
if (!showCatalogSelector) {
// Only clear catalog if it's not already null
if (currentCatalog !== null) {
setCurrentCatalog(null);
if (onCatalogChange && catalogRef.current != null) {
onCatalogChange(undefined);
}
}
} else if (catalogData && catalogData.length === 1) {
changeCatalog(catalogData[0]);
} else if (
catalogData &&
!catalogData.find(
catalogOption => catalogRef.current === catalogOption.value,
)
) {
// Current selection not in list - try to apply default on first load
if (
defaultCatalog &&
appliedCatalogDefaultRef.current !== defaultCatalog
) {
const defaultOption = catalogData.find(
c => c.value === defaultCatalog,
);
if (defaultOption) {
appliedCatalogDefaultRef.current = defaultCatalog;
changeCatalog(defaultOption);
} else {
changeCatalog(undefined);
}
} else {
changeCatalog(undefined);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [catalogData, defaultCatalog, loadingCatalogs, showCatalogSelector]);
function changeDatabase(
value: { label: string; value: number },
database: DatabaseValue,
@@ -326,6 +384,9 @@ export function DatabaseSelector({
setCurrentDb(databaseWithId);
setCurrentCatalog(undefined);
setCurrentSchema(undefined);
// Reset default refs so defaults can be applied for the new database
appliedCatalogDefaultRef.current = null;
appliedSchemaDefaultRef.current = null;
if (onDbChange) {
onDbChange(databaseWithId);
}

View File

@@ -29,14 +29,14 @@ import {
DATASOURCE_ENDPOINT,
setupDatasourceEditorMocks,
cleanupAsyncOperations,
asyncRender,
fastRender,
dismissDatasourceWarning,
} from './DatasourceEditor.test.utils';
type MetricType = DatasetObject['metrics'][number];
// Factory function for currency props - returns fresh copy to prevent test pollution
// Using single metric to minimize DOM size for faster test execution while still validating currency functionality
// Using single metric to minimize DOM size for faster test execution
const createPropsWithCurrency = () => {
const baseProps = createProps();
return {
@@ -54,6 +54,22 @@ const createPropsWithCurrency = () => {
};
};
// Shared setup to navigate to expanded currency section
const setupCurrencySection = async () => {
await dismissDatasourceWarning();
// Navigate to metrics tab - use findBy which has built-in waiting
const metricButton = await screen.findByTestId('collection-tab-Metrics');
await userEvent.click(metricButton);
// Expand the metric row
const expandToggles = await screen.findAllByLabelText(/expand row/i);
await userEvent.click(expandToggles[0]);
// Wait for currency section to be visible
await screen.findByText('Metric currency');
};
beforeEach(() => {
fetchMock.get(DATASOURCE_ENDPOINT, [], { overwriteRoutes: true });
setupDatasourceEditorMocks();
@@ -66,99 +82,57 @@ afterEach(async () => {
test('renders currency section in metrics tab', async () => {
const testProps = createPropsWithCurrency();
await asyncRender(testProps);
fastRender(testProps);
await dismissDatasourceWarning();
await setupCurrencySection();
// Navigate to metrics tab
const metricButton = await screen.findByTestId('collection-tab-Metrics');
await userEvent.click(metricButton);
// Expand the single metric row with currency
const expandToggles = await screen.findAllByLabelText(/expand row/i);
await userEvent.click(expandToggles[0]);
// Check for currency section header
const currencyHeader = await screen.findByText('Metric currency');
expect(currencyHeader).toBeVisible();
// Verify currency position selector exists
const positionSelector = screen.getByRole('combobox', {
name: 'Currency prefix or suffix',
});
expect(positionSelector).toBeInTheDocument();
// Verify currency symbol selector exists
const symbolSelector = screen.getByRole('combobox', {
name: 'Currency symbol',
});
expect(symbolSelector).toBeInTheDocument();
// Verify currency selectors exist
expect(
screen.getByRole('combobox', { name: 'Currency prefix or suffix' }),
).toBeInTheDocument();
expect(
screen.getByRole('combobox', { name: 'Currency symbol' }),
).toBeInTheDocument();
});
// Allow extra headroom for dropdown render on slower CI runners
test('changes currency position from prefix to suffix', async () => {
const testProps = createPropsWithCurrency();
fastRender(testProps);
await asyncRender(testProps);
await setupCurrencySection();
await dismissDatasourceWarning();
// Navigate to metrics tab
const metricButton = await screen.findByTestId('collection-tab-Metrics');
await userEvent.click(metricButton);
// Expand the metric with currency
const expandToggles = await screen.findAllByLabelText(/expand row/i);
await userEvent.click(expandToggles[0]);
// Select suffix option via shared helper (rc-virtual-list aware)
await selectOption('Suffix', 'Currency prefix or suffix');
await cleanupAsyncOperations();
// Verify onChange was called with suffix position
await waitFor(() => {
expect(testProps.onChange).toHaveBeenCalledTimes(1);
const callArg = testProps.onChange.mock.calls[0][0];
const metrics = callArg.metrics || [];
const updatedMetric = metrics.find(
(m: MetricType) => m.currency && m.currency.symbolPosition === 'suffix',
);
expect(updatedMetric?.currency?.symbol).toBe('USD');
});
}, 30000);
// Allow extra headroom for dropdown render on slower CI runners
// Verify the exact call arguments
const callArg = testProps.onChange.mock.calls[0][0];
const metrics = callArg.metrics || [];
const updatedMetric = metrics.find(
(m: MetricType) => m.currency?.symbolPosition === 'suffix',
);
expect(updatedMetric?.currency?.symbol).toBe('USD');
}, 60000);
test('changes currency symbol from USD to GBP', async () => {
const testProps = createPropsWithCurrency();
fastRender(testProps);
await asyncRender(testProps);
await setupCurrencySection();
await dismissDatasourceWarning();
// Navigate to metrics tab
const metricButton = await screen.findByTestId('collection-tab-Metrics');
await userEvent.click(metricButton);
// Expand the metric with currency
const expandToggles = await screen.findAllByLabelText(/expand row/i);
await userEvent.click(expandToggles[0]);
// Select GBP option via shared helper (rc-virtual-list aware)
await selectOption('£ (GBP)', 'Currency symbol');
await cleanupAsyncOperations();
// Verify onChange was called with GBP
await waitFor(() => {
expect(testProps.onChange).toHaveBeenCalledTimes(1);
const callArg = testProps.onChange.mock.calls[0][0];
const metrics = callArg.metrics || [];
const updatedMetric = metrics.find(
(m: MetricType) => m.currency && m.currency.symbol === 'GBP',
);
expect(updatedMetric?.currency?.symbolPosition).toBe('prefix');
});
}, 30000);
// Verify the exact call arguments
const callArg = testProps.onChange.mock.calls[0][0];
const metrics = callArg.metrics || [];
const updatedMetric = metrics.find(
(m: MetricType) => m.currency?.symbol === 'GBP',
);
expect(updatedMetric?.currency?.symbolPosition).toBe('prefix');
}, 60000);

View File

@@ -0,0 +1,230 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
render,
screen,
waitFor,
fireEvent,
} from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { createRef } from 'react';
import SearchFilter from './Search';
import type { FilterHandler } from './types';
const mockOnSubmit = jest.fn();
const defaultProps = {
Header: 'Search Header',
name: 'search-input',
initialValue: '',
toolTipDescription: undefined,
onSubmit: mockOnSubmit,
autoComplete: undefined,
};
const setup = (props = {}) =>
render(<SearchFilter {...defaultProps} {...props} />);
beforeEach(() => {
mockOnSubmit.mockClear();
});
test('renders search filter with header', () => {
setup();
expect(screen.getByText('Search Header')).toBeInTheDocument();
});
test('renders input with placeholder', () => {
setup();
expect(screen.getByPlaceholderText('Type a value')).toBeInTheDocument();
});
test('renders search icon', () => {
setup();
expect(screen.getByTestId('filters-search')).toBeInTheDocument();
// The icon should be present as a prefix
const input = screen.getByTestId('filters-search');
expect(input.parentElement?.querySelector('.anticon-search')).toBeTruthy();
});
test('renders with initial value', () => {
setup({ initialValue: 'initial search term' });
const input = screen.getByTestId('filters-search') as HTMLInputElement;
expect(input.value).toBe('initial search term');
});
test('renders tooltip when toolTipDescription is provided', () => {
setup({ toolTipDescription: 'This is a helpful tooltip' });
const tooltip = screen.getByRole('img', { name: /info-circle/i });
expect(tooltip).toBeInTheDocument();
});
test('does not render tooltip when toolTipDescription is undefined', () => {
setup({ toolTipDescription: undefined });
const tooltip = screen.queryByRole('img', { name: /info-circle/i });
expect(tooltip).not.toBeInTheDocument();
});
test('updates input value on change', async () => {
setup();
const input = screen.getByTestId('filters-search') as HTMLInputElement;
await userEvent.type(input, 'test query');
expect(input.value).toBe('test query');
});
test('calls onSubmit with trimmed value on blur', async () => {
setup();
const input = screen.getByTestId('filters-search');
await userEvent.type(input, ' search term ');
await userEvent.tab(); // Trigger blur
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith('search term');
});
});
test('calls onSubmit with trimmed value on Enter key', async () => {
setup();
const input = screen.getByTestId('filters-search');
await userEvent.type(input, ' another search ');
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith('another search');
});
});
test('calls onSubmit with empty string when input is cleared', async () => {
setup({ initialValue: 'initial value' });
const input = screen.getByTestId('filters-search');
await userEvent.clear(input);
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith('');
});
});
test('does not call onSubmit when empty value is submitted on blur', async () => {
setup();
const input = screen.getByTestId('filters-search');
await userEvent.click(input);
await userEvent.tab(); // Trigger blur without typing
// onSubmit should not be called for empty value on blur/enter
expect(mockOnSubmit).not.toHaveBeenCalled();
});
test('calls onSubmit with empty string when only whitespace is entered and cleared', async () => {
setup();
const input = screen.getByTestId('filters-search');
await userEvent.type(input, ' ');
// When cleared (which happens when the value becomes empty), onSubmit is called with ''
await userEvent.clear(input);
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith('');
});
});
test('clearFilter resets value and calls onSubmit with empty string', async () => {
const ref = createRef<FilterHandler>();
render(<SearchFilter {...defaultProps} initialValue="test" ref={ref} />);
const input = screen.getByTestId('filters-search') as HTMLInputElement;
expect(input.value).toBe('test');
// Call clearFilter via ref
ref.current?.clearFilter();
await waitFor(() => {
expect(input.value).toBe('');
expect(mockOnSubmit).toHaveBeenCalledWith('');
});
});
test('uses custom name attribute when provided', () => {
setup({ name: 'custom-search-name' });
const input = screen.getByTestId('filters-search') as HTMLInputElement;
expect(input.name).toBe('custom-search-name');
});
test('sets autocomplete to off by default', () => {
setup();
const input = screen.getByTestId('filters-search') as HTMLInputElement;
expect(input.autocomplete).toBe('off');
});
test('uses custom autocomplete value when provided', () => {
setup({ autoComplete: 'new-password' });
const input = screen.getByTestId('filters-search') as HTMLInputElement;
expect(input.autocomplete).toBe('new-password');
});
test('renders with allowClear prop', () => {
setup({ initialValue: 'test value' });
const input = screen.getByTestId('filters-search');
// Ant Design Input with allowClear should have a clear button when there's a value
expect(input).toBeInTheDocument();
});
test('multiple rapid changes only submit the final value', async () => {
setup();
const input = screen.getByTestId('filters-search');
await userEvent.type(input, 'first');
await userEvent.type(input, ' second');
await userEvent.type(input, ' third');
await userEvent.tab();
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith('first second third');
// Should only be called once on blur, not for each keystroke
expect(mockOnSubmit).toHaveBeenCalledTimes(1);
});
});
test('handles programmatic value changes through clearFilter', async () => {
const ref = createRef<FilterHandler>();
render(
<SearchFilter {...defaultProps} initialValue="initial value" ref={ref} />,
);
const input = screen.getByTestId('filters-search') as HTMLInputElement;
// Type something new
await userEvent.clear(input);
await userEvent.type(input, 'new value');
expect(input.value).toBe('new value');
// Clear using ref
ref.current?.clearFilter();
await waitFor(() => {
expect(input.value).toBe('');
expect(mockOnSubmit).toHaveBeenCalledWith('');
});
});

View File

@@ -42,6 +42,7 @@ interface SearchHeaderProps extends BaseFilter {
onSubmit: (val: string) => void;
name: string;
toolTipDescription: string | undefined;
autoComplete?: string;
}
function SearchFilter(
@@ -51,6 +52,7 @@ function SearchFilter(
initialValue,
toolTipDescription,
onSubmit,
autoComplete = 'off',
}: SearchHeaderProps,
ref: RefObject<FilterHandler>,
) {
@@ -91,6 +93,7 @@ function SearchFilter(
allowClear
data-test="filters-search"
placeholder={t('Type a value')}
autoComplete={autoComplete}
name={name}
value={value}
onChange={handleChange}

View File

@@ -0,0 +1,132 @@
/**
* 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/helpers/testing-library';
import { ListViewFilterOperator } from '../types';
import UIFilters from './index';
const mockUpdateFilterValue = jest.fn();
beforeEach(() => {
mockUpdateFilterValue.mockClear();
});
test('search filter uses id as input name when inputName is not provided', () => {
const filters = [
{
Header: 'Name',
key: 'name',
id: 'name',
input: 'search' as const,
operator: ListViewFilterOperator.Contains,
},
];
render(
<UIFilters
filters={filters}
internalFilters={[]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
const input = screen.getByTestId('filters-search') as HTMLInputElement;
expect(input.name).toBe('name');
});
test('search filter uses inputName when provided instead of id', () => {
const filters = [
{
Header: 'Name',
key: 'name',
id: 'name',
input: 'search' as const,
operator: ListViewFilterOperator.Contains,
inputName: 'custom_search_name',
},
];
render(
<UIFilters
filters={filters}
internalFilters={[]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
const input = screen.getByTestId('filters-search') as HTMLInputElement;
expect(input.name).toBe('custom_search_name');
});
test('search filter passes autoComplete prop correctly', () => {
const filters = [
{
Header: 'Name',
key: 'name',
id: 'name',
input: 'search' as const,
operator: ListViewFilterOperator.Contains,
autoComplete: 'new-password',
},
];
render(
<UIFilters
filters={filters}
internalFilters={[]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
const input = screen.getByTestId('filters-search') as HTMLInputElement;
expect(input.autocomplete).toBe('new-password');
});
test('renders multiple search filters with different inputName values', () => {
const filters = [
{
Header: 'Name',
key: 'name',
id: 'name',
input: 'search' as const,
operator: ListViewFilterOperator.Contains,
inputName: 'filter_name_search',
},
{
Header: 'Description',
key: 'description',
id: 'description',
input: 'search' as const,
operator: ListViewFilterOperator.Contains,
// No inputName - should use id
},
];
render(
<UIFilters
filters={filters}
internalFilters={[]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
const inputs = screen.getAllByTestId('filters-search') as HTMLInputElement[];
expect(inputs).toHaveLength(2);
expect(inputs[0].name).toBe('filter_name_search');
expect(inputs[1].name).toBe('description');
});

View File

@@ -81,6 +81,8 @@ function UIFilters(
min,
max,
dropdownStyle,
autoComplete,
inputName,
},
index,
) => {
@@ -121,7 +123,7 @@ function UIFilters(
Header={Header}
initialValue={initialValue}
key={key}
name={id}
name={inputName ?? id}
toolTipDescription={toolTipDescription}
onSubmit={(value: string) => {
if (onFilterUpdate) {
@@ -130,6 +132,7 @@ function UIFilters(
updateFilterValue(index, value);
}}
autoComplete={autoComplete}
/>
);
}

View File

@@ -65,6 +65,8 @@ export interface ListViewFilter {
min?: number;
max?: number;
dropdownStyle?: React.CSSProperties;
autoComplete?: string;
inputName?: string;
}
export type ListViewFilters = ListViewFilter[];

View File

@@ -16,9 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ToastType } from 'src/components/MessageToasts/types';
import { ToastType, ToastMeta } from 'src/components/MessageToasts/types';
export default [
const mockMessageToasts: Partial<ToastMeta>[] = [
{ id: 'info_id', toastType: ToastType.Info, text: 'info toast' },
{ id: 'danger_id', toastType: ToastType.Danger, text: 'danger toast' },
];
export default mockMessageToasts;

View File

@@ -16,14 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import { t } from '@superset-ui/core';
const propTypes = {
height: PropTypes.number.isRequired,
};
interface MissingChartProps {
height: number;
}
export default function MissingChart({ height }) {
export default function MissingChart({ height }: MissingChartProps) {
return (
<div className="missing-chart-container" style={{ height: height + 20 }}>
<div className="missing-chart-body">
@@ -37,5 +36,3 @@ export default function MissingChart({ height }) {
</div>
);
}
MissingChart.propTypes = propTypes;

View File

@@ -17,13 +17,22 @@
* under the License.
*/
import { throttle } from 'lodash';
import { DropTargetMonitor } from 'react-dnd';
import { DASHBOARD_ROOT_TYPE } from 'src/dashboard/util/componentTypes';
import getDropPosition from 'src/dashboard/util/getDropPosition';
import type {
DragDroppableProps,
DragDroppableComponent,
} from './dragDroppableConfig';
import handleScroll from './handleScroll';
const HOVER_THROTTLE_MS = 100;
function handleHover(props, monitor, Component) {
function handleHover(
props: DragDroppableProps,
monitor: DropTargetMonitor,
Component: DragDroppableComponent,
): void {
// this may happen due to throttling
if (!Component.mounted) return;
@@ -40,7 +49,7 @@ function handleHover(props, monitor, Component) {
return;
}
Component?.props?.onHover();
Component?.props?.onHover?.();
Component.setState(() => ({
dropIndicator: dropPosition,

View File

@@ -20,7 +20,7 @@ let scrollTopDashboardInterval: any;
const SCROLL_STEP = 120;
const INTERVAL_DELAY = 50;
export default function handleScroll(scroll: string) {
export default function handleScroll(scroll: string | null) {
const setupScroll =
scroll === 'SCROLL_TOP' &&
!scrollTopDashboardInterval &&

View File

@@ -16,16 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import cx from 'classnames';
import { FormLabel } from '@superset-ui/core/components';
const propTypes = {
label: PropTypes.string.isRequired,
isSelected: PropTypes.bool.isRequired,
};
interface FilterFieldItemProps {
label: string;
isSelected: boolean;
}
export default function FilterFieldItem({ label, isSelected }) {
export default function FilterFieldItem({
label,
isSelected,
}: FilterFieldItemProps) {
return (
<span
className={cx('filter-field-item filter-container', {
@@ -36,5 +38,3 @@ export default function FilterFieldItem({ label, isSelected }) {
</span>
);
}
FilterFieldItem.propTypes = propTypes;

View File

@@ -16,47 +16,39 @@
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import CheckboxTree from 'react-checkbox-tree';
import { filterScopeSelectorTreeNodePropShape } from 'src/dashboard/util/propShapes';
import CheckboxTree, { Node, OnCheckNode } from 'react-checkbox-tree';
import treeIcons from './treeIcons';
import renderFilterFieldTreeNodes from './renderFilterFieldTreeNodes';
import renderFilterFieldTreeNodes, {
FilterScopeTreeNode,
} from './renderFilterFieldTreeNodes';
const propTypes = {
activeKey: PropTypes.string,
nodes: PropTypes.arrayOf(filterScopeSelectorTreeNodePropShape).isRequired,
checked: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
).isRequired,
expanded: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
).isRequired,
onCheck: PropTypes.func.isRequired,
onExpand: PropTypes.func.isRequired,
onClick: PropTypes.func.isRequired,
};
const defaultProps = {
activeKey: null,
};
interface FilterFieldTreeProps {
activeKey?: string | null;
nodes: FilterScopeTreeNode[];
checked: (string | number)[];
expanded: (string | number)[];
onCheck: (checked: string[]) => void;
onExpand: (expanded: string[]) => void;
onClick: (node: OnCheckNode) => void;
}
export default function FilterFieldTree({
activeKey,
activeKey = null,
nodes = [],
checked = [],
expanded = [],
onClick,
onCheck,
onExpand,
}) {
}: FilterFieldTreeProps) {
return (
<CheckboxTree
showExpandAll
showNodeIcon={false}
expandOnClick
nodes={renderFilterFieldTreeNodes({ nodes, activeKey })}
checked={checked}
expanded={expanded}
nodes={renderFilterFieldTreeNodes({ nodes, activeKey }) as Node[]}
checked={checked.map(String)}
expanded={expanded.map(String)}
onClick={onClick}
onCheck={onCheck}
onExpand={onExpand}
@@ -64,6 +56,3 @@ export default function FilterFieldTree({
/>
);
}
FilterFieldTree.propTypes = propTypes;
FilterFieldTree.defaultProps = defaultProps;

View File

@@ -16,28 +16,20 @@
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import CheckboxTree from 'react-checkbox-tree';
import { filterScopeSelectorTreeNodePropShape } from 'src/dashboard/util/propShapes';
import renderFilterScopeTreeNodes from './renderFilterScopeTreeNodes';
import CheckboxTree, { Node } from 'react-checkbox-tree';
import renderFilterScopeTreeNodes, {
FilterScopeTreeNode,
} from './renderFilterScopeTreeNodes';
import treeIcons from './treeIcons';
const propTypes = {
nodes: PropTypes.arrayOf(filterScopeSelectorTreeNodePropShape).isRequired,
checked: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
).isRequired,
expanded: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
).isRequired,
onCheck: PropTypes.func.isRequired,
onExpand: PropTypes.func.isRequired,
selectedChartId: PropTypes.number,
};
const defaultProps = {
selectedChartId: null,
};
interface FilterScopeTreeProps {
nodes: FilterScopeTreeNode[];
checked: (string | number)[];
expanded: (string | number)[];
onCheck: (checked: string[]) => void;
onExpand: (expanded: string[]) => void;
selectedChartId?: number | null;
}
const NOOP = () => {};
@@ -47,16 +39,16 @@ export default function FilterScopeTree({
expanded = [],
onCheck,
onExpand,
selectedChartId,
}) {
selectedChartId = null,
}: FilterScopeTreeProps) {
return (
<CheckboxTree
showExpandAll
expandOnClick
showNodeIcon={false}
nodes={renderFilterScopeTreeNodes({ nodes, selectedChartId })}
checked={checked}
expanded={expanded}
nodes={renderFilterScopeTreeNodes({ nodes, selectedChartId }) as Node[]}
checked={checked.map(String)}
expanded={expanded.map(String)}
onCheck={onCheck}
onExpand={onExpand}
onClick={NOOP}
@@ -64,6 +56,3 @@ export default function FilterScopeTree({
/>
);
}
FilterScopeTree.propTypes = propTypes;
FilterScopeTree.defaultProps = defaultProps;

View File

@@ -16,23 +16,42 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ReactNode } from 'react';
import FilterFieldItem from './FilterFieldItem';
export default function renderFilterFieldTreeNodes({ nodes, activeKey }) {
if (!nodes) {
export interface FilterScopeTreeNode {
value: string | number;
label: string | ReactNode;
type?: string;
children?: FilterScopeTreeNode[];
}
interface RenderFilterFieldTreeNodesParams {
nodes: FilterScopeTreeNode[] | null;
activeKey?: string | null;
}
export default function renderFilterFieldTreeNodes({
nodes,
activeKey,
}: RenderFilterFieldTreeNodesParams): FilterScopeTreeNode[] {
if (!nodes || nodes.length === 0) {
return [];
}
const root = nodes[0];
const allFilterNodes = root.children;
const allFilterNodes = root.children || [];
const children = allFilterNodes.map(node => ({
...node,
children: node.children.map(child => {
children: (node.children || []).map(child => {
const { label, value } = child;
return {
...child,
label: (
<FilterFieldItem isSelected={value === activeKey} label={label} />
<FilterFieldItem
isSelected={value === activeKey}
label={String(label)}
/>
),
};
}),

View File

@@ -16,11 +16,29 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ReactNode } from 'react';
import cx from 'classnames';
import { styled } from '@apache-superset/core/ui';
import { Icons } from '@superset-ui/core/components/Icons';
import { CHART_TYPE } from 'src/dashboard/util/componentTypes';
export interface FilterScopeTreeNode {
value: string | number;
label: string | ReactNode;
type?: string;
children?: FilterScopeTreeNode[];
}
interface TraverseParams {
currentNode: FilterScopeTreeNode;
selectedChartId?: number | null;
}
interface RenderFilterScopeTreeNodesParams {
nodes: FilterScopeTreeNode[] | null;
selectedChartId?: number | null;
}
const ChartIcon = styled(Icons.BarChartOutlined)`
${({ theme }) => `
position: relative;
@@ -30,11 +48,10 @@ const ChartIcon = styled(Icons.BarChartOutlined)`
`}
`;
function traverse({ currentNode = {}, selectedChartId }) {
if (!currentNode) {
return null;
}
function traverse({
currentNode,
selectedChartId,
}: TraverseParams): FilterScopeTreeNode {
const { label, value, type, children } = currentNode;
if (children && children.length) {
const updatedChildren = children.map(child =>
@@ -44,7 +61,7 @@ function traverse({ currentNode = {}, selectedChartId }) {
...currentNode,
label: (
<span
className={cx(`filter-scope-type ${type.toLowerCase()}`, {
className={cx(`filter-scope-type ${type?.toLowerCase()}`, {
'selected-filter': selectedChartId === value,
})}
>
@@ -59,7 +76,7 @@ function traverse({ currentNode = {}, selectedChartId }) {
...currentNode,
label: (
<span
className={cx(`filter-scope-type ${type.toLowerCase()}`, {
className={cx(`filter-scope-type ${type?.toLowerCase()}`, {
'selected-filter': selectedChartId === value,
})}
>
@@ -69,7 +86,10 @@ function traverse({ currentNode = {}, selectedChartId }) {
};
}
export default function renderFilterScopeTreeNodes({ nodes, selectedChartId }) {
export default function renderFilterScopeTreeNodes({
nodes,
selectedChartId,
}: RenderFilterScopeTreeNodesParams): FilterScopeTreeNode[] {
if (!nodes) {
return [];
}

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { bindActionCreators } from 'redux';
import { bindActionCreators, Dispatch } from 'redux';
import { connect } from 'react-redux';
import DashboardGrid from '../components/DashboardGrid';
@@ -25,8 +25,9 @@ import {
resizeComponent,
} from '../actions/dashboardLayout';
import { setDirectPathToChild, setEditMode } from '../actions/dashboardState';
import { RootState } from 'src/dashboard/types';
function mapStateToProps({ dashboardState, dashboardInfo }) {
function mapStateToProps({ dashboardState, dashboardInfo }: RootState) {
return {
editMode: dashboardState.editMode,
canEdit: dashboardInfo.dash_edit_perm,
@@ -34,7 +35,7 @@ function mapStateToProps({ dashboardState, dashboardInfo }) {
};
}
function mapDispatchToProps(dispatch) {
function mapDispatchToProps(dispatch: Dispatch) {
return bindActionCreators(
{
handleComponentDrop,

View File

@@ -17,20 +17,21 @@
* under the License.
*/
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { bindActionCreators, Dispatch } from 'redux';
import { updateDashboardFiltersScope } from '../actions/dashboardFilters';
import { setUnsavedChanges } from '../actions/dashboardState';
import FilterScopeSelector from '../components/filterscope/FilterScopeSelector';
import { RootState } from 'src/dashboard/types';
function mapStateToProps({ dashboardLayout, dashboardFilters }) {
function mapStateToProps({ dashboardLayout, dashboardFilters }: RootState) {
return {
dashboardFilters,
layout: dashboardLayout.present,
};
}
function mapDispatchToProps(dispatch) {
function mapDispatchToProps(dispatch: Dispatch) {
return bindActionCreators(
{
updateDashboardFiltersScope,

View File

@@ -16,14 +16,19 @@
* specific language governing permissions and limitations
* under the License.
*/
import { bindActionCreators } from 'redux';
import { bindActionCreators, Dispatch, AnyAction } from 'redux';
import { connect } from 'react-redux';
import { fetchSlices, updateSlices } from '../actions/sliceEntities';
import SliceAdder from '../components/SliceAdder';
import { RootState } from 'src/dashboard/types';
interface OwnProps {
height?: number;
}
function mapStateToProps(
{ sliceEntities, dashboardInfo, dashboardState },
ownProps,
{ sliceEntities, dashboardInfo, dashboardState }: RootState,
ownProps: OwnProps,
) {
return {
height: ownProps.height,
@@ -38,14 +43,14 @@ function mapStateToProps(
};
}
function mapDispatchToProps(dispatch) {
function mapDispatchToProps(dispatch: Dispatch<AnyAction>) {
return bindActionCreators(
{
fetchSlices,
updateSlices,
},
} as any,
dispatch,
);
}
export default connect(mapStateToProps, mapDispatchToProps)(SliceAdder);
export default connect(mapStateToProps, mapDispatchToProps)(SliceAdder as any);

View File

@@ -18,13 +18,17 @@
*/
import { IN_COMPONENT_ELEMENT_TYPES } from './constants';
export default function getChartAndLabelComponentIdFromPath(directPathToChild) {
const result = {};
export default function getChartAndLabelComponentIdFromPath(
directPathToChild: (string | undefined)[],
): Record<string, string> {
const result: Record<string, string> = {};
if (directPathToChild.length > 0) {
const currentPath = directPathToChild.slice().filter(x => x !== undefined);
const currentPath = directPathToChild
.slice()
.filter((x): x is string => x !== undefined);
while (currentPath.length) {
const componentId = currentPath.pop();
const componentId = currentPath.pop()!;
const componentType = componentId.split('-')[0];
result[componentType.toLowerCase()] = componentId;

View File

@@ -22,11 +22,32 @@ import { getDashboardFilterKey } from './getDashboardFilterKey';
import { ALL_FILTERS_ROOT } from './constants';
import { DASHBOARD_ROOT_TYPE } from './componentTypes';
export default function getFilterFieldNodesTree({ dashboardFilters = {} }) {
interface DashboardFilter {
chartId: number;
filterName: string;
columns: Record<string, unknown>;
labels: Record<string, string>;
}
interface FilterFieldNode {
value: string | number;
label: string;
type?: string;
children?: FilterFieldNode[];
showCheckbox?: boolean;
}
interface GetFilterFieldNodesTreeParams {
dashboardFilters?: Record<string, DashboardFilter>;
}
export default function getFilterFieldNodesTree({
dashboardFilters = {},
}: GetFilterFieldNodesTreeParams): FilterFieldNode[] {
const allFilters = Object.values(dashboardFilters).map(dashboardFilter => {
const { chartId, filterName, columns, labels } = dashboardFilter;
const children = Object.keys(columns).map(column => ({
value: getDashboardFilterKey({ chartId, column }),
value: getDashboardFilterKey({ chartId: String(chartId), column }),
label: labels[column] || column,
}));
return {

View File

@@ -16,15 +16,28 @@
* specific language governing permissions and limitations
* under the License.
*/
export default function getFilterScopeParentNodes(nodes = [], depthLimit = -1) {
const parentNodes = [];
const traverse = (currentNode, depth) => {
interface FilterScopeTreeNode {
value?: string | number;
children?: FilterScopeTreeNode[];
}
export default function getFilterScopeParentNodes(
nodes: FilterScopeTreeNode[] = [],
depthLimit = -1,
): string[] {
const parentNodes: string[] = [];
const traverse = (
currentNode: FilterScopeTreeNode | undefined,
depth: number,
): void => {
if (!currentNode) {
return;
}
if (currentNode.children && (depthLimit === -1 || depth < depthLimit)) {
parentNodes.push(currentNode.value);
if (currentNode.value !== undefined) {
parentNodes.push(String(currentNode.value));
}
currentNode.children.forEach(child => traverse(child, depth + 1));
}
};

View File

@@ -18,10 +18,15 @@
*/
import { getChartIdAndColumnFromFilterKey } from './getDashboardFilterKey';
interface GetSelectedChartIdParams {
activeFilterField?: string | null;
checkedFilterFields: string[];
}
export default function getSelectedChartIdForFilterScopeTree({
activeFilterField,
checkedFilterFields,
}) {
}: GetSelectedChartIdParams): number | null {
// this function returns chart id based on current filter scope selector local state:
// 1. if in single-edit mode, return the chart id for selected filter field.
// 2. if in multi-edit mode, if all filter fields are from same chart id,

View File

@@ -16,14 +16,23 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Layout, LayoutItem } from 'src/dashboard/types';
import { TABS_TYPE, CHART_TYPE } from '../componentTypes';
interface FindNonTabChildChartIdsParams {
id: string;
layout: Layout;
}
// This function traverses the layout from the passed id, returning an array
// of any child chartIds NOT nested within a Tabs component. These helps us identify
// if the charts at a given "Tabs" level are loaded
function findNonTabChildChartIds({ id, layout }) {
const chartIds = [];
function recurseFromNode(node) {
function findNonTabChildChartIds({
id,
layout,
}: FindNonTabChildChartIdsParams): number[] {
const chartIds: number[] = [];
function recurseFromNode(node: LayoutItem | undefined): void {
if (node && node.type === CHART_TYPE) {
if (node.meta && node.meta.chartId) {
chartIds.push(node.meta.chartId);
@@ -49,10 +58,13 @@ function findNonTabChildChartIds({ id, layout }) {
}
// This method is called frequently, so cache results
let cachedLayout;
let cachedIdsLookup = {};
export default function findNonTabChildChartIdsWithCache({ id, layout }) {
if (cachedLayout === layout && cachedIdsLookup[id]) {
let cachedLayout: Layout | undefined;
let cachedIdsLookup: Record<string, number[]> = {};
export default function findNonTabChildChartIdsWithCache({
id,
layout,
}: FindNonTabChildChartIdsParams): number[] {
if (cachedLayout === layout && id in cachedIdsLookup) {
return cachedIdsLookup[id];
}
if (layout !== cachedLayout) {

View File

@@ -16,14 +16,33 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Layout, LayoutItem } from 'src/dashboard/types';
import { TAB_TYPE, DASHBOARD_GRID_TYPE } from '../componentTypes';
import { DASHBOARD_ROOT_ID } from '../constants';
import findNonTabChildChartIds from './findNonTabChildChartIds';
interface TopLevelNode {
id: string;
type: string;
parent_type: string | null;
parent_id: string | null;
index: number | null;
depth: number;
slice_ids: number[];
}
interface RecurseParams {
node: LayoutItem | undefined;
index?: number | null;
depth: number;
parentType?: string | null;
parentId?: string | null;
}
// This function traverses the layout to identify top grid + tab level components
// for which we track load times
function findTopLevelComponentIds(layout) {
const topLevelNodes = [];
function findTopLevelComponentIds(layout: Layout): TopLevelNode[] {
const topLevelNodes: TopLevelNode[] = [];
function recurseFromNode({
node,
@@ -31,7 +50,7 @@ function findTopLevelComponentIds(layout) {
depth,
parentType = null,
parentId = null,
}) {
}: RecurseParams): void {
if (!node) return;
let nextParentType = parentType;
@@ -79,9 +98,11 @@ function findTopLevelComponentIds(layout) {
}
// This method is called frequently, so cache results
let cachedLayout;
let cachedTopLevelNodes;
export default function findTopLevelComponentIdsWithCache(layout) {
let cachedLayout: Layout | undefined;
let cachedTopLevelNodes: TopLevelNode[] = [];
export default function findTopLevelComponentIdsWithCache(
layout: Layout,
): TopLevelNode[] {
if (layout === cachedLayout) {
return cachedTopLevelNodes;
}

View File

@@ -16,16 +16,31 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ChartState } from 'src/explore/types';
import { Layout } from 'src/dashboard/types';
import findTopLevelComponentIds from './findTopLevelComponentIds';
import childChartsDidLoad from './childChartsDidLoad';
interface GetLoadStatsParams {
layout: Layout;
chartQueries: Record<string, Partial<ChartState>>;
}
interface LoadStats {
didLoad: boolean;
id: string;
minQueryStartTime: number | null;
[key: string]: unknown;
}
export default function getLoadStatsPerTopLevelComponent({
layout,
chartQueries,
}) {
}: GetLoadStatsParams): Record<string, LoadStats> {
const topLevelComponents = findTopLevelComponentIds(layout);
const stats = {};
topLevelComponents.forEach(({ id, ...restStats }) => {
const stats: Record<string, LoadStats> = {};
topLevelComponents.forEach(topLevelComponent => {
const { id, ...restStats } = topLevelComponent;
const { didLoad, minQueryStartTime } = childChartsDidLoad({
id,
layout,

View File

@@ -18,19 +18,31 @@
*/
import { getChartIdAndColumnFromFilterKey } from './getDashboardFilterKey';
interface ActiveFilterEntry {
values: unknown;
}
type ActiveFiltersInput = Record<string, ActiveFilterEntry>;
type SerializedFilters = Record<string, Record<string, unknown>>;
// input: { [id_column1]: values, [id_column2]: values }
// output: { id: { column1: values, column2: values } }
export default function serializeActiveFilterValues(activeFilters) {
return Object.entries(activeFilters).reduce((map, entry) => {
const [filterKey, { values }] = entry;
const { chartId, column } = getChartIdAndColumnFromFilterKey(filterKey);
const entryByChartId = {
...map[chartId],
[column]: values,
};
return {
...map,
[chartId]: entryByChartId,
};
}, {});
export default function serializeActiveFilterValues(
activeFilters: ActiveFiltersInput,
): SerializedFilters {
return Object.entries(activeFilters).reduce<SerializedFilters>(
(map, entry) => {
const [filterKey, { values }] = entry;
const { chartId, column } = getChartIdAndColumnFromFilterKey(filterKey);
const entryByChartId = {
...map[chartId],
[column]: values,
};
return {
...map,
[chartId]: entryByChartId,
};
},
{},
);
}

View File

@@ -18,10 +18,22 @@
*/
import { logging } from '@superset-ui/core';
interface LayoutComponent {
id: string;
parents?: string[];
children?: string[];
[key: string]: unknown;
}
interface UpdateComponentParentsListParams {
currentComponent?: LayoutComponent | null;
layout?: Record<string, LayoutComponent>;
}
export default function updateComponentParentsList({
currentComponent,
layout = {},
}) {
}: UpdateComponentParentsListParams): void {
if (currentComponent && layout) {
if (layout[currentComponent.id]) {
const parentsList = Array.isArray(currentComponent.parents)

View File

@@ -437,7 +437,7 @@ describe('Additional actions tests', () => {
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export All Data'));
expect(await screen.findByText('Export to .CSV')).toBeInTheDocument();
@@ -461,7 +461,7 @@ describe('Additional actions tests', () => {
render(<ExploreHeader {...props} />, { useRedux: true });
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
// Now the submenu should exist
userEvent.hover(await screen.findByText('Export Current View'));
@@ -575,19 +575,12 @@ describe('Additional actions tests', () => {
test('Should call downloadAsImage when click on "Export screenshot (jpeg)"', async () => {
const props = createProps();
const spy = jest.spyOn(downloadAsImage, 'default');
render(<ExploreHeader {...props} />, {
useRedux: true,
});
await waitFor(() => {
expect(
screen.getByLabelText('Menu actions trigger'),
).toBeInTheDocument();
});
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export All Data'));
const downloadAsImageElement = await screen.findByText(
@@ -596,7 +589,7 @@ describe('Additional actions tests', () => {
userEvent.click(downloadAsImageElement);
await waitFor(() => {
expect(spy).toHaveBeenCalledTimes(1);
expect(spyDownloadAsImage.callCount).toBe(1);
});
});
@@ -606,7 +599,7 @@ describe('Additional actions tests', () => {
useRedux: true,
});
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export All Data'));
const exportCSVElement = await screen.findByText('Export to .CSV');
userEvent.click(exportCSVElement);
@@ -622,7 +615,7 @@ describe('Additional actions tests', () => {
});
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export All Data'));
const exportCSVElement = await screen.findByText('Export to .CSV');
userEvent.click(exportCSVElement);
@@ -636,7 +629,7 @@ describe('Additional actions tests', () => {
useRedux: true,
});
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export All Data'));
const exportJsonElement = await screen.findByText('Export to .JSON');
userEvent.click(exportJsonElement);
@@ -652,7 +645,7 @@ describe('Additional actions tests', () => {
});
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export All Data'));
const exportJsonElement = await screen.findByText('Export to .JSON');
userEvent.click(exportJsonElement);
@@ -667,7 +660,7 @@ describe('Additional actions tests', () => {
});
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export All Data'));
const exportCSVElement = await screen.findByText(
'Export to pivoted .CSV',
@@ -685,7 +678,7 @@ describe('Additional actions tests', () => {
});
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export All Data'));
const exportCSVElement = await screen.findByText(
'Export to pivoted .CSV',
@@ -700,7 +693,7 @@ describe('Additional actions tests', () => {
useRedux: true,
});
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export All Data'));
const exportExcelElement = await screen.findByText('Export to Excel');
userEvent.click(exportExcelElement);
@@ -715,7 +708,7 @@ describe('Additional actions tests', () => {
useRedux: true,
});
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export All Data'));
const exportExcelElement = await screen.findByText('Export to Excel');
userEvent.click(exportExcelElement);
@@ -788,7 +781,7 @@ describe('Additional actions tests', () => {
render(<ExploreHeader {...props} />, { useRedux: true });
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export Current View'));
// clear previous calls on the sinon spy you created in beforeEach
@@ -828,7 +821,7 @@ describe('Additional actions tests', () => {
render(<ExploreHeader {...props} />, { useRedux: true });
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export Current View'));
spyExportChart.resetHistory();
@@ -858,7 +851,7 @@ describe('Additional actions tests', () => {
render(<ExploreHeader {...props} />, { useRedux: true });
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export Current View'));
spyExportChart.resetHistory();
@@ -880,7 +873,7 @@ describe('Additional actions tests', () => {
render(<ExploreHeader {...props} />, { useRedux: true });
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export Current View'));
spyExportChart.resetHistory();
@@ -911,7 +904,7 @@ describe('Additional actions tests', () => {
render(<ExploreHeader {...props} />, { useRedux: true });
userEvent.click(await screen.findByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export Current View'));
spyExportChart.resetHistory();
@@ -931,7 +924,7 @@ describe('Additional actions tests', () => {
render(<ExploreHeader {...props} />, { useRedux: true });
userEvent.click(await screen.findByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export Current View'));
spyExportChart.resetHistory();
@@ -963,7 +956,7 @@ describe('Additional actions tests', () => {
render(<ExploreHeader {...props} />, { useRedux: true });
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Data Export Options'));
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export Current View'));
// server path expected → use the sinon spy and inspect call args

View File

@@ -16,22 +16,22 @@
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import { ColumnTypeLabel } from '@superset-ui/chart-controls';
import { AggregateOption as AggregateOptionType } from './types';
import aggregateOptionType from './aggregateOptionType';
interface AggregateOptionProps {
aggregate: AggregateOptionType;
showType?: boolean;
}
const propTypes = {
aggregate: aggregateOptionType,
showType: PropTypes.bool,
};
export default function AggregateOption({ aggregate, showType }) {
export default function AggregateOption({
aggregate,
showType,
}: AggregateOptionProps) {
return (
<div>
{showType && <ColumnTypeLabel type="aggregate" />}
{showType && <ColumnTypeLabel type={'aggregate' as any} />}
<span className="option-label">{aggregate.aggregate_name}</span>
</div>
);
}
AggregateOption.propTypes = propTypes;

View File

@@ -16,35 +16,40 @@
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import {
StyledColumnOption,
StyledMetricOption,
} from 'src/explore/components/optionRenderers';
import withToasts from 'src/components/MessageToasts/withToasts';
import AggregateOption from './AggregateOption';
import columnType from './columnType';
import aggregateOptionType from './aggregateOptionType';
import savedMetricType from './savedMetricType';
const propTypes = {
option: PropTypes.oneOfType([
columnType,
savedMetricType,
aggregateOptionType,
]).isRequired,
addWarningToast: PropTypes.func.isRequired,
};
interface MetricDefinitionOptionProps {
option: {
metric_name?: string;
column_name?: string;
aggregate_name?: string;
[key: string]: unknown;
};
addWarningToast: (message: string) => void;
}
function MetricDefinitionOption({ option, addWarningToast }) {
function MetricDefinitionOption({
option,
addWarningToast,
}: MetricDefinitionOptionProps) {
if (option.metric_name) {
return <StyledMetricOption metric={option} showType />;
return <StyledMetricOption metric={option as any} showType />;
}
if (option.column_name) {
return <StyledColumnOption column={option} showType />;
return <StyledColumnOption column={option as any} showType />;
}
if (option.aggregate_name) {
return <AggregateOption aggregate={option} showType />;
return (
<AggregateOption
aggregate={{ aggregate_name: option.aggregate_name }}
showType
/>
);
}
addWarningToast(
'You must supply either a saved metric, column or aggregate to MetricDefinitionOption',
@@ -52,6 +57,4 @@ function MetricDefinitionOption({ option, addWarningToast }) {
return null;
}
MetricDefinitionOption.propTypes = propTypes;
export default withToasts(MetricDefinitionOption);

View File

@@ -20,8 +20,26 @@
import * as actions from '../actions/saveModalActions';
import { HYDRATE_EXPLORE } from '../actions/hydrateExplore';
export default function saveModalReducer(state = {}, action) {
const actionHandlers = {
interface SaveModalState {
isVisible?: boolean;
dashboards?: unknown[];
saveModalAlert?: string;
data?: unknown;
}
interface SaveModalAction {
type: string;
isVisible?: boolean;
choices?: unknown[];
userId?: string;
data?: unknown;
}
export default function saveModalReducer(
state: SaveModalState = {},
action: SaveModalAction,
): SaveModalState {
const actionHandlers: Record<string, () => SaveModalState> = {
[actions.SET_SAVE_CHART_MODAL_VISIBILITY]() {
return { ...state, isVisible: action.isVisible };
},
@@ -37,11 +55,12 @@ export default function saveModalReducer(state = {}, action) {
[actions.SAVE_SLICE_FAILED]() {
return { ...state, saveModalAlert: 'Failed to save slice' };
},
[actions.SAVE_SLICE_SUCCESS](data) {
return { ...state, data };
[actions.SAVE_SLICE_SUCCESS]() {
return { ...state, data: action.data };
},
[HYDRATE_EXPLORE]() {
return { ...action.data.saveModal };
const payload = action.data as { saveModal?: SaveModalState } | undefined;
return { ...payload?.saveModal };
},
};

View File

@@ -105,301 +105,402 @@ const defaultProps = {
otherTabTitle: 'Examples',
};
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('DashboardTable', () => {
const history = createMemoryHistory();
const store = configureStore({
reducer: {
dashboards: (state = { dashboards: [] }) => state,
const history = createMemoryHistory();
const store = configureStore({
reducer: {
dashboards: (state = { dashboards: [] }) => state,
},
preloadedState: {
dashboards: {
dashboards: mockDashboards,
},
preloadedState: {
dashboards: {
dashboards: mockDashboards,
},
},
});
},
});
beforeEach(() => {
jest.spyOn(SupersetClient, 'get').mockImplementation(() =>
Promise.resolve({
json: {
result: mockDashboards[0],
},
response: new Response(),
}),
);
fetchMock.get(
'glob:*/api/v1/dashboard/*',
{
beforeEach(() => {
jest.spyOn(SupersetClient, 'get').mockImplementation(() =>
Promise.resolve({
json: {
result: mockDashboards[0],
},
{ overwriteRoutes: true },
); // Add overwriteRoutes option
response: new Response(),
}),
);
// Mock loading state for first render
jest.spyOn(hooks, 'useListViewResource').mockImplementationOnce(() => ({
state: {
loading: true,
resourceCollection: [],
resourceCount: 0,
bulkSelectEnabled: false,
lastFetched: undefined,
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: jest.fn(),
fetchData: jest.fn(),
toggleBulkSelect: jest.fn(),
}));
});
fetchMock.get(
'glob:*/api/v1/dashboard/*',
{
result: mockDashboards[0],
},
{ overwriteRoutes: true },
); // Add overwriteRoutes option
test('renders loading state initially', () => {
render(
<Router history={history}>
<DashboardTable {...defaultProps} />
</Router>,
{ store },
);
expect(screen.getByRole('img', { name: 'empty' })).toBeInTheDocument();
});
// Mock loading state for first render
jest.spyOn(hooks, 'useListViewResource').mockImplementationOnce(() => ({
state: {
loading: true,
resourceCollection: [],
resourceCount: 0,
bulkSelectEnabled: false,
lastFetched: undefined,
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: jest.fn(),
fetchData: jest.fn(),
toggleBulkSelect: jest.fn(),
}));
});
test('renders empty state when no dashboards', async () => {
render(
<Router history={history}>
<DashboardTable {...defaultProps} />
</Router>,
{ store },
);
test('renders loading state initially', () => {
render(
<Router history={history}>
<DashboardTable {...defaultProps} />
</Router>,
{ store },
);
expect(screen.getByRole('img', { name: 'empty' })).toBeInTheDocument();
});
await waitFor(() => {
expect(screen.getByText('No results')).toBeInTheDocument();
});
});
test('renders empty state when no dashboards', async () => {
render(
<Router history={history}>
<DashboardTable {...defaultProps} />
</Router>,
{ store },
);
test('renders dashboard cards when data is loaded', async () => {
jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({
state: {
loading: false,
resourceCollection: mockDashboards,
resourceCount: mockDashboards.length,
bulkSelectEnabled: false,
lastFetched: new Date().toISOString(),
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: jest.fn(),
fetchData: jest.fn(),
toggleBulkSelect: jest.fn(),
}));
render(
<Router history={history}>
<DashboardTable {...defaultProps} mine={mockDashboards} />
</Router>,
{ store },
);
await waitFor(() => {
mockDashboards.forEach(dashboard => {
expect(screen.getByText(dashboard.dashboard_title)).toBeInTheDocument();
});
});
});
test('switches to Mine tab correctly', async () => {
const props = {
...defaultProps,
mine: mockDashboards,
};
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
const mineTab = screen.getByRole('menuitem', { name: /mine/i });
await userEvent.click(mineTab);
await waitFor(() => {
expect(mineTab).toHaveClass('ant-menu-item-selected');
});
});
test('handles create dashboard button click', async () => {
const assignMock = jest.fn();
Object.defineProperty(window, 'location', {
value: { assign: assignMock },
writable: true,
});
render(
<Router history={history}>
<DashboardTable {...defaultProps} />
</Router>,
{ store },
);
const createButton = screen.getByRole('button', { name: /dashboard$/i });
await userEvent.click(createButton);
expect(assignMock).toHaveBeenCalledWith('/dashboard/new');
});
test('switches to Other tab when available', async () => {
const props = {
...defaultProps,
otherTabData: mockDashboards,
otherTabTitle: 'Examples',
};
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
const otherTab = screen.getByRole('tab', { name: 'Examples' });
await userEvent.click(otherTab);
expect(otherTab).toHaveClass('active');
});
test('handles bulk dashboard export with correct ID and shows spinner', async () => {
// Mock export to take some time before calling the done callback
mockExport.mockImplementation(
(resource: string, ids: number[], done: () => void) =>
new Promise(resolve => {
setTimeout(() => {
done();
resolve();
}, 100);
}),
);
const props = {
...defaultProps,
mine: mockDashboards,
};
jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({
state: {
loading: false,
resourceCollection: mockDashboards,
resourceCount: mockDashboards.length,
bulkSelectEnabled: false,
lastFetched: new Date().toISOString(),
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: jest.fn(),
fetchData: jest.fn(),
toggleBulkSelect: jest.fn(),
}));
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
const moreOptionsButton = screen.getAllByRole('img', {
name: 'more',
})[0];
await userEvent.click(moreOptionsButton);
// Wait for dropdown menu to appear
await waitFor(() => {
expect(screen.getByText('Export')).toBeInTheDocument();
});
const exportOption = screen.getByText('Export');
await userEvent.click(exportOption);
// Verify spinner shows up during export
await waitFor(() => {
expect(screen.getByRole('status')).toBeInTheDocument();
});
// Verify the export was called with correct parameters
expect(mockExport).toHaveBeenCalledWith(
'dashboard',
[1],
expect.any(Function),
);
// Wait for export to complete and spinner to disappear
await waitFor(
() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
},
{ timeout: 3000 },
);
});
test('handles dashboard deletion confirmation', async () => {
const props = {
...defaultProps,
mine: mockDashboards,
};
const refreshDataMock = jest.fn();
jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({
state: {
loading: false,
resourceCollection: mockDashboards,
resourceCount: mockDashboards.length,
bulkSelectEnabled: false,
lastFetched: new Date().toISOString(),
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: refreshDataMock,
fetchData: jest.fn(),
toggleBulkSelect: jest.fn(),
}));
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
const moreOptionsButton = screen.getAllByLabelText('more')[0];
await userEvent.click(moreOptionsButton);
await waitFor(() => {
expect(screen.getByText('Delete')).toBeInTheDocument();
});
const deleteOption = screen.getByText('Delete');
await userEvent.click(deleteOption);
// Verify Delete button is initially disabled
const confirmDeleteButton = screen.getByTestId('modal-confirm-button');
expect(confirmDeleteButton).toBeDisabled();
// Type DELETE in the confirmation input
const deleteInput = screen.getByTestId('delete-modal-input');
await userEvent.type(deleteInput, 'DELETE');
// Verify Delete button becomes enabled
await waitFor(() => {
expect(confirmDeleteButton).toBeEnabled();
});
// Click the now-enabled Delete button
await userEvent.click(confirmDeleteButton);
await waitFor(
() => {
expect(refreshDataMock).toHaveBeenCalled();
},
{ timeout: 3000 },
);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('No results')).toBeInTheDocument();
});
});
test('renders dashboard cards when data is loaded', async () => {
jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({
state: {
loading: false,
resourceCollection: mockDashboards,
resourceCount: mockDashboards.length,
bulkSelectEnabled: false,
lastFetched: new Date().toISOString(),
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: jest.fn(),
fetchData: jest.fn(),
toggleBulkSelect: jest.fn(),
}));
render(
<Router history={history}>
<DashboardTable {...defaultProps} mine={mockDashboards} />
</Router>,
{ store },
);
await waitFor(() => {
mockDashboards.forEach(dashboard => {
expect(screen.getByText(dashboard.dashboard_title)).toBeInTheDocument();
});
});
});
test('switches to Mine tab correctly', async () => {
const props = {
...defaultProps,
mine: mockDashboards,
};
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
const mineTab = screen.getByRole('menuitem', { name: /mine/i });
await userEvent.click(mineTab);
await waitFor(() => {
expect(mineTab).toHaveClass('ant-menu-item-selected');
});
});
test('handles create dashboard button click', async () => {
const assignMock = jest.fn();
Object.defineProperty(window, 'location', {
value: { assign: assignMock },
writable: true,
});
render(
<Router history={history}>
<DashboardTable {...defaultProps} />
</Router>,
{ store },
);
const createButton = screen.getByRole('button', { name: /dashboard$/i });
await userEvent.click(createButton);
expect(assignMock).toHaveBeenCalledWith('/dashboard/new');
});
test('switches to Other tab when available', async () => {
const props = {
...defaultProps,
otherTabData: mockDashboards,
otherTabTitle: 'Examples',
};
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
const otherTab = screen.getByRole('tab', { name: 'Examples' });
await userEvent.click(otherTab);
expect(otherTab).toHaveClass('active');
});
test('handles bulk dashboard export with correct ID and shows spinner', async () => {
// Mock export to take some time before calling the done callback
mockExport.mockImplementation(
(resource: string, ids: number[], done: () => void) =>
new Promise(resolve => {
setTimeout(() => {
done();
resolve();
}, 100);
}),
);
const props = {
...defaultProps,
mine: mockDashboards,
};
jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({
state: {
loading: false,
resourceCollection: mockDashboards,
resourceCount: mockDashboards.length,
bulkSelectEnabled: false,
lastFetched: new Date().toISOString(),
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: jest.fn(),
fetchData: jest.fn(),
toggleBulkSelect: jest.fn(),
}));
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
const moreOptionsButton = screen.getAllByRole('img', {
name: 'more',
})[0];
await userEvent.click(moreOptionsButton);
// Wait for dropdown menu to appear
await waitFor(() => {
expect(screen.getByText('Export')).toBeInTheDocument();
});
const exportOption = screen.getByText('Export');
await userEvent.click(exportOption);
// Verify spinner shows up during export
await waitFor(() => {
expect(screen.getByRole('status')).toBeInTheDocument();
});
// Verify the export was called with correct parameters
expect(mockExport).toHaveBeenCalledWith(
'dashboard',
[1],
expect.any(Function),
);
// Wait for export to complete and spinner to disappear
await waitFor(
() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
},
{ timeout: 3000 },
);
});
test('handles dashboard deletion confirmation', async () => {
const props = {
...defaultProps,
mine: mockDashboards,
};
const refreshDataMock = jest.fn();
jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({
state: {
loading: false,
resourceCollection: mockDashboards,
resourceCount: mockDashboards.length,
bulkSelectEnabled: false,
lastFetched: new Date().toISOString(),
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: refreshDataMock,
fetchData: jest.fn(),
toggleBulkSelect: jest.fn(),
}));
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
const moreOptionsButton = screen.getAllByLabelText('more')[0];
await userEvent.click(moreOptionsButton);
await waitFor(() => {
expect(screen.getByText('Delete')).toBeInTheDocument();
});
const deleteOption = screen.getByText('Delete');
await userEvent.click(deleteOption);
// Verify Delete button is initially disabled
const confirmDeleteButton = screen.getByTestId('modal-confirm-button');
expect(confirmDeleteButton).toBeDisabled();
// Type DELETE in the confirmation input
const deleteInput = screen.getByTestId('delete-modal-input');
await userEvent.type(deleteInput, 'DELETE');
// Verify Delete button becomes enabled
await waitFor(() => {
expect(confirmDeleteButton).toBeEnabled();
});
// Click the now-enabled Delete button
await userEvent.click(confirmDeleteButton);
await waitFor(
() => {
expect(refreshDataMock).toHaveBeenCalled();
},
{ timeout: 3000 },
);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
test('passes correct parameters to handleDashboardDelete for Other tab', async () => {
const mockHandleDashboardDelete =
require('src/views/CRUD/utils').handleDashboardDelete;
mockHandleDashboardDelete.mockClear();
const refreshDataMock = jest.fn();
const fetchDataMock = jest.fn().mockName('getData');
jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({
state: {
loading: false,
resourceCollection: mockDashboards,
resourceCount: mockDashboards.length,
bulkSelectEnabled: false,
lastFetched: new Date().toISOString(),
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: refreshDataMock,
fetchData: fetchDataMock,
toggleBulkSelect: jest.fn(),
}));
const props = {
...defaultProps,
otherTabData: mockDashboards,
otherTabTitle: 'All',
};
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
await waitFor(() => {
expect(screen.getByText('Test Dashboard 1')).toBeInTheDocument();
});
const otherTab = screen.getByRole('tab', { name: 'All' });
await userEvent.click(otherTab);
await waitFor(() => {
expect(screen.getByText('Test Dashboard 1')).toBeInTheDocument();
});
const moreOptionsButtons = screen.getAllByLabelText(/more|options/i);
expect(moreOptionsButtons.length).toBeGreaterThan(0);
await userEvent.click(moreOptionsButtons[0]);
await waitFor(() => {
expect(screen.getByText('Delete')).toBeInTheDocument();
});
const deleteOption = screen.getByText('Delete');
await userEvent.click(deleteOption);
await waitFor(() => {
expect(screen.getByText('Please confirm')).toBeInTheDocument();
expect(screen.getByTestId('delete-modal-input')).toBeInTheDocument();
});
const deleteInput = screen.getByTestId('delete-modal-input');
await userEvent.type(deleteInput, 'DELETE');
const confirmDeleteButton = screen.getByTestId('modal-confirm-button');
await waitFor(() => {
expect(confirmDeleteButton).toBeEnabled();
});
await userEvent.click(confirmDeleteButton);
await waitFor(() => {
expect(mockHandleDashboardDelete).toHaveBeenCalled();
});
expect(mockHandleDashboardDelete).toHaveBeenCalledWith(
expect.objectContaining({
id: 1,
dashboard_title: 'Test Dashboard 1',
}),
expect.any(Function),
expect.any(Function),
expect.any(Function),
'Other',
mockUser.userId,
expect.any(Function),
);
const lastCall = mockHandleDashboardDelete.mock.calls[0];
const getDataParam = lastCall[6];
getDataParam('Other');
expect(fetchDataMock).toHaveBeenCalledWith({
filters: [],
pageIndex: 0,
pageSize: 5,
sortBy: [{ desc: true, id: 'changed_on_delta_humanized' }],
});
});

View File

@@ -242,6 +242,7 @@ function DashboardTable({
addDangerToast,
activeTab,
user?.userId,
getData,
);
setDashboardToDelete(null);
}}

View File

@@ -0,0 +1,204 @@
/**
* 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 rison from 'rison';
import fetchMock from 'fetch-mock';
import { act, renderHook } from '@testing-library/react-hooks';
import {
createWrapper,
defaultStore as store,
} from 'spec/helpers/testing-library';
import { api } from 'src/hooks/apiResources/queryApi';
import { useCatalogs } from './catalogs';
const fakeApiResult = {
result: ['catalog_a', 'catalog_b'],
default: 'catalog_a',
};
const fakeApiResult2 = {
result: ['catalog_c', 'catalog_d'],
default: null,
};
const expectedResult = fakeApiResult.result.map((value: string) => ({
value,
label: value,
title: value,
}));
const expectedResult2 = fakeApiResult2.result.map((value: string) => ({
value,
label: value,
title: value,
}));
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('useCatalogs hook', () => {
beforeEach(() => {
fetchMock.reset();
store.dispatch(api.util.resetApiState());
});
test('returns api response mapping json result with default catalog', async () => {
const expectDbId = 'db1';
const forceRefresh = false;
const catalogApiRoute = `glob:*/api/v1/database/${expectDbId}/catalogs/*`;
fetchMock.get(catalogApiRoute, fakeApiResult);
const onSuccess = jest.fn();
const { result, waitFor } = renderHook(
() =>
useCatalogs({
dbId: expectDbId,
onSuccess,
}),
{
wrapper: createWrapper({
useRedux: true,
store,
}),
},
);
await waitFor(() =>
expect(fetchMock.calls(catalogApiRoute).length).toBe(1),
);
expect(result.current.data).toEqual(expectedResult);
expect(result.current.defaultCatalog).toBe('catalog_a');
expect(
fetchMock.calls(
`end:/api/v1/database/${expectDbId}/catalogs/?q=${rison.encode({
force: forceRefresh,
})}`,
).length,
).toBe(1);
expect(onSuccess).toHaveBeenCalledTimes(1);
act(() => {
result.current.refetch();
});
await waitFor(() =>
expect(fetchMock.calls(catalogApiRoute).length).toBe(2),
);
expect(
fetchMock.calls(
`end:/api/v1/database/${expectDbId}/catalogs/?q=${rison.encode({
force: true,
})}`,
).length,
).toBe(1);
expect(onSuccess).toHaveBeenCalledTimes(2);
expect(result.current.data).toEqual(expectedResult);
expect(result.current.defaultCatalog).toBe('catalog_a');
});
test('returns cached data without api request', async () => {
const expectDbId = 'db1';
const catalogApiRoute = `glob:*/api/v1/database/${expectDbId}/catalogs/*`;
fetchMock.get(catalogApiRoute, fakeApiResult);
const { result, rerender, waitFor } = renderHook(
() =>
useCatalogs({
dbId: expectDbId,
}),
{
wrapper: createWrapper({
useRedux: true,
store,
}),
},
);
await waitFor(() => expect(result.current.data).toEqual(expectedResult));
expect(result.current.defaultCatalog).toBe('catalog_a');
expect(fetchMock.calls(catalogApiRoute).length).toBe(1);
rerender();
await waitFor(() => expect(result.current.data).toEqual(expectedResult));
expect(result.current.defaultCatalog).toBe('catalog_a');
expect(fetchMock.calls(catalogApiRoute).length).toBe(1);
});
test('returns refreshed data after switching databases', async () => {
const expectDbId = 'db1';
const catalogApiRoute = `glob:*/api/v1/database/*/catalogs/*`;
fetchMock.get(catalogApiRoute, url =>
url.includes(expectDbId) ? fakeApiResult : fakeApiResult2,
);
const onSuccess = jest.fn();
const { result, rerender, waitFor } = renderHook(
({ dbId }) =>
useCatalogs({
dbId,
onSuccess,
}),
{
initialProps: { dbId: expectDbId },
wrapper: createWrapper({
useRedux: true,
store,
}),
},
);
await waitFor(() => expect(result.current.data).toEqual(expectedResult));
expect(result.current.defaultCatalog).toBe('catalog_a');
expect(fetchMock.calls(catalogApiRoute).length).toBe(1);
expect(onSuccess).toHaveBeenCalledTimes(1);
rerender({ dbId: 'db2' });
await waitFor(() => expect(result.current.data).toEqual(expectedResult2));
expect(result.current.defaultCatalog).toBeNull();
expect(fetchMock.calls(catalogApiRoute).length).toBe(2);
expect(onSuccess).toHaveBeenCalledTimes(2);
rerender({ dbId: expectDbId });
await waitFor(() => expect(result.current.data).toEqual(expectedResult));
expect(result.current.defaultCatalog).toBe('catalog_a');
expect(fetchMock.calls(catalogApiRoute).length).toBe(2);
expect(onSuccess).toHaveBeenCalledTimes(2);
// clean up cache
act(() => {
store.dispatch(api.util.invalidateTags(['Catalogs']));
});
await waitFor(() =>
expect(fetchMock.calls(catalogApiRoute).length).toBe(4),
);
expect(fetchMock.calls(catalogApiRoute)[2][0]).toContain(expectDbId);
await waitFor(() => expect(result.current.data).toEqual(expectedResult));
expect(result.current.defaultCatalog).toBe('catalog_a');
});
test('returns null defaultCatalog when API response has no default', async () => {
const expectDbId = 'db-no-default';
const catalogApiRoute = `glob:*/api/v1/database/${expectDbId}/catalogs/*`;
fetchMock.get(catalogApiRoute, { result: ['catalog1', 'catalog2'] });
const { result, waitFor } = renderHook(
() =>
useCatalogs({
dbId: expectDbId,
}),
{
wrapper: createWrapper({
useRedux: true,
store,
}),
},
);
await waitFor(() =>
expect(fetchMock.calls(catalogApiRoute).length).toBe(1),
);
expect(result.current.defaultCatalog).toBeNull();
});
});

View File

@@ -30,27 +30,39 @@ export type CatalogOption = {
export type FetchCatalogsQueryParams = {
dbId?: string | number;
forceRefresh: boolean;
onSuccess?: (data: CatalogOption[], isRefetched: boolean) => void;
onSuccess?: (
data: CatalogOption[],
isRefetched: boolean,
defaultCatalog: string | null,
) => void;
onError?: (error: ClientErrorObject) => void;
};
type Params = Omit<FetchCatalogsQueryParams, 'forceRefresh'>;
// Internal type for transformed API response
type CatalogsApiResponse = {
catalogs: CatalogOption[];
defaultCatalog: string | null;
};
const catalogApi = api.injectEndpoints({
endpoints: builder => ({
catalogs: builder.query<CatalogOption[], FetchCatalogsQueryParams>({
catalogs: builder.query<CatalogsApiResponse, FetchCatalogsQueryParams>({
providesTags: [{ type: 'Catalogs', id: 'LIST' }],
query: ({ dbId, forceRefresh }) => ({
endpoint: `/api/v1/database/${dbId}/catalogs/`,
urlParams: {
force: forceRefresh,
},
transformResponse: ({ json }: JsonResponse) =>
json.result.sort().map((value: string) => ({
transformResponse: ({ json }: JsonResponse) => ({
catalogs: json.result.sort().map((value: string) => ({
value,
label: value,
title: value,
})),
defaultCatalog: json.default ?? null,
}),
}),
serializeQueryArgs: ({ queryArgs: { dbId } }) => ({
dbId,
@@ -89,7 +101,11 @@ export function useCatalogs(options: Params) {
if (dbId && (!result.currentData || forceRefresh)) {
trigger({ dbId, forceRefresh }).then(({ isSuccess, isError, data }) => {
if (isSuccess) {
onSuccess?.(data || EMPTY_CATALOGS, forceRefresh);
onSuccess?.(
data?.catalogs || EMPTY_CATALOGS,
forceRefresh,
data?.defaultCatalog ?? null,
);
}
if (isError) {
onError?.(result.error as ClientErrorObject);
@@ -110,5 +126,7 @@ export function useCatalogs(options: Params) {
return {
...result,
refetch,
data: result.data?.catalogs,
defaultCatalog: result.data?.defaultCatalog ?? null,
};
}

View File

@@ -28,12 +28,15 @@ import { useSchemas } from './schemas';
const fakeApiResult = {
result: ['test schema 1', 'test schema b'],
default: 'test schema 1',
};
const fakeApiResult2 = {
result: ['test schema 2', 'test schema a'],
default: null,
};
const fakeApiResult3 = {
result: ['test schema 3', 'test schema c'],
default: 'test schema c',
};
const expectedResult = fakeApiResult.result.map((value: string) => ({
@@ -80,6 +83,7 @@ describe('useSchemas hook', () => {
);
await waitFor(() => expect(fetchMock.calls(schemaApiRoute).length).toBe(1));
expect(result.current.data).toEqual(expectedResult);
expect(result.current.defaultSchema).toBe('test schema 1');
expect(
fetchMock.calls(
`end:/api/v1/database/${expectDbId}/schemas/?q=${rison.encode({
@@ -120,9 +124,11 @@ describe('useSchemas hook', () => {
},
);
await waitFor(() => expect(result.current.data).toEqual(expectedResult));
expect(result.current.defaultSchema).toBe('test schema 1');
expect(fetchMock.calls(schemaApiRoute).length).toBe(1);
rerender();
await waitFor(() => expect(result.current.data).toEqual(expectedResult));
expect(result.current.defaultSchema).toBe('test schema 1');
expect(fetchMock.calls(schemaApiRoute).length).toBe(1);
});
@@ -148,23 +154,20 @@ describe('useSchemas hook', () => {
},
);
await waitFor(() =>
expect(result.current.currentData).toEqual(expectedResult),
);
await waitFor(() => expect(result.current.data).toEqual(expectedResult));
expect(result.current.defaultSchema).toBe('test schema 1');
expect(fetchMock.calls(schemaApiRoute).length).toBe(1);
expect(onSuccess).toHaveBeenCalledTimes(1);
rerender({ dbId: 'db2' });
await waitFor(() =>
expect(result.current.currentData).toEqual(expectedResult2),
);
await waitFor(() => expect(result.current.data).toEqual(expectedResult2));
expect(result.current.defaultSchema).toBeNull();
expect(fetchMock.calls(schemaApiRoute).length).toBe(2);
expect(onSuccess).toHaveBeenCalledTimes(2);
rerender({ dbId: expectDbId });
await waitFor(() =>
expect(result.current.currentData).toEqual(expectedResult),
);
await waitFor(() => expect(result.current.data).toEqual(expectedResult));
expect(result.current.defaultSchema).toBe('test schema 1');
expect(fetchMock.calls(schemaApiRoute).length).toBe(2);
expect(onSuccess).toHaveBeenCalledTimes(2);
@@ -175,9 +178,7 @@ describe('useSchemas hook', () => {
await waitFor(() => expect(fetchMock.calls(schemaApiRoute).length).toBe(4));
expect(fetchMock.calls(schemaApiRoute)[2][0]).toContain(expectDbId);
await waitFor(() =>
expect(result.current.currentData).toEqual(expectedResult),
);
await waitFor(() => expect(result.current.data).toEqual(expectedResult));
});
test('returns correct schema list by a catalog', async () => {
@@ -208,14 +209,37 @@ describe('useSchemas hook', () => {
await waitFor(() => expect(fetchMock.calls(schemaApiRoute).length).toBe(1));
expect(result.current.data).toEqual(expectedResult3);
expect(result.current.defaultSchema).toBe('test schema c');
expect(onSuccess).toHaveBeenCalledTimes(1);
rerender({ dbId, catalog: 'catalog2' });
await waitFor(() => expect(fetchMock.calls(schemaApiRoute).length).toBe(2));
expect(result.current.data).toEqual(expectedResult2);
expect(result.current.defaultSchema).toBeNull();
rerender({ dbId, catalog: expectCatalog });
expect(result.current.data).toEqual(expectedResult3);
expect(result.current.defaultSchema).toBe('test schema c');
expect(fetchMock.calls(schemaApiRoute).length).toBe(2);
});
test('returns null defaultSchema when API response has no default', async () => {
const expectDbId = 'db-no-default';
const schemaApiRoute = `glob:*/api/v1/database/${expectDbId}/schemas/*`;
fetchMock.get(schemaApiRoute, { result: ['schema1', 'schema2'] });
const { result, waitFor } = renderHook(
() =>
useSchemas({
dbId: expectDbId,
}),
{
wrapper: createWrapper({
useRedux: true,
store,
}),
},
);
await waitFor(() => expect(fetchMock.calls(schemaApiRoute).length).toBe(1));
expect(result.current.defaultSchema).toBeNull();
});
});

View File

@@ -31,15 +31,25 @@ export type FetchSchemasQueryParams = {
dbId?: string | number;
catalog?: string;
forceRefresh: boolean;
onSuccess?: (data: SchemaOption[], isRefetched: boolean) => void;
onSuccess?: (
data: SchemaOption[],
isRefetched: boolean,
defaultSchema: string | null,
) => void;
onError?: (error: ClientErrorObject) => void;
};
type Params = Omit<FetchSchemasQueryParams, 'forceRefresh'>;
// Internal type for transformed API response
type SchemasApiResponse = {
schemas: SchemaOption[];
defaultSchema: string | null;
};
const schemaApi = api.injectEndpoints({
endpoints: builder => ({
schemas: builder.query<SchemaOption[], FetchSchemasQueryParams>({
schemas: builder.query<SchemasApiResponse, FetchSchemasQueryParams>({
providesTags: [{ type: 'Schemas', id: 'LIST' }],
query: ({ dbId, catalog, forceRefresh }) => ({
endpoint: `/api/v1/database/${dbId}/schemas/`,
@@ -48,12 +58,14 @@ const schemaApi = api.injectEndpoints({
force: forceRefresh,
...(catalog !== undefined && { catalog }),
},
transformResponse: ({ json }: JsonResponse) =>
json.result.sort().map((value: string) => ({
transformResponse: ({ json }: JsonResponse) => ({
schemas: json.result.sort().map((value: string) => ({
value,
label: value,
title: value,
})),
defaultSchema: json.default ?? null,
}),
}),
serializeQueryArgs: ({ queryArgs: { dbId, catalog } }) => ({
dbId,
@@ -98,7 +110,11 @@ export function useSchemas(options: Params) {
trigger({ dbId, catalog, forceRefresh }).then(
({ isSuccess, isError, data }) => {
if (isSuccess) {
onSuccess?.(data || EMPTY_SCHEMAS, forceRefresh);
onSuccess?.(
data?.schemas || EMPTY_SCHEMAS,
forceRefresh,
data?.defaultSchema ?? null,
);
}
if (isError) {
onError?.(result.error as ClientErrorObject);
@@ -120,5 +136,7 @@ export function useSchemas(options: Params) {
return {
...result,
refetch,
data: result.currentData?.schemas,
defaultSchema: result.currentData?.defaultSchema ?? null,
};
}

View File

@@ -167,7 +167,7 @@ export const {
export function useTables(options: Params) {
const { dbId, catalog, schema, onSuccess, onError } = options || {};
const isMountedRef = useRef(false);
const { currentData: schemaOptions, isFetching } = useSchemas({
const { data: schemaOptions, isFetching } = useSchemas({
dbId,
catalog: catalog || undefined,
});

View File

@@ -471,6 +471,7 @@ function AlertList({
id: 'name',
input: 'search',
operator: FilterOperator.Contains,
inputName: 'alert_report_list_search',
},
{
Header: t('Owner'),

View File

@@ -255,6 +255,7 @@ function AnnotationLayersList({
id: 'name',
input: 'search',
operator: FilterOperator.Contains,
inputName: 'annotation_layer_list_search',
},
{
Header: t('Changed by'),

View File

@@ -299,6 +299,7 @@ function GroupsList({ user }: GroupsListProps) {
id: 'name',
input: 'search',
operator: ListViewFilterOperator.Contains,
inputName: 'group_list_search',
},
{
Header: t('Label'),

View File

@@ -300,6 +300,7 @@ function RolesList({ addDangerToast, addSuccessToast, user }: RolesListProps) {
id: 'name',
input: 'search',
operator: FilterOperator.Contains,
inputName: 'role_list_search',
},
{
Header: t('Users'),

View File

@@ -266,6 +266,7 @@ function RowLevelSecurityList(props: RLSProps) {
id: 'name',
input: 'search',
operator: FilterOperator.StartsWith,
inputName: 'rls_list_search',
},
{
Header: t('Filter Type'),

View File

@@ -266,6 +266,7 @@ function TagList(props: TagListProps) {
id: 'name',
input: 'search',
operator: FilterOperator.Contains,
inputName: 'tag_list_search',
},
{
Header: t('Modified by'),

View File

@@ -1,52 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import memoizeOne from 'memoize-one';
import { isControlPanelSectionConfig } from '@superset-ui/chart-controls';
import { getChartControlPanelRegistry } from '@superset-ui/core';
import { controls } from '../explore/controls';
const memoizedControls = memoizeOne((vizType, controlPanel) => {
const controlsMap = {};
(controlPanel?.controlPanelSections || [])
.filter(isControlPanelSectionConfig)
.forEach(section => {
section.controlSetRows.forEach(row => {
row.forEach(control => {
if (!control) return;
if (typeof control === 'string') {
// For now, we have to look in controls.jsx to get the config for some controls.
// Once everything is migrated out, delete this if statement.
controlsMap[control] = controls[control];
} else if (control.name && control.config) {
// condition needed because there are elements, e.g. <hr /> in some control configs (I'm looking at you, FilterBox!)
controlsMap[control.name] = control.config;
}
});
});
});
return controlsMap;
});
const getControlsForVizType = vizType => {
const controlPanel = getChartControlPanelRegistry().get(vizType);
return memoizedControls(vizType, controlPanel);
};
export default getControlsForVizType;

View File

@@ -1,71 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { nanoid } from 'nanoid';
export function addToObject(state, arrKey, obj) {
const newObject = { ...state[arrKey] };
const copiedObject = { ...obj };
if (!copiedObject.id) {
copiedObject.id = nanoid();
}
newObject[copiedObject.id] = copiedObject;
return { ...state, [arrKey]: newObject };
}
export function alterInObject(state, arrKey, obj, alterations) {
const newObject = { ...state[arrKey] };
newObject[obj.id] = { ...newObject[obj.id], ...alterations };
return { ...state, [arrKey]: newObject };
}
export function alterInArr(state, arrKey, obj, alterations) {
// Finds an item in an array in the state and replaces it with a
// new object with an altered property
const idKey = 'id';
const newArr = [];
state[arrKey].forEach(arrItem => {
if (obj[idKey] === arrItem[idKey]) {
newArr.push({ ...arrItem, ...alterations });
} else {
newArr.push(arrItem);
}
});
return { ...state, [arrKey]: newArr };
}
export function removeFromArr(state, arrKey, obj, idKey = 'id') {
const newArr = [];
state[arrKey].forEach(arrItem => {
if (!(obj[idKey] === arrItem[idKey])) {
newArr.push(arrItem);
}
});
return { ...state, [arrKey]: newArr };
}
export function addToArr(state, arrKey, obj) {
const newObj = { ...obj };
if (!newObj.id) {
newObj.id = nanoid();
}
const newState = {};
newState[arrKey] = [...state[arrKey], newObj];
return { ...state, ...newState };
}

View File

@@ -310,6 +310,7 @@ export function handleDashboardDelete(
addDangerToast: (arg0: string) => void,
dashboardFilter?: string,
userId?: string | number,
getData?: (tab: TableTab) => void,
) {
return SupersetClient.delete({
endpoint: `/api/v1/dashboard/${id}`,
@@ -333,6 +334,8 @@ export function handleDashboardDelete(
],
};
if (dashboardFilter === 'Mine') refreshData(filters);
else if (dashboardFilter === 'Other' && getData)
getData(dashboardFilter as TableTab);
else refreshData();
addSuccessToast(t('Deleted: %s', dashboardTitle));
},

View File

@@ -25,7 +25,7 @@
"@types/jest": "^29.5.14",
"@types/jsonwebtoken": "^9.0.10",
"@types/lodash": "^4.17.21",
"@types/node": "^25.0.2",
"@types/node": "^25.0.3",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.26.0",
@@ -40,7 +40,7 @@
"ts-node": "^10.9.2",
"tscw-config": "^1.1.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.49.0"
"typescript-eslint": "^8.50.0"
},
"engines": {
"node": "^20.19.4",
@@ -1822,9 +1822,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.0.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz",
"integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==",
"version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1874,17 +1874,17 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz",
"integrity": "sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz",
"integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/type-utils": "8.49.0",
"@typescript-eslint/utils": "8.49.0",
"@typescript-eslint/visitor-keys": "8.49.0",
"@typescript-eslint/scope-manager": "8.50.0",
"@typescript-eslint/type-utils": "8.50.0",
"@typescript-eslint/utils": "8.50.0",
"@typescript-eslint/visitor-keys": "8.50.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0"
@@ -1897,7 +1897,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.49.0",
"@typescript-eslint/parser": "^8.50.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
@@ -1913,16 +1913,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.49.0.tgz",
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz",
"integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/types": "8.49.0",
"@typescript-eslint/typescript-estree": "8.49.0",
"@typescript-eslint/visitor-keys": "8.49.0",
"@typescript-eslint/scope-manager": "8.50.0",
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/typescript-estree": "8.50.0",
"@typescript-eslint/visitor-keys": "8.50.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1938,14 +1938,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.49.0.tgz",
"integrity": "sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz",
"integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.49.0",
"@typescript-eslint/types": "^8.49.0",
"@typescript-eslint/tsconfig-utils": "^8.50.0",
"@typescript-eslint/types": "^8.50.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1960,14 +1960,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz",
"integrity": "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz",
"integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.49.0",
"@typescript-eslint/visitor-keys": "8.49.0"
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/visitor-keys": "8.50.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1978,9 +1978,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz",
"integrity": "sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz",
"integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1995,15 +1995,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz",
"integrity": "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz",
"integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.49.0",
"@typescript-eslint/typescript-estree": "8.49.0",
"@typescript-eslint/utils": "8.49.0",
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/typescript-estree": "8.50.0",
"@typescript-eslint/utils": "8.50.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -2020,9 +2020,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.49.0.tgz",
"integrity": "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz",
"integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2034,16 +2034,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz",
"integrity": "sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz",
"integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.49.0",
"@typescript-eslint/tsconfig-utils": "8.49.0",
"@typescript-eslint/types": "8.49.0",
"@typescript-eslint/visitor-keys": "8.49.0",
"@typescript-eslint/project-service": "8.50.0",
"@typescript-eslint/tsconfig-utils": "8.50.0",
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/visitor-keys": "8.50.0",
"debug": "^4.3.4",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
@@ -2088,16 +2088,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.49.0.tgz",
"integrity": "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz",
"integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/types": "8.49.0",
"@typescript-eslint/typescript-estree": "8.49.0"
"@typescript-eslint/scope-manager": "8.50.0",
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/typescript-estree": "8.50.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2112,13 +2112,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz",
"integrity": "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz",
"integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.49.0",
"@typescript-eslint/types": "8.50.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -6215,16 +6215,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.49.0.tgz",
"integrity": "sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.0.tgz",
"integrity": "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.49.0",
"@typescript-eslint/parser": "8.49.0",
"@typescript-eslint/typescript-estree": "8.49.0",
"@typescript-eslint/utils": "8.49.0"
"@typescript-eslint/eslint-plugin": "8.50.0",
"@typescript-eslint/parser": "8.50.0",
"@typescript-eslint/typescript-estree": "8.50.0",
"@typescript-eslint/utils": "8.50.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -7940,9 +7940,9 @@
"dev": true
},
"@types/node": {
"version": "25.0.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz",
"integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==",
"version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"dev": true,
"requires": {
"undici-types": "~7.16.0"
@@ -7990,16 +7990,16 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz",
"integrity": "sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz",
"integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==",
"dev": true,
"requires": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/type-utils": "8.49.0",
"@typescript-eslint/utils": "8.49.0",
"@typescript-eslint/visitor-keys": "8.49.0",
"@typescript-eslint/scope-manager": "8.50.0",
"@typescript-eslint/type-utils": "8.50.0",
"@typescript-eslint/utils": "8.50.0",
"@typescript-eslint/visitor-keys": "8.50.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0"
@@ -8014,75 +8014,75 @@
}
},
"@typescript-eslint/parser": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.49.0.tgz",
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz",
"integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
"dev": true,
"requires": {
"@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/types": "8.49.0",
"@typescript-eslint/typescript-estree": "8.49.0",
"@typescript-eslint/visitor-keys": "8.49.0",
"@typescript-eslint/scope-manager": "8.50.0",
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/typescript-estree": "8.50.0",
"@typescript-eslint/visitor-keys": "8.50.0",
"debug": "^4.3.4"
}
},
"@typescript-eslint/project-service": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.49.0.tgz",
"integrity": "sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz",
"integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==",
"dev": true,
"requires": {
"@typescript-eslint/tsconfig-utils": "^8.49.0",
"@typescript-eslint/types": "^8.49.0",
"@typescript-eslint/tsconfig-utils": "^8.50.0",
"@typescript-eslint/types": "^8.50.0",
"debug": "^4.3.4"
}
},
"@typescript-eslint/scope-manager": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz",
"integrity": "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz",
"integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.49.0",
"@typescript-eslint/visitor-keys": "8.49.0"
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/visitor-keys": "8.50.0"
}
},
"@typescript-eslint/tsconfig-utils": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz",
"integrity": "sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz",
"integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==",
"dev": true,
"requires": {}
},
"@typescript-eslint/type-utils": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz",
"integrity": "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz",
"integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.49.0",
"@typescript-eslint/typescript-estree": "8.49.0",
"@typescript-eslint/utils": "8.49.0",
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/typescript-estree": "8.50.0",
"@typescript-eslint/utils": "8.50.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
}
},
"@typescript-eslint/types": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.49.0.tgz",
"integrity": "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz",
"integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz",
"integrity": "sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz",
"integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==",
"dev": true,
"requires": {
"@typescript-eslint/project-service": "8.49.0",
"@typescript-eslint/tsconfig-utils": "8.49.0",
"@typescript-eslint/types": "8.49.0",
"@typescript-eslint/visitor-keys": "8.49.0",
"@typescript-eslint/project-service": "8.50.0",
"@typescript-eslint/tsconfig-utils": "8.50.0",
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/visitor-keys": "8.50.0",
"debug": "^4.3.4",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
@@ -8111,24 +8111,24 @@
}
},
"@typescript-eslint/utils": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.49.0.tgz",
"integrity": "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz",
"integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/types": "8.49.0",
"@typescript-eslint/typescript-estree": "8.49.0"
"@typescript-eslint/scope-manager": "8.50.0",
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/typescript-estree": "8.50.0"
}
},
"@typescript-eslint/visitor-keys": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz",
"integrity": "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz",
"integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.49.0",
"@typescript-eslint/types": "8.50.0",
"eslint-visitor-keys": "^4.2.1"
},
"dependencies": {
@@ -11101,15 +11101,15 @@
"dev": true
},
"typescript-eslint": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.49.0.tgz",
"integrity": "sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.0.tgz",
"integrity": "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==",
"dev": true,
"requires": {
"@typescript-eslint/eslint-plugin": "8.49.0",
"@typescript-eslint/parser": "8.49.0",
"@typescript-eslint/typescript-estree": "8.49.0",
"@typescript-eslint/utils": "8.49.0"
"@typescript-eslint/eslint-plugin": "8.50.0",
"@typescript-eslint/parser": "8.50.0",
"@typescript-eslint/typescript-estree": "8.50.0",
"@typescript-eslint/utils": "8.50.0"
}
},
"uglify-js": {

View File

@@ -33,7 +33,7 @@
"@types/jest": "^29.5.14",
"@types/jsonwebtoken": "^9.0.10",
"@types/lodash": "^4.17.21",
"@types/node": "^25.0.2",
"@types/node": "^25.0.3",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.26.0",
@@ -48,7 +48,7 @@
"ts-node": "^10.9.2",
"tscw-config": "^1.1.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.49.0"
"typescript-eslint": "^8.50.0"
},
"engines": {
"node": "^20.19.4",

View File

@@ -317,6 +317,32 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
"changed_by": [["id", BaseFilterRelatedUsers, lambda: []]],
}
@staticmethod
def _get_default_schema(
database: Database,
catalog: str | None,
accessible_schemas: set[str],
pk: int,
) -> str | None:
"""
Get the default schema for a database/catalog, with error handling.
Returns None if the default cannot be determined or is not accessible.
"""
try:
default_schema = database.get_default_schema(catalog)
# Only include if user has access to it
if default_schema and default_schema not in accessible_schemas:
return None
return default_schema
except Exception: # pylint: disable=broad-except
logger.debug(
"Could not get default schema for database %s, catalog %s",
pk,
catalog,
)
return None
@expose("/<int:pk>/connection", methods=("GET",))
@protect()
@safe
@@ -726,7 +752,18 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
database,
catalogs,
)
return self.response(200, result=list(catalogs))
# Get default catalog with error handling
default_catalog = None
try:
default_catalog = database.get_default_catalog()
# Only include if user has access to it
if default_catalog and default_catalog not in catalogs:
default_catalog = None
except Exception: # pylint: disable=broad-except
logger.debug("Could not get default catalog for database %s", pk)
return self.response(200, result=list(catalogs), default=default_catalog)
except OperationalError:
return self.response(
500,
@@ -795,23 +832,30 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
catalog,
schemas,
)
default_schema = self._get_default_schema(database, catalog, schemas, pk)
if params.get("upload_allowed"):
if not database.allow_file_upload:
return self.response(200, result=[])
return self.response(200, result=[], default=None)
if allowed_schemas := database.get_schema_access_for_file_upload():
# some databases might return the list of schemas in uppercase,
# while the list of allowed schemas is manually inputted so
# could be lowercase
allowed_schemas = {schema.lower() for schema in allowed_schemas}
filtered_schemas = [
schema
for schema in schemas
if schema.lower() in allowed_schemas
]
# Check if default is in filtered list
if default_schema and default_schema.lower() not in allowed_schemas:
default_schema = None
return self.response(
200,
result=[
schema
for schema in schemas
if schema.lower() in allowed_schemas
],
result=filtered_schemas,
default=default_schema,
)
return self.response(200, result=list(schemas))
return self.response(200, result=list(schemas), default=default_schema)
except OperationalError:
return self.response(
500, message="There was an error connecting to the database"

View File

@@ -742,12 +742,22 @@ class SchemasResponseSchema(Schema):
result = fields.List(
fields.String(metadata={"description": "A database schema name"})
)
default = fields.String(
allow_none=True,
load_default=None,
metadata={"description": "The default schema for this database/catalog"},
)
class CatalogsResponseSchema(Schema):
result = fields.List(
fields.String(metadata={"description": "A database catalog name"})
)
default = fields.String(
allow_none=True,
load_default=None,
metadata={"description": "The default catalog for this database"},
)
class DatabaseTablesResponse(Schema):

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