mirror of
https://github.com/apache/superset.git
synced 2026-06-14 03:59:22 +00:00
Compare commits
43 Commits
file-handl
...
default-db
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a2850a33c | ||
|
|
ff17daa424 | ||
|
|
89c5c55dcb | ||
|
|
db201285e5 | ||
|
|
1910a5c607 | ||
|
|
b377ce564b | ||
|
|
4c6df01353 | ||
|
|
b8f31124d0 | ||
|
|
da8e077a44 | ||
|
|
32435bc3e9 | ||
|
|
2cf0d7936e | ||
|
|
0830a57fa6 | ||
|
|
0f56e3b9ae | ||
|
|
ee45b26ad7 | ||
|
|
f3407d7a56 | ||
|
|
f51f7f3307 | ||
|
|
2f4f64dfe8 | ||
|
|
ae584c8886 | ||
|
|
b1e004e122 | ||
|
|
737a5162e4 | ||
|
|
b800412eda | ||
|
|
24a4f8510d | ||
|
|
33a425bbbc | ||
|
|
5ce4c52cfa | ||
|
|
c9ec173647 | ||
|
|
71f9dcff5a | ||
|
|
479b7a3fba | ||
|
|
594ea972ca | ||
|
|
d77f7b6d20 | ||
|
|
f4ded02e0d | ||
|
|
f97fa08477 | ||
|
|
789be78166 | ||
|
|
ea3d247017 | ||
|
|
6456f4c516 | ||
|
|
e9bbf06938 | ||
|
|
ebee35ea5a | ||
|
|
d0fb77cbc8 | ||
|
|
46659c2bd1 | ||
|
|
8407e9cf3b | ||
|
|
5eeba2e734 | ||
|
|
4ca8c000d1 | ||
|
|
7108658de0 | ||
|
|
42311f602e |
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
644
RESOURCES/INTHEWILD.yaml
Normal 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/
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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: [](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;
|
||||
}),
|
||||
);
|
||||
|
||||
165
docs/src/pages/inTheWild.tsx
Normal file
165
docs/src/pages/inTheWild.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
28
docs/src/types/yaml.d.ts
vendored
Normal 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;
|
||||
}
|
||||
@@ -25,6 +25,13 @@ export default function webpackExtendPlugin(): Plugin<void> {
|
||||
name: 'custom-webpack-plugin',
|
||||
configureWebpack(config) {
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
// Add YAML loader rule directly to existing rules
|
||||
config.module?.rules?.push({
|
||||
test: /\.ya?ml$/,
|
||||
use: 'js-yaml-loader',
|
||||
});
|
||||
|
||||
return {
|
||||
devtool: isDev ? 'eval-source-map' : config.devtool,
|
||||
...(isDev && {
|
||||
|
||||
|
Before Width: | Height: | Size: 476 KiB After Width: | Height: | Size: 476 KiB |
BIN
docs/static/img/extensions/gsheets-export.png
vendored
Normal file
BIN
docs/static/img/extensions/gsheets-export.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 379 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
19
docs/static/img/logos/preset.svg
vendored
Normal file
19
docs/static/img/logos/preset.svg
vendored
Normal 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
64
docs/static/llms.txt
vendored
Normal 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
|
||||
325
docs/yarn.lock
325
docs/yarn.lock
@@ -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"
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
5268
superset-frontend/package-lock.json
generated
5268
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@apache-superset/core",
|
||||
"version": "0.0.1-rc5",
|
||||
"version": "0.0.1-rc6",
|
||||
"description": "This package contains UI elements, APIs, and utility functions used by Superset.",
|
||||
"sideEffects": false,
|
||||
"main": "lib/index.js",
|
||||
@@ -17,7 +17,7 @@
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"install": "^0.13.0",
|
||||
"npm": "^11.1.0",
|
||||
"npm": "^11.7.0",
|
||||
"typescript": "^5.0.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@types/lodash": "^4.17.21",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"ag-grid-community": "34.3.1",
|
||||
"ag-grid-react": "34.3.1",
|
||||
"brace": "^0.11.1",
|
||||
"classnames": "^2.2.5",
|
||||
"classnames": "^2.5.1",
|
||||
"csstype": "^3.1.3",
|
||||
"core-js": "^3.38.1",
|
||||
"d3-format": "^1.3.2",
|
||||
@@ -78,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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -437,15 +437,21 @@ export function getLegendProps(
|
||||
zoomable = false,
|
||||
legendState?: LegendState,
|
||||
padding?: LegendPaddingType,
|
||||
): LegendComponentOption | LegendComponentOption[] {
|
||||
const legend: LegendComponentOption | LegendComponentOption[] = {
|
||||
): LegendComponentOption {
|
||||
const isHorizontal =
|
||||
orientation === LegendOrientation.Top ||
|
||||
orientation === LegendOrientation.Bottom;
|
||||
|
||||
const effectiveType =
|
||||
type === LegendType.Scroll || !isHorizontal ? type : LegendType.Scroll;
|
||||
const legend: LegendComponentOption = {
|
||||
orient: [LegendOrientation.Top, LegendOrientation.Bottom].includes(
|
||||
orientation,
|
||||
)
|
||||
? 'horizontal'
|
||||
: 'vertical',
|
||||
show,
|
||||
type,
|
||||
type: effectiveType,
|
||||
selected: legendState,
|
||||
selector: ['all', 'inverse'],
|
||||
selectorLabel: {
|
||||
|
||||
@@ -139,7 +139,6 @@ describe('Gantt transformProps', () => {
|
||||
legend: expect.objectContaining({
|
||||
show: true,
|
||||
type: 'scroll',
|
||||
selector: ['all', 'inverse'],
|
||||
}),
|
||||
tooltip: {
|
||||
formatter: expect.anything(),
|
||||
|
||||
@@ -891,14 +891,27 @@ describe('getLegendProps', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the correct props for plain type with bottom orientation', () => {
|
||||
it('should default plain legends to scroll for bottom orientation', () => {
|
||||
expect(
|
||||
getLegendProps(LegendType.Plain, LegendOrientation.Bottom, false, theme),
|
||||
).toEqual({
|
||||
show: false,
|
||||
bottom: 0,
|
||||
orient: 'horizontal',
|
||||
type: 'plain',
|
||||
type: 'scroll',
|
||||
...expectedThemeProps,
|
||||
});
|
||||
});
|
||||
|
||||
it('should default plain legends to scroll for top orientation', () => {
|
||||
expect(
|
||||
getLegendProps(LegendType.Plain, LegendOrientation.Top, false, theme),
|
||||
).toEqual({
|
||||
show: false,
|
||||
top: 0,
|
||||
right: 0,
|
||||
orient: 'horizontal',
|
||||
type: 'scroll',
|
||||
...expectedThemeProps,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "*",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/classnames": "*",
|
||||
"@types/react": "*",
|
||||
"match-sorter": "^6.3.3",
|
||||
"react": "^17.0.2",
|
||||
|
||||
@@ -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 } : {},
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -29,14 +29,14 @@ import {
|
||||
DATASOURCE_ENDPOINT,
|
||||
setupDatasourceEditorMocks,
|
||||
cleanupAsyncOperations,
|
||||
asyncRender,
|
||||
fastRender,
|
||||
dismissDatasourceWarning,
|
||||
} from './DatasourceEditor.test.utils';
|
||||
|
||||
type MetricType = DatasetObject['metrics'][number];
|
||||
|
||||
// Factory function for currency props - returns fresh copy to prevent test pollution
|
||||
// Using single metric to minimize DOM size for faster test execution while still validating currency functionality
|
||||
// Using single metric to minimize DOM size for faster test execution
|
||||
const createPropsWithCurrency = () => {
|
||||
const baseProps = createProps();
|
||||
return {
|
||||
@@ -54,6 +54,22 @@ const createPropsWithCurrency = () => {
|
||||
};
|
||||
};
|
||||
|
||||
// Shared setup to navigate to expanded currency section
|
||||
const setupCurrencySection = async () => {
|
||||
await dismissDatasourceWarning();
|
||||
|
||||
// Navigate to metrics tab - use findBy which has built-in waiting
|
||||
const metricButton = await screen.findByTestId('collection-tab-Metrics');
|
||||
await userEvent.click(metricButton);
|
||||
|
||||
// Expand the metric row
|
||||
const expandToggles = await screen.findAllByLabelText(/expand row/i);
|
||||
await userEvent.click(expandToggles[0]);
|
||||
|
||||
// Wait for currency section to be visible
|
||||
await screen.findByText('Metric currency');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.get(DATASOURCE_ENDPOINT, [], { overwriteRoutes: true });
|
||||
setupDatasourceEditorMocks();
|
||||
@@ -66,99 +82,57 @@ afterEach(async () => {
|
||||
|
||||
test('renders currency section in metrics tab', async () => {
|
||||
const testProps = createPropsWithCurrency();
|
||||
await asyncRender(testProps);
|
||||
fastRender(testProps);
|
||||
|
||||
await dismissDatasourceWarning();
|
||||
await setupCurrencySection();
|
||||
|
||||
// Navigate to metrics tab
|
||||
const metricButton = await screen.findByTestId('collection-tab-Metrics');
|
||||
await userEvent.click(metricButton);
|
||||
|
||||
// Expand the single metric row with currency
|
||||
const expandToggles = await screen.findAllByLabelText(/expand row/i);
|
||||
await userEvent.click(expandToggles[0]);
|
||||
|
||||
// Check for currency section header
|
||||
const currencyHeader = await screen.findByText('Metric currency');
|
||||
expect(currencyHeader).toBeVisible();
|
||||
|
||||
// Verify currency position selector exists
|
||||
const positionSelector = screen.getByRole('combobox', {
|
||||
name: 'Currency prefix or suffix',
|
||||
});
|
||||
expect(positionSelector).toBeInTheDocument();
|
||||
|
||||
// Verify currency symbol selector exists
|
||||
const symbolSelector = screen.getByRole('combobox', {
|
||||
name: 'Currency symbol',
|
||||
});
|
||||
expect(symbolSelector).toBeInTheDocument();
|
||||
// Verify currency selectors exist
|
||||
expect(
|
||||
screen.getByRole('combobox', { name: 'Currency prefix or suffix' }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('combobox', { name: 'Currency symbol' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Allow extra headroom for dropdown render on slower CI runners
|
||||
test('changes currency position from prefix to suffix', async () => {
|
||||
const testProps = createPropsWithCurrency();
|
||||
fastRender(testProps);
|
||||
|
||||
await asyncRender(testProps);
|
||||
await setupCurrencySection();
|
||||
|
||||
await dismissDatasourceWarning();
|
||||
|
||||
// Navigate to metrics tab
|
||||
const metricButton = await screen.findByTestId('collection-tab-Metrics');
|
||||
await userEvent.click(metricButton);
|
||||
|
||||
// Expand the metric with currency
|
||||
const expandToggles = await screen.findAllByLabelText(/expand row/i);
|
||||
await userEvent.click(expandToggles[0]);
|
||||
|
||||
// Select suffix option via shared helper (rc-virtual-list aware)
|
||||
await selectOption('Suffix', 'Currency prefix or suffix');
|
||||
await cleanupAsyncOperations();
|
||||
|
||||
// Verify onChange was called with suffix position
|
||||
await waitFor(() => {
|
||||
expect(testProps.onChange).toHaveBeenCalledTimes(1);
|
||||
const callArg = testProps.onChange.mock.calls[0][0];
|
||||
|
||||
const metrics = callArg.metrics || [];
|
||||
const updatedMetric = metrics.find(
|
||||
(m: MetricType) => m.currency && m.currency.symbolPosition === 'suffix',
|
||||
);
|
||||
|
||||
expect(updatedMetric?.currency?.symbol).toBe('USD');
|
||||
});
|
||||
}, 30000);
|
||||
|
||||
// Allow extra headroom for dropdown render on slower CI runners
|
||||
// Verify the exact call arguments
|
||||
const callArg = testProps.onChange.mock.calls[0][0];
|
||||
const metrics = callArg.metrics || [];
|
||||
const updatedMetric = metrics.find(
|
||||
(m: MetricType) => m.currency?.symbolPosition === 'suffix',
|
||||
);
|
||||
expect(updatedMetric?.currency?.symbol).toBe('USD');
|
||||
}, 60000);
|
||||
|
||||
test('changes currency symbol from USD to GBP', async () => {
|
||||
const testProps = createPropsWithCurrency();
|
||||
fastRender(testProps);
|
||||
|
||||
await asyncRender(testProps);
|
||||
await setupCurrencySection();
|
||||
|
||||
await dismissDatasourceWarning();
|
||||
|
||||
// Navigate to metrics tab
|
||||
const metricButton = await screen.findByTestId('collection-tab-Metrics');
|
||||
await userEvent.click(metricButton);
|
||||
|
||||
// Expand the metric with currency
|
||||
const expandToggles = await screen.findAllByLabelText(/expand row/i);
|
||||
await userEvent.click(expandToggles[0]);
|
||||
|
||||
// Select GBP option via shared helper (rc-virtual-list aware)
|
||||
await selectOption('£ (GBP)', 'Currency symbol');
|
||||
await cleanupAsyncOperations();
|
||||
|
||||
// Verify onChange was called with GBP
|
||||
await waitFor(() => {
|
||||
expect(testProps.onChange).toHaveBeenCalledTimes(1);
|
||||
const callArg = testProps.onChange.mock.calls[0][0];
|
||||
|
||||
const metrics = callArg.metrics || [];
|
||||
const updatedMetric = metrics.find(
|
||||
(m: MetricType) => m.currency && m.currency.symbol === 'GBP',
|
||||
);
|
||||
|
||||
expect(updatedMetric?.currency?.symbolPosition).toBe('prefix');
|
||||
});
|
||||
}, 30000);
|
||||
|
||||
// Verify the exact call arguments
|
||||
const callArg = testProps.onChange.mock.calls[0][0];
|
||||
const metrics = callArg.metrics || [];
|
||||
const updatedMetric = metrics.find(
|
||||
(m: MetricType) => m.currency?.symbol === 'GBP',
|
||||
);
|
||||
expect(updatedMetric?.currency?.symbolPosition).toBe('prefix');
|
||||
}, 60000);
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
fireEvent,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { createRef } from 'react';
|
||||
import SearchFilter from './Search';
|
||||
import type { FilterHandler } from './types';
|
||||
|
||||
const mockOnSubmit = jest.fn();
|
||||
|
||||
const defaultProps = {
|
||||
Header: 'Search Header',
|
||||
name: 'search-input',
|
||||
initialValue: '',
|
||||
toolTipDescription: undefined,
|
||||
onSubmit: mockOnSubmit,
|
||||
autoComplete: undefined,
|
||||
};
|
||||
|
||||
const setup = (props = {}) =>
|
||||
render(<SearchFilter {...defaultProps} {...props} />);
|
||||
|
||||
beforeEach(() => {
|
||||
mockOnSubmit.mockClear();
|
||||
});
|
||||
|
||||
test('renders search filter with header', () => {
|
||||
setup();
|
||||
expect(screen.getByText('Search Header')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders input with placeholder', () => {
|
||||
setup();
|
||||
expect(screen.getByPlaceholderText('Type a value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders search icon', () => {
|
||||
setup();
|
||||
expect(screen.getByTestId('filters-search')).toBeInTheDocument();
|
||||
// The icon should be present as a prefix
|
||||
const input = screen.getByTestId('filters-search');
|
||||
expect(input.parentElement?.querySelector('.anticon-search')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('renders with initial value', () => {
|
||||
setup({ initialValue: 'initial search term' });
|
||||
const input = screen.getByTestId('filters-search') as HTMLInputElement;
|
||||
expect(input.value).toBe('initial search term');
|
||||
});
|
||||
|
||||
test('renders tooltip when toolTipDescription is provided', () => {
|
||||
setup({ toolTipDescription: 'This is a helpful tooltip' });
|
||||
const tooltip = screen.getByRole('img', { name: /info-circle/i });
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not render tooltip when toolTipDescription is undefined', () => {
|
||||
setup({ toolTipDescription: undefined });
|
||||
const tooltip = screen.queryByRole('img', { name: /info-circle/i });
|
||||
expect(tooltip).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('updates input value on change', async () => {
|
||||
setup();
|
||||
const input = screen.getByTestId('filters-search') as HTMLInputElement;
|
||||
|
||||
await userEvent.type(input, 'test query');
|
||||
|
||||
expect(input.value).toBe('test query');
|
||||
});
|
||||
|
||||
test('calls onSubmit with trimmed value on blur', async () => {
|
||||
setup();
|
||||
const input = screen.getByTestId('filters-search');
|
||||
|
||||
await userEvent.type(input, ' search term ');
|
||||
await userEvent.tab(); // Trigger blur
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith('search term');
|
||||
});
|
||||
});
|
||||
|
||||
test('calls onSubmit with trimmed value on Enter key', async () => {
|
||||
setup();
|
||||
const input = screen.getByTestId('filters-search');
|
||||
|
||||
await userEvent.type(input, ' another search ');
|
||||
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith('another search');
|
||||
});
|
||||
});
|
||||
|
||||
test('calls onSubmit with empty string when input is cleared', async () => {
|
||||
setup({ initialValue: 'initial value' });
|
||||
const input = screen.getByTestId('filters-search');
|
||||
|
||||
await userEvent.clear(input);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
||||
|
||||
test('does not call onSubmit when empty value is submitted on blur', async () => {
|
||||
setup();
|
||||
const input = screen.getByTestId('filters-search');
|
||||
|
||||
await userEvent.click(input);
|
||||
await userEvent.tab(); // Trigger blur without typing
|
||||
|
||||
// onSubmit should not be called for empty value on blur/enter
|
||||
expect(mockOnSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('calls onSubmit with empty string when only whitespace is entered and cleared', async () => {
|
||||
setup();
|
||||
const input = screen.getByTestId('filters-search');
|
||||
|
||||
await userEvent.type(input, ' ');
|
||||
|
||||
// When cleared (which happens when the value becomes empty), onSubmit is called with ''
|
||||
await userEvent.clear(input);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
||||
|
||||
test('clearFilter resets value and calls onSubmit with empty string', async () => {
|
||||
const ref = createRef<FilterHandler>();
|
||||
render(<SearchFilter {...defaultProps} initialValue="test" ref={ref} />);
|
||||
|
||||
const input = screen.getByTestId('filters-search') as HTMLInputElement;
|
||||
expect(input.value).toBe('test');
|
||||
|
||||
// Call clearFilter via ref
|
||||
ref.current?.clearFilter();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(input.value).toBe('');
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
||||
|
||||
test('uses custom name attribute when provided', () => {
|
||||
setup({ name: 'custom-search-name' });
|
||||
const input = screen.getByTestId('filters-search') as HTMLInputElement;
|
||||
expect(input.name).toBe('custom-search-name');
|
||||
});
|
||||
|
||||
test('sets autocomplete to off by default', () => {
|
||||
setup();
|
||||
const input = screen.getByTestId('filters-search') as HTMLInputElement;
|
||||
expect(input.autocomplete).toBe('off');
|
||||
});
|
||||
|
||||
test('uses custom autocomplete value when provided', () => {
|
||||
setup({ autoComplete: 'new-password' });
|
||||
const input = screen.getByTestId('filters-search') as HTMLInputElement;
|
||||
expect(input.autocomplete).toBe('new-password');
|
||||
});
|
||||
|
||||
test('renders with allowClear prop', () => {
|
||||
setup({ initialValue: 'test value' });
|
||||
const input = screen.getByTestId('filters-search');
|
||||
// Ant Design Input with allowClear should have a clear button when there's a value
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('multiple rapid changes only submit the final value', async () => {
|
||||
setup();
|
||||
const input = screen.getByTestId('filters-search');
|
||||
|
||||
await userEvent.type(input, 'first');
|
||||
await userEvent.type(input, ' second');
|
||||
await userEvent.type(input, ' third');
|
||||
await userEvent.tab();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith('first second third');
|
||||
// Should only be called once on blur, not for each keystroke
|
||||
expect(mockOnSubmit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('handles programmatic value changes through clearFilter', async () => {
|
||||
const ref = createRef<FilterHandler>();
|
||||
render(
|
||||
<SearchFilter {...defaultProps} initialValue="initial value" ref={ref} />,
|
||||
);
|
||||
|
||||
const input = screen.getByTestId('filters-search') as HTMLInputElement;
|
||||
|
||||
// Type something new
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, 'new value');
|
||||
expect(input.value).toBe('new value');
|
||||
|
||||
// Clear using ref
|
||||
ref.current?.clearFilter();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(input.value).toBe('');
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
||||
@@ -42,6 +42,7 @@ interface SearchHeaderProps extends BaseFilter {
|
||||
onSubmit: (val: string) => void;
|
||||
name: string;
|
||||
toolTipDescription: string | undefined;
|
||||
autoComplete?: string;
|
||||
}
|
||||
|
||||
function SearchFilter(
|
||||
@@ -51,6 +52,7 @@ function SearchFilter(
|
||||
initialValue,
|
||||
toolTipDescription,
|
||||
onSubmit,
|
||||
autoComplete = 'off',
|
||||
}: SearchHeaderProps,
|
||||
ref: RefObject<FilterHandler>,
|
||||
) {
|
||||
@@ -91,6 +93,7 @@ function SearchFilter(
|
||||
allowClear
|
||||
data-test="filters-search"
|
||||
placeholder={t('Type a value')}
|
||||
autoComplete={autoComplete}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
|
||||
132
superset-frontend/src/components/ListView/Filters/index.test.tsx
Normal file
132
superset-frontend/src/components/ListView/Filters/index.test.tsx
Normal 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');
|
||||
});
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,8 @@ export interface ListViewFilter {
|
||||
min?: number;
|
||||
max?: number;
|
||||
dropdownStyle?: React.CSSProperties;
|
||||
autoComplete?: string;
|
||||
inputName?: string;
|
||||
}
|
||||
|
||||
export type ListViewFilters = ListViewFilter[];
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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)}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}),
|
||||
@@ -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 [];
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
@@ -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) {
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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 };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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' }],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -242,6 +242,7 @@ function DashboardTable({
|
||||
addDangerToast,
|
||||
activeTab,
|
||||
user?.userId,
|
||||
getData,
|
||||
);
|
||||
setDashboardToDelete(null);
|
||||
}}
|
||||
|
||||
204
superset-frontend/src/hooks/apiResources/catalogs.test.ts
Normal file
204
superset-frontend/src/hooks/apiResources/catalogs.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -471,6 +471,7 @@ function AlertList({
|
||||
id: 'name',
|
||||
input: 'search',
|
||||
operator: FilterOperator.Contains,
|
||||
inputName: 'alert_report_list_search',
|
||||
},
|
||||
{
|
||||
Header: t('Owner'),
|
||||
|
||||
@@ -255,6 +255,7 @@ function AnnotationLayersList({
|
||||
id: 'name',
|
||||
input: 'search',
|
||||
operator: FilterOperator.Contains,
|
||||
inputName: 'annotation_layer_list_search',
|
||||
},
|
||||
{
|
||||
Header: t('Changed by'),
|
||||
|
||||
@@ -299,6 +299,7 @@ function GroupsList({ user }: GroupsListProps) {
|
||||
id: 'name',
|
||||
input: 'search',
|
||||
operator: ListViewFilterOperator.Contains,
|
||||
inputName: 'group_list_search',
|
||||
},
|
||||
{
|
||||
Header: t('Label'),
|
||||
|
||||
@@ -300,6 +300,7 @@ function RolesList({ addDangerToast, addSuccessToast, user }: RolesListProps) {
|
||||
id: 'name',
|
||||
input: 'search',
|
||||
operator: FilterOperator.Contains,
|
||||
inputName: 'role_list_search',
|
||||
},
|
||||
{
|
||||
Header: t('Users'),
|
||||
|
||||
@@ -266,6 +266,7 @@ function RowLevelSecurityList(props: RLSProps) {
|
||||
id: 'name',
|
||||
input: 'search',
|
||||
operator: FilterOperator.StartsWith,
|
||||
inputName: 'rls_list_search',
|
||||
},
|
||||
{
|
||||
Header: t('Filter Type'),
|
||||
|
||||
@@ -266,6 +266,7 @@ function TagList(props: TagListProps) {
|
||||
id: 'name',
|
||||
input: 'search',
|
||||
operator: FilterOperator.Contains,
|
||||
inputName: 'tag_list_search',
|
||||
},
|
||||
{
|
||||
Header: t('Modified by'),
|
||||
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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));
|
||||
},
|
||||
|
||||
258
superset-websocket/package-lock.json
generated
258
superset-websocket/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user