mirror of
https://github.com/apache/superset.git
synced 2026-05-05 16:04:19 +00:00
Compare commits
100 Commits
fix-webpac
...
6.0.0rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
886f525545 | ||
|
|
b76a0e1d2d | ||
|
|
542efcdb11 | ||
|
|
0ac7464649 | ||
|
|
e5bb8bf0ff | ||
|
|
8306b66515 | ||
|
|
9a0dc23755 | ||
|
|
5561f529f2 | ||
|
|
b0ceb2a162 | ||
|
|
7fde43b476 | ||
|
|
99519cd4ce | ||
|
|
9525742b56 | ||
|
|
ad7acecbf2 | ||
|
|
a423f8ecda | ||
|
|
1265b3d3e5 | ||
|
|
b2fd9e2fb1 | ||
|
|
f3163e1c27 | ||
|
|
8c1fdcb179 | ||
|
|
eb2b4bfc30 | ||
|
|
5baee67df7 | ||
|
|
e8562bc641 | ||
|
|
dedc10065e | ||
|
|
4dedfac238 | ||
|
|
2c870e8528 | ||
|
|
baee1ab82b | ||
|
|
a8cf0981fa | ||
|
|
8768a3f55a | ||
|
|
cf1902a4cc | ||
|
|
d94c92db01 | ||
|
|
acec8743c0 | ||
|
|
9918670315 | ||
|
|
997e000f6b | ||
|
|
428c97a1d6 | ||
|
|
7996359719 | ||
|
|
444b98b95e | ||
|
|
b51eda51ce | ||
|
|
8a3dcadf87 | ||
|
|
07fd60fe20 | ||
|
|
06ada5472e | ||
|
|
22826ba876 | ||
|
|
dbe0845dc0 | ||
|
|
0c045127e4 | ||
|
|
f69bdf5475 | ||
|
|
d5a523db25 | ||
|
|
38b456bc8a | ||
|
|
e1efc87fdc | ||
|
|
e84bdfaa6d | ||
|
|
930038d763 | ||
|
|
d30cd5dc2a | ||
|
|
714a03e007 | ||
|
|
026e016720 | ||
|
|
5442521e18 | ||
|
|
b8426b92c7 | ||
|
|
2f086475f8 | ||
|
|
1c95ea5ab8 | ||
|
|
a3a2c494cc | ||
|
|
d6cc324798 | ||
|
|
5756d25a7c | ||
|
|
b16d6ed224 | ||
|
|
18e8b064de | ||
|
|
0c0dfc0601 | ||
|
|
c80b8fea85 | ||
|
|
160a8fe16c | ||
|
|
fc80861f47 | ||
|
|
7169d9f2bd | ||
|
|
37347525e7 | ||
|
|
b6aa68dbfc | ||
|
|
e4f371b126 | ||
|
|
d0d816047c | ||
|
|
2e28e22596 | ||
|
|
a475d68693 | ||
|
|
dfd36f5a54 | ||
|
|
ea5ebd2ec9 | ||
|
|
d454a22f1c | ||
|
|
d01934d9d8 | ||
|
|
25775504b9 | ||
|
|
4cc6984ebf | ||
|
|
548d9e6f7b | ||
|
|
1adaf20ccb | ||
|
|
7e9658fad6 | ||
|
|
5ff6679804 | ||
|
|
0899496ca5 | ||
|
|
e2a469c32d | ||
|
|
2ffc1b95ba | ||
|
|
3b3aa1e302 | ||
|
|
958b29acbc | ||
|
|
f0cfd17dc5 | ||
|
|
ebfddb2b39 | ||
|
|
e10b6e8ae9 | ||
|
|
18d4acdee9 | ||
|
|
c199213e8e | ||
|
|
91834bbede | ||
|
|
7253c79959 | ||
|
|
04738c716c | ||
|
|
928dbe43e0 | ||
|
|
3f597a6551 | ||
|
|
3661482eb3 | ||
|
|
9443fe8b78 | ||
|
|
453241eb33 | ||
|
|
a5f7d236ac |
@@ -44,4 +44,8 @@ under the License.
|
||||
- [4.0.1](./CHANGELOG/4.0.1.md)
|
||||
- [4.0.2](./CHANGELOG/4.0.2.md)
|
||||
- [4.1.0](./CHANGELOG/4.1.0.md)
|
||||
- [4.1.1](./CHANGELOG/4.1.1.md)
|
||||
- [4.1.2](./CHANGELOG/4.1.2.md)
|
||||
- [4.1.3](./CHANGELOG/4.1.3.md)
|
||||
- [5.0.0](./CHANGELOG/5.0.0.md)
|
||||
- [6.0.0](./CHANGELOG/6.0.0.md)
|
||||
|
||||
895
CHANGELOG/6.0.0.md
Normal file
895
CHANGELOG/6.0.0.md
Normal file
@@ -0,0 +1,895 @@
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
## Change Log
|
||||
|
||||
### 6.0 (Mon Sep 22 12:40:16 2025 -0400)
|
||||
|
||||
**Database Migrations**
|
||||
|
||||
- [#34560](https://github.com/apache/superset/pull/34560) feat: Implement UI-based system theme administration (@mistercrunch)
|
||||
- [#34521](https://github.com/apache/superset/pull/34521) chore: use logger on all migrations (@villebro)
|
||||
- [#34345](https://github.com/apache/superset/pull/34345) chore: proper current_app.config proxy usage (@mistercrunch)
|
||||
- [#34182](https://github.com/apache/superset/pull/34182) feat: add a theme CRUD page to manage themes (@mistercrunch)
|
||||
- [#34292](https://github.com/apache/superset/pull/34292) chore: move some rules from ruff -> pylint (@mistercrunch)
|
||||
- [#33682](https://github.com/apache/superset/pull/33682) fix: Dataset currency (@Vitor-Avila)
|
||||
- [#33564](https://github.com/apache/superset/pull/33564) chore: remove sqlparse (@betodealmeida)
|
||||
- [#33303](https://github.com/apache/superset/pull/33303) fix: `metric.currency` should be JSON, not string (@betodealmeida)
|
||||
- [#33155](https://github.com/apache/superset/pull/33155) chore: migrate to more db migration utils (@eschutho)
|
||||
- [#33072](https://github.com/apache/superset/pull/33072) chore: use create table util (@eschutho)
|
||||
- [#33116](https://github.com/apache/superset/pull/33116) feat(explore): X-axis sort by specific metric when more than 1 metric is set (@kgabryje)
|
||||
- [#32852](https://github.com/apache/superset/pull/32852) chore: update migrations to use utils (@sadpandajoe)
|
||||
- [#32759](https://github.com/apache/superset/pull/32759) fix(migrations): fix foreign keys to match FAB 4.6.0 tables (@Antonio-RiveroMartnez)
|
||||
- [#32680](https://github.com/apache/superset/pull/32680) feat: DB migration for dataset folders (@betodealmeida)
|
||||
- [#32352](https://github.com/apache/superset/pull/32352) fix(dev/ci): pre-commit fixes galore (@rusackas)
|
||||
|
||||
**Features**
|
||||
|
||||
- [#34732](https://github.com/apache/superset/pull/34732) feat: completely migrate from DeprecatedThemeColors to Antd semantic tokens (@mistercrunch)
|
||||
- [#29573](https://github.com/apache/superset/pull/29573) feat(api): Added uuid filed support to dataset, chart, dashboard API (@dankor)
|
||||
- [#34712](https://github.com/apache/superset/pull/34712) feat: replace react-color with AntD ColorPicker for theming support (@mistercrunch)
|
||||
- [#34678](https://github.com/apache/superset/pull/34678) feat(extension): Add extension for chart header (@justinpark)
|
||||
- [#34658](https://github.com/apache/superset/pull/34658) feat(sqllab): improve SaveDatasetModal design with proper theme spacing (@kasiazjc)
|
||||
- [#27086](https://github.com/apache/superset/pull/27086) feat(filter_state): Added @api and @has_access_api to all methods of filter_state API. (@xneg)
|
||||
- [#34663](https://github.com/apache/superset/pull/34663) feat: add @sadpandajoe to migrations CODEOWNERS (@mistercrunch)
|
||||
- [#34655](https://github.com/apache/superset/pull/34655) feat(dashboard): change chart background option from "White" to "Solid" (@kasiazjc)
|
||||
- [#34561](https://github.com/apache/superset/pull/34561) feat: Tiled screenshots in Playwright reports (@kgabryje)
|
||||
- [#34434](https://github.com/apache/superset/pull/34434) feat: Add ESLint rule to enforce sentence case in button text (@sadpandajoe)
|
||||
- [#34322](https://github.com/apache/superset/pull/34322) feat(deckgl): add selected cross-filter indication (@DamianPendrak)
|
||||
- [#34373](https://github.com/apache/superset/pull/34373) feat(docker): Add pytest support to docker-compose-light.yml (@mistercrunch)
|
||||
- [#34375](https://github.com/apache/superset/pull/34375) feat(timeshift): Add support for date range timeshifts (@msyavuz)
|
||||
- [#34319](https://github.com/apache/superset/pull/34319) feat: Enable drilling in embedded (@Vitor-Avila)
|
||||
- [#34406](https://github.com/apache/superset/pull/34406) feat: Add configurable query identifiers for Mixed Timeseries charts (@yousoph)
|
||||
- [#34416](https://github.com/apache/superset/pull/34416) feat: add runtime custom font loading via configuration (@mistercrunch)
|
||||
- [#34409](https://github.com/apache/superset/pull/34409) feat(codespaces): auto-setup Python venv with dependencies (@mistercrunch)
|
||||
- [#34206](https://github.com/apache/superset/pull/34206) feat(i18n): update Spanish translations (messages.po) (@cbausaonebox)
|
||||
- [#34376](https://github.com/apache/superset/pull/34376) feat: Add GitHub Codespaces support with docker-compose-light (@mistercrunch)
|
||||
- [#34383](https://github.com/apache/superset/pull/34383) feat(charts): Enable async buildQuery support for complex chart logic (@mistercrunch)
|
||||
- [#34380](https://github.com/apache/superset/pull/34380) feat: allow creating dataset without exploring (@betodealmeida)
|
||||
- [#34379](https://github.com/apache/superset/pull/34379) feat: focus on text input when modal opens (@betodealmeida)
|
||||
- [#34359](https://github.com/apache/superset/pull/34359) feat: read column metadata (@betodealmeida)
|
||||
- [#34273](https://github.com/apache/superset/pull/34273) feat(theming): Align embedded sdk with theme configs (@gabotorresruiz)
|
||||
- [#34324](https://github.com/apache/superset/pull/34324) feat: introducing a docker-compose-light.yml for lighter development (@mistercrunch)
|
||||
- [#34308](https://github.com/apache/superset/pull/34308) feat(timeseries): enhance 'Series Limit' to support grouping the long tail (@mistercrunch)
|
||||
- [#34294](https://github.com/apache/superset/pull/34294) feat: re-order CRUD list view action buttons (@mistercrunch)
|
||||
- [#34290](https://github.com/apache/superset/pull/34290) feat: make `SupersetClient` retry on 502-504 (@betodealmeida)
|
||||
- [#34258](https://github.com/apache/superset/pull/34258) feat(docker): do not include chromium (headless browser) by default in Dockerfile (@mistercrunch)
|
||||
- [#34194](https://github.com/apache/superset/pull/34194) feat: introduce comprehensive LLM context guides for AI-powered development (@mistercrunch)
|
||||
- [#34207](https://github.com/apache/superset/pull/34207) feat(docs): migrate ESLint to v9 (@hainenber)
|
||||
- [#34231](https://github.com/apache/superset/pull/34231) feat: add Claude Code GitHub Action integration (@mistercrunch)
|
||||
- [#34204](https://github.com/apache/superset/pull/34204) feat(deckgl): add support for OpenStreetMap as our new default and make "tile-providers" more configurable FIX (@plavacquery)
|
||||
- [#33569](https://github.com/apache/superset/pull/33569) feat(pivot-table-chart): Download as pivoted excel (@mdusmanalvi)
|
||||
- [#34156](https://github.com/apache/superset/pull/34156) feat(snowflake): Support Snowflake private keys w/o passphrase (@junyoneyama)
|
||||
- [#33953](https://github.com/apache/superset/pull/33953) feat(i18n): add Catalan (ca) translations (@cbausaonebox)
|
||||
- [#34177](https://github.com/apache/superset/pull/34177) feat: removing dup logic in sqla/models.py and models/helpers.py (@mistercrunch)
|
||||
- [#34144](https://github.com/apache/superset/pull/34144) feat(theming): Introduce bootstrap-driven Superset theme configurations (@gabotorresruiz)
|
||||
- [#32870](https://github.com/apache/superset/pull/32870) feat(filter panel): hide filter panel on all dashboard by default. (@SBIN2010)
|
||||
- [#34119](https://github.com/apache/superset/pull/34119) feat(i18n): load language pack asynchronously (@mistercrunch)
|
||||
- [#34140](https://github.com/apache/superset/pull/34140) feat: improve Doris catalog support (@betodealmeida)
|
||||
- [#34017](https://github.com/apache/superset/pull/34017) feat(deckgl): add new color controls with color breakpoints (@DamianPendrak)
|
||||
- [#33603](https://github.com/apache/superset/pull/33603) feat(deckgl): add support for OpenStreetMap as our new default and make "tile-providers" more configurable (@plavacquery)
|
||||
- [#33769](https://github.com/apache/superset/pull/33769) feat(deck-gl): Enable individual deck.gl layer selection in FilterScope tree (@richardfogaca)
|
||||
- [#34095](https://github.com/apache/superset/pull/34095) feat: Don't show the row limit warning for embedded dashboards by default (@Vitor-Avila)
|
||||
- [#33517](https://github.com/apache/superset/pull/33517) feat(viz-type): Ag grid table plugin Integration (@amaannawab923)
|
||||
- [#33789](https://github.com/apache/superset/pull/33789) feat(deckgl): add cross-filters to deck.gl charts (@DamianPendrak)
|
||||
- [#33170](https://github.com/apache/superset/pull/33170) feat(filter): Add Slider Range Inputs Option for Numerical Range Filters (@payose)
|
||||
- [#33716](https://github.com/apache/superset/pull/33716) feat(plugin-chart-echarts): add Gantt Chart plugin (@Quatters)
|
||||
- [#34023](https://github.com/apache/superset/pull/34023) feat(flag): Added feature_flag for superset security_views (@alexandrusoare)
|
||||
- [#33809](https://github.com/apache/superset/pull/33809) feat: Add confirmation modal for unsaved changes (@gabotorresruiz)
|
||||
- [#33947](https://github.com/apache/superset/pull/33947) feat(Table): Add infrastructure to override time shifts (@msyavuz)
|
||||
- [#33929](https://github.com/apache/superset/pull/33929) feat(db): remove Rockset DB support (@hainenber)
|
||||
- [#33781](https://github.com/apache/superset/pull/33781) feat(Dashboard): Row limit warning in dashboards (@msyavuz)
|
||||
- [#33631](https://github.com/apache/superset/pull/33631) feat(User Registrations): Migrate user registrations fab view (@msyavuz)
|
||||
- [#33871](https://github.com/apache/superset/pull/33871) feat(charts): Add row limit control to box plot chart (@DamianPendrak)
|
||||
- [#33863](https://github.com/apache/superset/pull/33863) feat(Icons): Add HistoryOutlined (@msyavuz)
|
||||
- [#33851](https://github.com/apache/superset/pull/33851) feat(theming): improving theme docs and configuration (@mistercrunch)
|
||||
- [#31590](https://github.com/apache/superset/pull/31590) feat(theming): land Ant Design v5 overhaul — dynamic themes, real dark mode + massive styling refactor (@mistercrunch)
|
||||
- [#33847](https://github.com/apache/superset/pull/33847) feat: initial Dremio sqlglot dialect (@betodealmeida)
|
||||
- [#33829](https://github.com/apache/superset/pull/33829) feat(extension): Added extension point for Time Filters (@alexandrusoare)
|
||||
- [#33656](https://github.com/apache/superset/pull/33656) feat(chart): add toggle for percentage metric calculation mode in Table chart (@LevisNgigi)
|
||||
- [#33709](https://github.com/apache/superset/pull/33709) feat(DatasourceEditor): Format sql shortcut and bigger table (@msyavuz)
|
||||
- [#33729](https://github.com/apache/superset/pull/33729) feat: x axis interval control to show ALL ticks on timeseries charts (@rusackas)
|
||||
- [#32610](https://github.com/apache/superset/pull/32610) feat(clickhouse): allow dynamic schema (@codenamelxl)
|
||||
- [#33634](https://github.com/apache/superset/pull/33634) feat(MixedTimeSeries): Add onlyTotal and Sort Series to Mixed TimeSeries (@nilmonto)
|
||||
- [#33443](https://github.com/apache/superset/pull/33443) feat(Dataset): editor improvements - run in sqllab (@rebenitez1802)
|
||||
- [#33620](https://github.com/apache/superset/pull/33620) feat(UserInfo): Migrate User Info FAB to React (@EnxDev)
|
||||
- [#33301](https://github.com/apache/superset/pull/33301) feat(List Groups): Migrate List Groups FAB to React (@EnxDev)
|
||||
- [#32887](https://github.com/apache/superset/pull/32887) feat(database): add SingleStore engine specification (@AdalbertMemSQL)
|
||||
- [#33434](https://github.com/apache/superset/pull/33434) feat: Python 3.12 support (@rad-pat)
|
||||
- [#33560](https://github.com/apache/superset/pull/33560) feat: use sqlglot to validate adhoc subquery (@betodealmeida)
|
||||
- [#33542](https://github.com/apache/superset/pull/33542) feat(sqllab): use sqlglot instead of sqlparse (@betodealmeida)
|
||||
- [#33614](https://github.com/apache/superset/pull/33614) feat: current_user_rls_rules Jinja macro (@Vitor-Avila)
|
||||
- [#33525](https://github.com/apache/superset/pull/33525) feat: implement CVAS/CTAS in sqlglot (@betodealmeida)
|
||||
- [#33524](https://github.com/apache/superset/pull/33524) feat: implement RLS in sqlglot (@betodealmeida)
|
||||
- [#33518](https://github.com/apache/superset/pull/33518) feat: implement CTEs logic in sqlglot (@betodealmeida)
|
||||
- [#33298](https://github.com/apache/superset/pull/33298) feat(Action Logs): Migrate Action Log FAB to React (@EnxDev)
|
||||
- [#33473](https://github.com/apache/superset/pull/33473) feat: use sqlglot to set limit (@betodealmeida)
|
||||
- [#33456](https://github.com/apache/superset/pull/33456) feat: implement limit extraction in sqlglot (@betodealmeida)
|
||||
- [#32707](https://github.com/apache/superset/pull/32707) feat(stack by dimension): add a stack by dimension dropdown list (@jpchev)
|
||||
- [#33451](https://github.com/apache/superset/pull/33451) feat(chart): add dynamicQueryObjectCount property to Chart Metadata (@DamianPendrak)
|
||||
- [#33348](https://github.com/apache/superset/pull/33348) feat(Pie Chart): threshold for Other (@Quatters)
|
||||
- [#33357](https://github.com/apache/superset/pull/33357) feat(Table Chart): Row limit Increase , Backend Sorting , Backend Search , Excel/CSV Improvements (@amaannawab923)
|
||||
- [#33340](https://github.com/apache/superset/pull/33340) feat: Run SQL on DataSourceEditor implementation (@rebenitez1802)
|
||||
- [#33099](https://github.com/apache/superset/pull/33099) feat: add metric name for big number chart types #33013 (@fardin-developer)
|
||||
- [#29580](https://github.com/apache/superset/pull/29580) feat: Persian translations (@CodeWithEmad)
|
||||
- [#33208](https://github.com/apache/superset/pull/33208) feat(maps): Adding Republic of Serbia to country maps (@rusackas)
|
||||
- [#33192](https://github.com/apache/superset/pull/33192) feat(i18n): Frontend add zh_TW Option (@bestlong)
|
||||
- [#33198](https://github.com/apache/superset/pull/33198) feat(maps): Adding Ivory Coast / Côte d'Ivoire (@rusackas)
|
||||
- [#32695](https://github.com/apache/superset/pull/32695) feat(country-map): fix France Regions IDF region code - Fixes #32627 (@tarraschk)
|
||||
- [#33043](https://github.com/apache/superset/pull/33043) feat(Select): Select all and Deselect all that works on visible items while searching (@msyavuz)
|
||||
- [#33054](https://github.com/apache/superset/pull/33054) feat(Native Filters): Exclude Filter Values (@amaannawab923)
|
||||
- [#32882](https://github.com/apache/superset/pull/32882) feat(List Users): Migrate List Users FAB to React (@EnxDev)
|
||||
- [#29827](https://github.com/apache/superset/pull/29827) feat(lang): update Italian language (@WLCFaro)
|
||||
- [#33104](https://github.com/apache/superset/pull/33104) feat(explore): Integrate dataset panel with Folders feature (@eschutho)
|
||||
- [#28751](https://github.com/apache/superset/pull/28751) feat: catalogs for DuckDB (@betodealmeida)
|
||||
- [#32520](https://github.com/apache/superset/pull/32520) feat: dataset folders (backend) (@betodealmeida)
|
||||
- [#33096](https://github.com/apache/superset/pull/33096) feat(Native Filters): Configure creatable filter behavior (@geido)
|
||||
- [#33000](https://github.com/apache/superset/pull/33000) feat: optimize catalog permission sync (@betodealmeida)
|
||||
- [#32975](https://github.com/apache/superset/pull/32975) feat(charts): add subtitle option and metric customization controls (@LevisNgigi)
|
||||
- [#30134](https://github.com/apache/superset/pull/30134) feat: Allow superset to be deployed under a prefixed URL (@martyngigg)
|
||||
- [#33046](https://github.com/apache/superset/pull/33046) feat: add a title prop to the dashboard link in CRUD LIST view (@mistercrunch)
|
||||
- [#30833](https://github.com/apache/superset/pull/30833) feat(tags): Export and Import Functionality for Superset Dashboards and Charts (@asher-lab)
|
||||
- [#32997](https://github.com/apache/superset/pull/32997) feat: Add getDataMask function to embedded SDK (@kgabryje)
|
||||
- [#31331](https://github.com/apache/superset/pull/31331) feat(embedding-sdk): emit data-mask events through embedded sdk to iframe parent (@MohamedHalat)
|
||||
- [#32432](https://github.com/apache/superset/pull/32432) feat(List Roles): Migrate FAB view to React (@EnxDev)
|
||||
- [#30760](https://github.com/apache/superset/pull/30760) feat: add latest partition support for BigQuery (@mistercrunch)
|
||||
- [#32900](https://github.com/apache/superset/pull/32900) feat: Enable passing a permalink to cache_dashboard_screenshot endpoint (@kgabryje)
|
||||
- [#28605](https://github.com/apache/superset/pull/28605) feat(plugins): Make comparison values on BigNumberPeriodOverPeriod toggleable (@mkramer5454)
|
||||
- [#32814](https://github.com/apache/superset/pull/32814) feat(chart controls): Add "%d.%m.%Y" time format option (@Quatters)
|
||||
- [#32767](https://github.com/apache/superset/pull/32767) feat: Add Aggregation Method for Big Number with Trendline (@LevisNgigi)
|
||||
- [#32770](https://github.com/apache/superset/pull/32770) feat: Add current_user_roles() Jinja macro (@bmaquet)
|
||||
- [#32781](https://github.com/apache/superset/pull/32781) feat(Jinja): to_datetime filter (@Vitor-Avila)
|
||||
- [#32721](https://github.com/apache/superset/pull/32721) feat(FormModal): Specialized Modal component for forms (@alexandrusoare)
|
||||
- [#32735](https://github.com/apache/superset/pull/32735) feat(embedded): Force a specific referrerPolicy for the iframe request (@Vitor-Avila)
|
||||
- [#32731](https://github.com/apache/superset/pull/32731) feat(where_in): Support returning None if filter_values return None (@Vitor-Avila)
|
||||
- [#32702](https://github.com/apache/superset/pull/32702) feat(file uploads): List only allowed schemas in the file uploads dialog (@Vitor-Avila)
|
||||
- [#32670](https://github.com/apache/superset/pull/32670) feat: Implement sparse import for ImportAssetsCommand (@withnale)
|
||||
- [#32682](https://github.com/apache/superset/pull/32682) feat(docs): Adding Kapa.ai integration (@rusackas)
|
||||
- [#32662](https://github.com/apache/superset/pull/32662) feat: add a note to install cors-related dependency when using ENABLE_CORS (@mistercrunch)
|
||||
- [#32546](https://github.com/apache/superset/pull/32546) feat: `OAuth2StoreTokenCommand` (@betodealmeida)
|
||||
- [#32366](https://github.com/apache/superset/pull/32366) feat(reports): removing index column (@SkinnyPigeon)
|
||||
- [#32170](https://github.com/apache/superset/pull/32170) feat(charts): add two new boxplot parameter sets (@sfirke)
|
||||
- [#32510](https://github.com/apache/superset/pull/32510) feat(slack): adds rate limit error handler for Slack client (@Usiel)
|
||||
- [#32509](https://github.com/apache/superset/pull/32509) feat(KustoKQL): Update KQL alchemy version and update timegrain expressions (@ag-ramachandran)
|
||||
- [#32506](https://github.com/apache/superset/pull/32506) feat: make user agent customizable (@villebro)
|
||||
- [#32317](https://github.com/apache/superset/pull/32317) feat(flag flip): Setting Horizontal Filters to True by default. (@rusackas)
|
||||
- [#32121](https://github.com/apache/superset/pull/32121) feat: security, user group support (@dpgaspar)
|
||||
- [#31996](https://github.com/apache/superset/pull/31996) feat: cache the frontend's bootstrap data (@mistercrunch)
|
||||
- [#32048](https://github.com/apache/superset/pull/32048) feat: improve GSheets OAuth2 (@betodealmeida)
|
||||
- [#32231](https://github.com/apache/superset/pull/32231) feat: Update database permissions in async mode (@Vitor-Avila)
|
||||
- [#31726](https://github.com/apache/superset/pull/31726) feat(filter): adding inputs to Numerical Range Filter (@alexandrusoare)
|
||||
- [#31506](https://github.com/apache/superset/pull/31506) feat(i18n): Add polish to default language (@EmmanuelCbd)
|
||||
- [#32403](https://github.com/apache/superset/pull/32403) feat: default ports for SSH tunnel (@betodealmeida)
|
||||
- [#32358](https://github.com/apache/superset/pull/32358) feat: Adding the option and feature to enable borders with color, opacity and width control on heatmaps along with white borders on emphasis (@Dev10-34)
|
||||
- [#32339](https://github.com/apache/superset/pull/32339) feat: allow importing encrypted_extra (@betodealmeida)
|
||||
- [#32264](https://github.com/apache/superset/pull/32264) feat(number-format): adds memory data transfer rates in binary and decimal format (@tshallenberger)
|
||||
- [#32261](https://github.com/apache/superset/pull/32261) feat(type-checking): Add type-checking pre-commit hooks (@alveifbklsiu259)
|
||||
- [#32228](https://github.com/apache/superset/pull/32228) feat: recursive metric definitions (@betodealmeida)
|
||||
- [#32189](https://github.com/apache/superset/pull/32189) feat(dropdown accessibility): Wrap dropdown triggers with buttons for accessibility (@msyavuz)
|
||||
- [#31998](https://github.com/apache/superset/pull/31998) feat: Add parseJson Handlebars Helper to Support Processing Nested JSON Data (@AdrianKoszalka)
|
||||
- [#31998](https://github.com/apache/superset/pull/31998) feat: Add parseJson Handlebars Helper to Support Processing Nested JSON Data (@AdrianKoszalka)
|
||||
- [#32041](https://github.com/apache/superset/pull/32041) feat: add TDengine.py driver to db_engine (@DuanKuanJun)
|
||||
|
||||
**Fixes**
|
||||
|
||||
- [#35212](https://github.com/apache/superset/pull/35212) fix(SQLPopover): Use correct component (@msyavuz)
|
||||
- [#35179](https://github.com/apache/superset/pull/35179) fix: bug in tooltip timeseries chart in calculated total with annotation layer (@SBIN2010)
|
||||
- [#34999](https://github.com/apache/superset/pull/34999) fix: Bump pandas to 2.1.4 for python 3.12 (@rad-pat)
|
||||
- [#35076](https://github.com/apache/superset/pull/35076) fix(Funnel): onInit overridden row_limit to default value on save chart (@SBIN2010)
|
||||
- [#35189](https://github.com/apache/superset/pull/35189) fix(gantt-chart): fix Y-axis label visibility in dark theme (@LevisNgigi)
|
||||
- [#35155](https://github.com/apache/superset/pull/35155) fix(CrudThemeProvider): Optimized theme loading logic (@marunrun)
|
||||
- [#35168](https://github.com/apache/superset/pull/35168) fix(embedded): resolve theme context error in Loading component (@marunrun)
|
||||
- [#35151](https://github.com/apache/superset/pull/35151) fix(viz): resolve dark mode compatibility issues in BigNumber and Heatmap (@mistercrunch)
|
||||
- [#35144](https://github.com/apache/superset/pull/35144) fix: import bug template params (@SBIN2010)
|
||||
- [#35142](https://github.com/apache/superset/pull/35142) fix(deck.gl): restore legend display for Polygon charts with linear palette and fixed color schemes (@sadpandajoe)
|
||||
- [#35124](https://github.com/apache/superset/pull/35124) fix: Remove emotion-rgba from dependencies and codebase (@eschutho)
|
||||
- [#35057](https://github.com/apache/superset/pull/35057) fix(ListView): implement AntD pagination for ListView component (@gabotorresruiz)
|
||||
- [#35114](https://github.com/apache/superset/pull/35114) fix(theming): Lighter text colors on dark mode (@msyavuz)
|
||||
- [#33055](https://github.com/apache/superset/pull/33055) fix: Bump FAB to 5.X (@dpgaspar)
|
||||
- [#35105](https://github.com/apache/superset/pull/35105) fix: SQL Lab tab events (@michael-s-molina)
|
||||
- [#35095](https://github.com/apache/superset/pull/35095) fix: page size options 'all' correct in table and remove PAGE_SIZE_OPTIONS in handlebars (@SBIN2010)
|
||||
- [#35086](https://github.com/apache/superset/pull/35086) fix(pie): fixes pie chart other click error (@cbum-dev)
|
||||
- [#35090](https://github.com/apache/superset/pull/35090) fix(theming): replace error color with bolt icon for local themes (@gabotorresruiz)
|
||||
- [#35094](https://github.com/apache/superset/pull/35094) fix(templates): Restores templates files accidentally removed (@rebenitez1802)
|
||||
- [#35096](https://github.com/apache/superset/pull/35096) fix(settingsMenu): Version (@rebenitez1802)
|
||||
- [#34694](https://github.com/apache/superset/pull/34694) fix(drill-to-detail): ensure axis label filters map to original column names (@LisaHusband)
|
||||
- [#35072](https://github.com/apache/superset/pull/35072) fix(timeshifts): Add missing feature flag to enum (@msyavuz)
|
||||
- [#34558](https://github.com/apache/superset/pull/34558) fix(Table Chart): render null dates properly (@nicob3y)
|
||||
- [#35064](https://github.com/apache/superset/pull/35064) fix(table): table search input placeholder (@SBIN2010)
|
||||
- [#35007](https://github.com/apache/superset/pull/35007) fix(tests): one of integration test in TestSqlaTableModel does not support MySQL "concat" (@catpineapple)
|
||||
- [#35001](https://github.com/apache/superset/pull/35001) fix(dashboard): normalize spacings and background colors (@gabotorresruiz)
|
||||
- [#34828](https://github.com/apache/superset/pull/34828) fix(theming): Icons in ExecutionLogList and Country map chart tooltip theme consistency (@rebenitez1802)
|
||||
- [#35036](https://github.com/apache/superset/pull/35036) fix: mixed timeseries chart add legend margin (@SBIN2010)
|
||||
- [#34973](https://github.com/apache/superset/pull/34973) fix(chart): change "No query." to "Query cannot be loaded" in Multi Layer Deck.gl Chart (@DamianPendrak)
|
||||
- [#35005](https://github.com/apache/superset/pull/35005) fix: display legend mixed timeseries chart (@SBIN2010)
|
||||
- [#34662](https://github.com/apache/superset/pull/34662) fix(sql): Add Impala dialect support to sqlglot parser (@rusackas)
|
||||
- [#34987](https://github.com/apache/superset/pull/34987) fix(theming): more visual bugs (@msyavuz)
|
||||
- [#35017](https://github.com/apache/superset/pull/35017) fix(RoleListEditModal): display user's other properties in table (@msyavuz)
|
||||
- [#35011](https://github.com/apache/superset/pull/35011) fix: doris genericDataType modify (@catpineapple)
|
||||
- [#34870](https://github.com/apache/superset/pull/34870) fix(deps): expand pyarrow version range to <19 (@sha174n)
|
||||
- [#34995](https://github.com/apache/superset/pull/34995) fix(tests): resolve AlertReportModal checkmark test failures (@sadpandajoe)
|
||||
- [#34874](https://github.com/apache/superset/pull/34874) fix(ui-core): Invalid postTransform process (@justinpark)
|
||||
- [#34781](https://github.com/apache/superset/pull/34781) fix(sqllab): autocomplete and delete tabs (@justinpark)
|
||||
- [#34803](https://github.com/apache/superset/pull/34803) fix(error-handling): jinja2 error handling improvements (@gabotorresruiz)
|
||||
- [#34991](https://github.com/apache/superset/pull/34991) fix(databricks): string escaper v2 (@Vitor-Avila)
|
||||
- [#34760](https://github.com/apache/superset/pull/34760) fix(charts): Handle virtual dataset names without schema prefix correctly (@rusackas)
|
||||
- [#34761](https://github.com/apache/superset/pull/34761) fix(echarts): Display NULL values in categorical x-axis for bar charts (@rusackas)
|
||||
- [#34918](https://github.com/apache/superset/pull/34918) fix(ChartCreation): Translate chart description (@msyavuz)
|
||||
- [#34978](https://github.com/apache/superset/pull/34978) fix: playwright feature flag evaluation (@dpgaspar)
|
||||
- [#34989](https://github.com/apache/superset/pull/34989) fix(TimeTable): use type-only export for TableChartProps to resolve webpack warnings (@gabotorresruiz)
|
||||
- [#34975](https://github.com/apache/superset/pull/34975) fix(dashboard): table charts render correctly after tab switch and refresh (@gabotorresruiz)
|
||||
- [#34895](https://github.com/apache/superset/pull/34895) fix: Athena quoting (@betodealmeida)
|
||||
- [#34909](https://github.com/apache/superset/pull/34909) fix: revert mistake setting TALISMAN_ENABLED=False (@mistercrunch)
|
||||
- [#34868](https://github.com/apache/superset/pull/34868) fix(theming): fix TimeTable chart issues (@gabotorresruiz)
|
||||
- [#34850](https://github.com/apache/superset/pull/34850) fix: complete theme management system import/export (@mistercrunch)
|
||||
- [#34887](https://github.com/apache/superset/pull/34887) fix: Improve table layout and column sizing (@kgabryje)
|
||||
- [#34724](https://github.com/apache/superset/pull/34724) fix(drilling): drill by pagination works with MSSQL data source, cont. (@sfirke)
|
||||
- [#34900](https://github.com/apache/superset/pull/34900) fix: Filter bar orientation submenu should not be highlighted (@kgabryje)
|
||||
- [#34864](https://github.com/apache/superset/pull/34864) fix(ConfirmStatusChange): remove deprecated event.persist() to fix headless browser crashes (@sadpandajoe)
|
||||
- [#34878](https://github.com/apache/superset/pull/34878) fix(tests): Improve MessageChannel mocking to prevent worker force exits (@sadpandajoe)
|
||||
- [#34858](https://github.com/apache/superset/pull/34858) fix: SelectControl default sort numeric choices by value (@kgabryje)
|
||||
- [#34869](https://github.com/apache/superset/pull/34869) fix: Undefined error when viewing query in Explore + visual fixes (@kgabryje)
|
||||
- [#34871](https://github.com/apache/superset/pull/34871) fix(tests): Mock MessageChannel to prevent Jest hanging from rc-overflow (@sadpandajoe)
|
||||
- [#34855](https://github.com/apache/superset/pull/34855) fix: Remove the underline from the right section of main menu (@kgabryje)
|
||||
- [#34854](https://github.com/apache/superset/pull/34854) fix: DB icon sizes in database add modal (@kgabryje)
|
||||
- [#34843](https://github.com/apache/superset/pull/34843) fix(dashboard): Anchor link positions (@kgabryje)
|
||||
- [#34846](https://github.com/apache/superset/pull/34846) fix(sqllab): Missing executed sql value in the result table (@justinpark)
|
||||
- [#34665](https://github.com/apache/superset/pull/34665) fix: Avoid dataset drill request if no perm (@Vitor-Avila)
|
||||
- [#34782](https://github.com/apache/superset/pull/34782) fix: Add dataset ID to file name on exports (@Vitor-Avila)
|
||||
- [#34795](https://github.com/apache/superset/pull/34795) fix(theming): explore chart type style fixes, nav right menu spacing fixed (@rebenitez1802)
|
||||
- [#34801](https://github.com/apache/superset/pull/34801) fix: make `get_image()` always return `BytesIO` (@betodealmeida)
|
||||
- [#34798](https://github.com/apache/superset/pull/34798) fix: Unexpected overflow ellipsis dots after status icon in Dashboard list (@kgabryje)
|
||||
- [#34815](https://github.com/apache/superset/pull/34815) fix(echarts): Series labels hard to read in dark mode (@kgabryje)
|
||||
- [#34809](https://github.com/apache/superset/pull/34809) fix(Icons): Add missing data-test and aria-label attributes to custom icons (@sadpandajoe)
|
||||
- [#34790](https://github.com/apache/superset/pull/34790) fix(DetailsPanel): Applied filters colors (@msyavuz)
|
||||
- [#34812](https://github.com/apache/superset/pull/34812) fix(native-filters): Low contrast of empty state in dark mode (@kgabryje)
|
||||
- [#34811](https://github.com/apache/superset/pull/34811) fix: Low contrast in viz creator selected tag in dark mode (@kgabryje)
|
||||
- [#34814](https://github.com/apache/superset/pull/34814) fix: Remove border around textarea in dashboard edit mode (@kgabryje)
|
||||
- [#34799](https://github.com/apache/superset/pull/34799) fix: Misaligned global controls in Table chart (@kgabryje)
|
||||
- [#34777](https://github.com/apache/superset/pull/34777) fix(dashboard): enable undo/redo buttons for layout changes (@gabotorresruiz)
|
||||
- [#34679](https://github.com/apache/superset/pull/34679) fix: Check migration status before initializing database-dependent features (@mistercrunch)
|
||||
- [#34719](https://github.com/apache/superset/pull/34719) fix: default value in run-server.sh (@prochac)
|
||||
- [#32640](https://github.com/apache/superset/pull/32640) fix: catch no table error (@eschutho)
|
||||
- [#34793](https://github.com/apache/superset/pull/34793) fix(PivotExcelExport): select correct chart for export (@msyavuz)
|
||||
- [#34780](https://github.com/apache/superset/pull/34780) fix(tests): make SingleStore test_adjust_engine_params version-agnostic (@sadpandajoe)
|
||||
- [#34791](https://github.com/apache/superset/pull/34791) fix(webpack): Bump webpack dev-server to handle Errors on Firefox where error object is not defined (@amaannawab923)
|
||||
- [#34765](https://github.com/apache/superset/pull/34765) fix(sqllab): Fix save query modal closing prematurely on new tabs (@rusackas)
|
||||
- [#34051](https://github.com/apache/superset/pull/34051) fix(translations): Fix translation of time-related strings like "7 seconds ago", "a minute ago", etc (@PolinaFam)
|
||||
- [#34769](https://github.com/apache/superset/pull/34769) fix: Fix TypeError in Slice.get() method when using filter_by() with BinaryExpression (@sadpandajoe)
|
||||
- [#34743](https://github.com/apache/superset/pull/34743) fix(duckdb): Add support for DuckDB-specific numeric types (@rusackas)
|
||||
- [#34683](https://github.com/apache/superset/pull/34683) fix(sqllab): Invisible grid table due to the invalid height (@justinpark)
|
||||
- [#34757](https://github.com/apache/superset/pull/34757) fix: Users can't skip column sync when saving virtual datasets (@michael-s-molina)
|
||||
- [#34720](https://github.com/apache/superset/pull/34720) fix(sqllab): Reduce flushing caused by ID updates (@justinpark)
|
||||
- [#34758](https://github.com/apache/superset/pull/34758) fix(saved_query): Copy link to clipboard before redirect to edit (#34567) (@justinpark)
|
||||
- [#34756](https://github.com/apache/superset/pull/34756) fix(RightMenu): Move RightMenu carets to the right side (@msyavuz)
|
||||
- [#34705](https://github.com/apache/superset/pull/34705) fix: Highlight outline of numerical range and time range filters (@kgabryje)
|
||||
- [#34676](https://github.com/apache/superset/pull/34676) fix(theming): Some visual issues (@rebenitez1802)
|
||||
- [#34660](https://github.com/apache/superset/pull/34660) fix: Table chart server side pagination not working on dashboard (@kgabryje)
|
||||
- [#34708](https://github.com/apache/superset/pull/34708) fix(dashboard): Remove Tab from Dashboard Confirm Modal themed (@rebenitez1802)
|
||||
- [#34706](https://github.com/apache/superset/pull/34706) fix(dashboard): Titles tooltip flickering (@rebenitez1802)
|
||||
- [#34654](https://github.com/apache/superset/pull/34654) fix: centralize cache timeout -1 logic to prevent caching (@dpgaspar)
|
||||
- [#34686](https://github.com/apache/superset/pull/34686) fix(ag-grid): Fix broken string column filters in AG Grid Table V2 (@amaannawab923)
|
||||
- [#34418](https://github.com/apache/superset/pull/34418) fix(dashboard): update cross filter scoping chart id references during dashboard import (@trentontrees)
|
||||
- [#34690](https://github.com/apache/superset/pull/34690) fix(deck.gl): add webpack rule to define module global for deck.gl charts (@richardfogaca)
|
||||
- [#34698](https://github.com/apache/superset/pull/34698) fix: Invalid error tooltip if control label is function (@kgabryje)
|
||||
- [#34671](https://github.com/apache/superset/pull/34671) fix: Bar chart crash when switching from Big Number (@kgabryje)
|
||||
- [#34680](https://github.com/apache/superset/pull/34680) fix(bootstrapData): Missing application_root data throws an error (@justinpark)
|
||||
- [#34675](https://github.com/apache/superset/pull/34675) fix(theming): Fix ag-grid theming regression in SQL Lab (@mistercrunch)
|
||||
- [#34672](https://github.com/apache/superset/pull/34672) fix(row_level_security): Correct api response code for update (@msyavuz)
|
||||
- [#34585](https://github.com/apache/superset/pull/34585) fix(theming): Theming visual fixes p5 (@msyavuz)
|
||||
- [#34664](https://github.com/apache/superset/pull/34664) fix(csv_tests): Import from utils (@msyavuz)
|
||||
- [#34511](https://github.com/apache/superset/pull/34511) fix(sqllab): show actual execution duration in Query History (@rusackas)
|
||||
- [#34395](https://github.com/apache/superset/pull/34395) fix(superset-ui-core): Include appRoot in endpoint of SupersetClientClass.postForm action (@martyngigg)
|
||||
- [#34304](https://github.com/apache/superset/pull/34304) fix(presto): return proper data type for column (@betodealmeida)
|
||||
- [#32340](https://github.com/apache/superset/pull/32340) fix(security): grant TableSchemaView to only sql_lab role (@codenamelxl)
|
||||
- [#33503](https://github.com/apache/superset/pull/33503) fix: activity table delta time (@natilehrer)
|
||||
- [#33202](https://github.com/apache/superset/pull/33202) fix(open-api): Add missing FormatQueryPayloadSchema and DashboardScreenshotPostSchema to open-api component schemas (@dogfootruler-kr)
|
||||
- [#32405](https://github.com/apache/superset/pull/32405) fix(daos/tag): prevent non-unique tags getting created along with unique ones (@hainenber)
|
||||
- [#21083](https://github.com/apache/superset/pull/21083) fix(install): set SUPERSET_VERSION_RC at the right time (@Joel-Haeberli)
|
||||
- [#34645](https://github.com/apache/superset/pull/34645) fix(webpack): webpack warnings (@gabotorresruiz)
|
||||
- [#34005](https://github.com/apache/superset/pull/34005) fix: update Russian translations (@PolinaFam)
|
||||
- [#34644](https://github.com/apache/superset/pull/34644) fix: Fix Slice import on has_drill_by_access (@Vitor-Avila)
|
||||
- [#34641](https://github.com/apache/superset/pull/34641) fix: Slack channels and Color Palettes search (@Vitor-Avila)
|
||||
- [#34584](https://github.com/apache/superset/pull/34584) fix(initialization): prevent startup failures when database tables don't exist (@eschutho)
|
||||
- [#34625](https://github.com/apache/superset/pull/34625) fix: Remove deprecated @types/classnames package (@rusackas)
|
||||
- [#34602](https://github.com/apache/superset/pull/34602) fix(Dashboards): Tabs highlight and dataset contrast in darkmode issues (@rebenitez1802)
|
||||
- [#34620](https://github.com/apache/superset/pull/34620) fix: Use labels in Drill to Detail (@Vitor-Avila)
|
||||
- [#34636](https://github.com/apache/superset/pull/34636) fix(DatabaseModal): Don't set activeKey to undefined repeatedly (@msyavuz)
|
||||
- [#33843](https://github.com/apache/superset/pull/33843) fix: Reset description height to zero when chart is not expanded (@abhinav-1305)
|
||||
- [#34239](https://github.com/apache/superset/pull/34239) fix(Heatmap): addin x axis label rotation (@SBIN2010)
|
||||
- [#34598](https://github.com/apache/superset/pull/34598) fix(db_engine_specs): generate correct boolean filter SQL syntax for Athena compatibility (@oscep)
|
||||
- [#34582](https://github.com/apache/superset/pull/34582) fix(Timeshift): Determine temporal column correctly (@msyavuz)
|
||||
- [#34175](https://github.com/apache/superset/pull/34175) fix(Table chart): fix percentage metric column (@LevisNgigi)
|
||||
- [#34508](https://github.com/apache/superset/pull/34508) fix: update copy text for better capitalization and abbreviation standards (@yousoph)
|
||||
- [#34507](https://github.com/apache/superset/pull/34507) fix(theming): More theming bugs/regressions (@msyavuz)
|
||||
- [#34545](https://github.com/apache/superset/pull/34545) fix: Avoid null `scrollLeft` in `VirtualTable` (@Vitor-Avila)
|
||||
- [#34528](https://github.com/apache/superset/pull/34528) fix(explore): Fix missing await for async buildV1ChartDataPayload calls (@mistercrunch)
|
||||
- [#34512](https://github.com/apache/superset/pull/34512) fix(sqllab): prevent strings with angle brackets from being hidden (@rusackas)
|
||||
- [#34520](https://github.com/apache/superset/pull/34520) fix: docs eslint command (@villebro)
|
||||
- [#34438](https://github.com/apache/superset/pull/34438) fix: Update table chart configuration labels to sentence case (@yousoph)
|
||||
- [#34435](https://github.com/apache/superset/pull/34435) fix(pie chart): Total now positioned correctly with all Legend positions, and respects theming (@rusackas)
|
||||
- [#34436](https://github.com/apache/superset/pull/34436) fix(echarts): resolve bar chart X-axis time formatting stuck on adaptive (@rusackas)
|
||||
- [#34424](https://github.com/apache/superset/pull/34424) fix(theming): Visual bugs p-3 (@msyavuz)
|
||||
- [#34431](https://github.com/apache/superset/pull/34431) fix: time grain and DB dropdowns (@betodealmeida)
|
||||
- [#34137](https://github.com/apache/superset/pull/34137) fix(dashboard): adds dependent filter select first value fixes (@ObservabilityTeam)
|
||||
- [#34433](https://github.com/apache/superset/pull/34433) fix(migrations): prevent theme seeding before themes table exists (@mistercrunch)
|
||||
- [#34412](https://github.com/apache/superset/pull/34412) fix: prevent anonymous code in Postgres (@betodealmeida)
|
||||
- [#31495](https://github.com/apache/superset/pull/31495) fix(sunburst): Fix sunburst chart cross-filter logic (@gerbermichi)
|
||||
- [#34389](https://github.com/apache/superset/pull/34389) fix(theme-list): reorder buttons to place import leftmost (@mistercrunch)
|
||||
- [#34178](https://github.com/apache/superset/pull/34178) fix: Console errors from various sources (@msyavuz)
|
||||
- [#34390](https://github.com/apache/superset/pull/34390) fix(charts): Fix unquoted 'Others' literal in series limit GROUP BY clause (@mistercrunch)
|
||||
- [#34296](https://github.com/apache/superset/pull/34296) fix(big number with trendline): running 2 identical queries for no good reason (@mistercrunch)
|
||||
- [#34381](https://github.com/apache/superset/pull/34381) fix: rate limiting issues with example data hosted on github.com (@mistercrunch)
|
||||
- [#34339](https://github.com/apache/superset/pull/34339) fix: prevent theme initialization errors during fresh installs (@mistercrunch)
|
||||
- [#34360](https://github.com/apache/superset/pull/34360) fix: use catalog name on generated queries (@betodealmeida)
|
||||
- [#34374](https://github.com/apache/superset/pull/34374) fix: subquery alias in RLS (@betodealmeida)
|
||||
- [#34351](https://github.com/apache/superset/pull/34351) fix(PivotTable): Render html in cells if allowRenderHtml is true (@msyavuz)
|
||||
- [#34318](https://github.com/apache/superset/pull/34318) fix(NavBar): Add brand text back (@geido)
|
||||
- [#34268](https://github.com/apache/superset/pull/34268) fix(cartodiagram): add missing locales for rendering echarts (@jansule)
|
||||
- [#34305](https://github.com/apache/superset/pull/34305) fix(npm): more reliable execution of `npm run update-maps` (@rusackas)
|
||||
- [#34300](https://github.com/apache/superset/pull/34300) fix: preserve correct column order when table layout is changed with time comparison enabled (@payose)
|
||||
- [#33084](https://github.com/apache/superset/pull/33084) fix: enhance disallowed SQL functions list for improved security (@sha174n)
|
||||
- [#34303](https://github.com/apache/superset/pull/34303) fix: return 422 on invalid SQL (@betodealmeida)
|
||||
- [#34237](https://github.com/apache/superset/pull/34237) fix(theming): Fix visual regressions from theming P7 (@EnxDev)
|
||||
- [#34299](https://github.com/apache/superset/pull/34299) fix: address numerous long-standing console errors (python & web) (@mistercrunch)
|
||||
- [#34293](https://github.com/apache/superset/pull/34293) fix: Hide View in SQL Lab for users without access (@Vitor-Avila)
|
||||
- [#34233](https://github.com/apache/superset/pull/34233) fix(chart-download): ensure full table or handlebar chart is captured in image export (@fardin-developer)
|
||||
- [#34213](https://github.com/apache/superset/pull/34213) fix(charting): correctly categorize numeric columns with NULL values (@LisaHusband)
|
||||
- [#34235](https://github.com/apache/superset/pull/34235) fix(sqllab_export): manually encode CSV output to support utf-8-sig (@Habeeb556)
|
||||
- [#34275](https://github.com/apache/superset/pull/34275) fix: fix the pre-commit hook for tsc (@mistercrunch)
|
||||
- [#34244](https://github.com/apache/superset/pull/34244) fix(deckgl): fix deck.gl color breakpoints Control (@DamianPendrak)
|
||||
- [#34279](https://github.com/apache/superset/pull/34279) fix(theming): Visual regressions p2 (@msyavuz)
|
||||
- [#34253](https://github.com/apache/superset/pull/34253) fix(theming): Theming visual fixes (@msyavuz)
|
||||
- [#34272](https://github.com/apache/superset/pull/34272) fix: build issues on master with 'npm run dev' (@mistercrunch)
|
||||
- [#34259](https://github.com/apache/superset/pull/34259) fix: Missing ownState and isCached props in Chart.jsx (@kgabryje)
|
||||
- [#34126](https://github.com/apache/superset/pull/34126) fix: database model Collapse state (@SBIN2010)
|
||||
- [#34193](https://github.com/apache/superset/pull/34193) fix: bug when updating dashboard (@SBIN2010)
|
||||
- [#34224](https://github.com/apache/superset/pull/34224) fix(Chart): Calculate chart height correctly (@msyavuz)
|
||||
- [#34229](https://github.com/apache/superset/pull/34229) fix(theming): World map tooltip color (@msyavuz)
|
||||
- [#34199](https://github.com/apache/superset/pull/34199) fix: proper handling of boolean filters with snowflake (@mistercrunch)
|
||||
- [#33933](https://github.com/apache/superset/pull/33933) fix(dashboard): Fix subitem selection on dashboard download menu (@tahvane1)
|
||||
- [#34218](https://github.com/apache/superset/pull/34218) fix(theming): Superset theme configurations correctly applying to charts (@gabotorresruiz)
|
||||
- [#34192](https://github.com/apache/superset/pull/34192) fix: dataset endpoint `/rowlevelsecurity/related/tables` doesn't apply filters as expected (@mistercrunch)
|
||||
- [#33450](https://github.com/apache/superset/pull/33450) fix(chart): update geographical info for latvia (@eriks47)
|
||||
- [#34188](https://github.com/apache/superset/pull/34188) fix(theming): Remove leftover antd5 prefix (@msyavuz)
|
||||
- [#34181](https://github.com/apache/superset/pull/34181) fix(sqllab): database ID (@betodealmeida)
|
||||
- [#34180](https://github.com/apache/superset/pull/34180) fix(databricks): string escaper (@betodealmeida)
|
||||
- [#33955](https://github.com/apache/superset/pull/33955) fix(sqllab): pass DB id instead of name (@betodealmeida)
|
||||
- [#34171](https://github.com/apache/superset/pull/34171) fix(DrillBy): make drill by work with multi metric charts (@msyavuz)
|
||||
- [#34147](https://github.com/apache/superset/pull/34147) fix: adding and removing tags does not work in control panel properties modal (@SBIN2010)
|
||||
- [#34118](https://github.com/apache/superset/pull/34118) fix: frontend translation framework crashes on string errors (@mistercrunch)
|
||||
- [#34153](https://github.com/apache/superset/pull/34153) fix(dataset): trigger `onChange` when switching to physical dataset to clear SQL (@ongdisheng)
|
||||
- [#34112](https://github.com/apache/superset/pull/34112) fix(DatabaseModal): Resolve Connect button issue for SQLAlchemy URI database connections (@EnxDev)
|
||||
- [#34127](https://github.com/apache/superset/pull/34127) fix: Apply metric d3format when currency config is {} for table charts (@Vitor-Avila)
|
||||
- [#33974](https://github.com/apache/superset/pull/33974) fix(i18n): Update Japanese translations (@aikawa-ohno)
|
||||
- [#34114](https://github.com/apache/superset/pull/34114) fix(screenshots): Change default for `SCREENSHOT_PLAYWRIGHT_WAIT_EVENT` to `domcontentloaded` (@rusackas)
|
||||
- [#34115](https://github.com/apache/superset/pull/34115) fix: make flask-cors a core dependency (@mistercrunch)
|
||||
- [#34108](https://github.com/apache/superset/pull/34108) fix: improve login page placement and width (@mistercrunch)
|
||||
- [#34113](https://github.com/apache/superset/pull/34113) fix(UI): Adjust background color for Dashboard, Tabs, and ListView component (@EnxDev)
|
||||
- [#32734](https://github.com/apache/superset/pull/32734) fix: upload data model Collapse state (@SBIN2010)
|
||||
- [#34103](https://github.com/apache/superset/pull/34103) fix(deps): Revert "chore(deps): update @deck.gl/aggregation-layers requirement from ^9.0.38 to ^9.1.12 in /superset-frontend/plugins/legacy-preset-chart-deckgl" (@DamianPendrak)
|
||||
- [#34098](https://github.com/apache/superset/pull/34098) fix: Apply metric d3format from dataset when currency config is {} (@Vitor-Avila)
|
||||
- [#34049](https://github.com/apache/superset/pull/34049) fix(translations): Fix language switching behavior when default language is not English (@PolinaFam)
|
||||
- [#34090](https://github.com/apache/superset/pull/34090) fix(deps) : Revert "chore(deps-dev): bump webpack-dev-server from 4.15.2 to 5.2.1 (@msyavuz)
|
||||
- [#34080](https://github.com/apache/superset/pull/34080) fix: Support metric currency as dict during import (@Vitor-Avila)
|
||||
- [#34014](https://github.com/apache/superset/pull/34014) fix(Table): Allow timeshifts to be overriden (@msyavuz)
|
||||
- [#34066](https://github.com/apache/superset/pull/34066) fix(styles): Remove custom z-indexes (@msyavuz)
|
||||
- [#33954](https://github.com/apache/superset/pull/33954) fix(chart controls): remove duplicated descriptions for chart controls (@Quatters)
|
||||
- [#34031](https://github.com/apache/superset/pull/34031) fix(styling): various minor visual tweaks and adjustments (@mistercrunch)
|
||||
- [#33971](https://github.com/apache/superset/pull/33971) fix(dashboard): prevent crash on invalid CSS selectors in CSS templates (@HarshithGamini)
|
||||
- [#33958](https://github.com/apache/superset/pull/33958) fix: Dashboard native filter fixes (@Vitor-Avila)
|
||||
- [#34016](https://github.com/apache/superset/pull/34016) fix(handlebars): remove serverPaginationControlSetRow from control pa… (@LisaHusband)
|
||||
- [#33977](https://github.com/apache/superset/pull/33977) fix(explore): Change dataset icon on explore to match datasets view (@xavier-GitHub76)
|
||||
- [#33949](https://github.com/apache/superset/pull/33949) fix: Theme logo links to external superset site (@martimors)
|
||||
- [#33939](https://github.com/apache/superset/pull/33939) fix(dremio): apply same fix as for drill to solve alias ambiguity (@mistercrunch)
|
||||
- [#33942](https://github.com/apache/superset/pull/33942) fix(rls): removing unnecessary wrapper (@lohart13)
|
||||
- [#32849](https://github.com/apache/superset/pull/32849) fix(plugin-chart-echarts): correct label position for Negative Values bar chart (@SBIN2010)
|
||||
- [#32857](https://github.com/apache/superset/pull/32857) fix: add suffix to Drill labels to avoid collision (@fhyy)
|
||||
- [#33916](https://github.com/apache/superset/pull/33916) fix: Consider default catalog when getting tables and view lists (@Vitor-Avila)
|
||||
- [#33923](https://github.com/apache/superset/pull/33923) fix(fe/user_info): resolve visual oddities in User Info page (@hainenber)
|
||||
- [#33898](https://github.com/apache/superset/pull/33898) fix(theming): Fix visual regressions from theming P6 (@EnxDev)
|
||||
- [#33846](https://github.com/apache/superset/pull/33846) fix: Correct state handling in CSS Template modal (@abhinav-1305)
|
||||
- [#33826](https://github.com/apache/superset/pull/33826) fix(DatabaseModal): Improve database modal validation and fix visual Issues (@EnxDev)
|
||||
- [#33834](https://github.com/apache/superset/pull/33834) fix(native filters): Make the Apply button available after click on Clear All (@Vitor-Avila)
|
||||
- [#33833](https://github.com/apache/superset/pull/33833) fix(api): Added uuid as a valid search column (@withnale)
|
||||
- [#33867](https://github.com/apache/superset/pull/33867) fix(logo): fix logo url typo (@LevisNgigi)
|
||||
- [#33849](https://github.com/apache/superset/pull/33849) fix: sqlglot linter (@betodealmeida)
|
||||
- [#33764](https://github.com/apache/superset/pull/33764) fix: use risingwave as the sqlalchemy_uri_placeholder prefix for RisingWave engine (@hzxa21)
|
||||
- [#33830](https://github.com/apache/superset/pull/33830) fix: Consider last data point for Big Number comparison lag (@Vitor-Avila)
|
||||
- [#33821](https://github.com/apache/superset/pull/33821) fix: Set time filter's isExtra to false when saving as new chart (@Vitor-Avila)
|
||||
- [#28737](https://github.com/apache/superset/pull/28737) fix: ensure numeric values for extra metadata_cache_timeout payloads (@kidusmakonnen)
|
||||
- [#33763](https://github.com/apache/superset/pull/33763) fix: select star (@betodealmeida)
|
||||
- [#33673](https://github.com/apache/superset/pull/33673) fix: clarify GUEST_TOKEN_JWT_AUDIENCE usage in the SDK (@schollz)
|
||||
- [#33694](https://github.com/apache/superset/pull/33694) fix(chart): set tab name as chart name (@anthonyhungnguyen)
|
||||
- [#33727](https://github.com/apache/superset/pull/33727) fix: typo in SQL dialect map (@betodealmeida)
|
||||
- [#33700](https://github.com/apache/superset/pull/33700) fix(compose): environment entries in compose*.yml override values in docker/.env (@denodo-research-labs)
|
||||
- [#33693](https://github.com/apache/superset/pull/33693) fix: Do not convert dataset changed_on to UTC (@Vitor-Avila)
|
||||
- [#33679](https://github.com/apache/superset/pull/33679) fix: optimize catalog permission sync when importing dashboards (@arafoperata)
|
||||
- [#33626](https://github.com/apache/superset/pull/33626) fix: Update dataset's last modified date from column/metric update (@Vitor-Avila)
|
||||
- [#33195](https://github.com/apache/superset/pull/33195) fix(sqllab): save datasets with template parameters (@ethan-l-geotab)
|
||||
- [#33577](https://github.com/apache/superset/pull/33577) fix(Security): Apply permissions to the AllEntities list/get_objects API endpoint (@Vitor-Avila)
|
||||
- [#33519](https://github.com/apache/superset/pull/33519) fix: add query identifier to legend items in mixed time series charts (@fardin-developer)
|
||||
- [#33407](https://github.com/apache/superset/pull/33407) fix(big number with trendline): add None option to the aggregation method dropdown (@LevisNgigi)
|
||||
- [#33586](https://github.com/apache/superset/pull/33586) fix: correct typos (@castodius)
|
||||
- [#33559](https://github.com/apache/superset/pull/33559) fix(Radar): Radar chart normalisation (@amaannawab923)
|
||||
- [#33516](https://github.com/apache/superset/pull/33516) fix: text => JSON migration util (@betodealmeida)
|
||||
- [#33543](https://github.com/apache/superset/pull/33543) fix(Select): Add buttonStyle prop for backward compatibility (@geido)
|
||||
- [#33521](https://github.com/apache/superset/pull/33521) fix(CI): adding explicit allowable licenses for python dependencies (@rusackas)
|
||||
- [#33501](https://github.com/apache/superset/pull/33501) fix: optimize Explore popovers rendering (@mistercrunch)
|
||||
- [#33494](https://github.com/apache/superset/pull/33494) fix(table): table ui fixes (@amaannawab923)
|
||||
- [#33475](https://github.com/apache/superset/pull/33475) fix(dependabot): adds required schedule to uv updates (@rusackas)
|
||||
- [#33467](https://github.com/apache/superset/pull/33467) fix(NativeFilters): Apply existing values (@geido)
|
||||
- [#33412](https://github.com/apache/superset/pull/33412) fix: loading examples in CI returns http error "too many requests" (@mistercrunch)
|
||||
- [#33356](https://github.com/apache/superset/pull/33356) fix(embedded): handle SUPERSET_APP_ROOT in embedded dashboard URLs (@irodriguez-nebustream)
|
||||
- [#33384](https://github.com/apache/superset/pull/33384) fix: Persist catalog change during dataset update + validation fixes (@Vitor-Avila)
|
||||
- [#33271](https://github.com/apache/superset/pull/33271) fix: Exclude Filter Values (@amaannawab923)
|
||||
- [#33363](https://github.com/apache/superset/pull/33363) fix: bump FAB to 4.6.3 (@dpgaspar)
|
||||
- [#33338](https://github.com/apache/superset/pull/33338) fix: show only filterable columns on filter dropdown (@betodealmeida)
|
||||
- [#33254](https://github.com/apache/superset/pull/33254) fix: `Unexpected input(s) 'depth'` CI warnings (@hamirmahal)
|
||||
- [#33196](https://github.com/apache/superset/pull/33196) fix(chart): Restore subheader used in bignumber with trendline (@LevisNgigi)
|
||||
- [#33205](https://github.com/apache/superset/pull/33205) fix(Native Filters): Keep default filter values when configuring creatable behavior (@geido)
|
||||
- [#33205](https://github.com/apache/superset/pull/33205) fix(Native Filters): Keep default filter values when configuring creatable behavior (@geido)
|
||||
- [#33172](https://github.com/apache/superset/pull/33172) fix: subheader should show as subtitle (@eschutho)
|
||||
- [#33142](https://github.com/apache/superset/pull/33142) fix: add folders to import schema (@eschutho)
|
||||
- [#33141](https://github.com/apache/superset/pull/33141) fix: app icon should not use subdirectory (@eschutho)
|
||||
- [#33126](https://github.com/apache/superset/pull/33126) fix(plugin-chart-table): Don't render redundant items in column config when time comparison is enabled (@kgabryje)
|
||||
- [#33124](https://github.com/apache/superset/pull/33124) fix: `master` builds are failing while trying to push report to cypress (@mistercrunch)
|
||||
- [#33100](https://github.com/apache/superset/pull/33100) fix(OAuth2): Update connection should not fail if connection is missing OAuth2 token (@Vitor-Avila)
|
||||
- [#33114](https://github.com/apache/superset/pull/33114) fix: Broken menu links to datasets and sql lab (@kgabryje)
|
||||
- [#33092](https://github.com/apache/superset/pull/33092) fix: CI file change detector to handle large PRs (@mistercrunch)
|
||||
- [#33095](https://github.com/apache/superset/pull/33095) fix: Broken Python tests on master after merging prefix branch (@martyngigg)
|
||||
- [#33063](https://github.com/apache/superset/pull/33063) fix(docs): Update quickstart.mdx to reflect latest version tag (@clayheaton)
|
||||
- [#33060](https://github.com/apache/superset/pull/33060) fix(list roles): dont send invalid querystrings (@landryb)
|
||||
- [#32990](https://github.com/apache/superset/pull/32990) fix(frontend): add missing antd-5 icon to import (@trentlavoie)
|
||||
- [#32866](https://github.com/apache/superset/pull/32866) fix: make packages PEP 625 compliant (@sadpandajoe)
|
||||
- [#32848](https://github.com/apache/superset/pull/32848) fix: Bump FAB to 4.6.1 (@michael-s-molina)
|
||||
- [#32801](https://github.com/apache/superset/pull/32801) fix(docs): scrollable table of content right bar in Superset docs (@hainenber)
|
||||
- [#32732](https://github.com/apache/superset/pull/32732) fix(asf): Revert "Revert "fix(asf): moving notifications to the top of `.asf.yaml`"" (@rusackas)
|
||||
- [#32730](https://github.com/apache/superset/pull/32730) fix(asf): Revert "fix(asf): moving notifications to the top of `.asf.yaml`" (@rusackas)
|
||||
- [#32728](https://github.com/apache/superset/pull/32728) fix(docs): Another CSP hole for run.app to allow Kapa AI (@rusackas)
|
||||
- [#32727](https://github.com/apache/superset/pull/32727) fix(docs): poking ANOTHER hole in the CSP for the AI bot. (@rusackas)
|
||||
- [#32726](https://github.com/apache/superset/pull/32726) fix(asf): moving notifications to the top of `.asf.yaml` (@rusackas)
|
||||
- [#32724](https://github.com/apache/superset/pull/32724) fix(docs): allow recaptcha in CSP (@rusackas)
|
||||
- [#32713](https://github.com/apache/superset/pull/32713) fix(docs): Fixes scrolling issue with AI widget on docs site (@rusackas)
|
||||
- [#32703](https://github.com/apache/superset/pull/32703) fix(repo): re-enable GitHub Discussions (@rusackas)
|
||||
- [#32704](https://github.com/apache/superset/pull/32704) fix(docs): poking a CSP hole for Kapa AI widget (@rusackas)
|
||||
- [#32571](https://github.com/apache/superset/pull/32571) fix(no-restricted-imports): Fix overrides and include no-fa-icons-usage (@geido)
|
||||
- [#32658](https://github.com/apache/superset/pull/32658) fix(sync perms): Avoid UnboundLocalError during perm sync for DBs that don't support catalogs (@Vitor-Avila)
|
||||
- [#32381](https://github.com/apache/superset/pull/32381) fix(sqllab): Grid header menu (@justinpark)
|
||||
- [#32553](https://github.com/apache/superset/pull/32553) fix(comp/async-ace-editor): proper import of `ace-builds` (@hainenber)
|
||||
- [#32525](https://github.com/apache/superset/pull/32525) fix: always extract query source from request (@villebro)
|
||||
- [#32481](https://github.com/apache/superset/pull/32481) fix(docker compose): replace port 8088 with 9000 (@jpchev)
|
||||
- [#32401](https://github.com/apache/superset/pull/32401) fix: prevent nested transactions (@betodealmeida)
|
||||
- [#32377](https://github.com/apache/superset/pull/32377) fix: ephemeral CI fetching task ENI (@dpgaspar)
|
||||
- [#32333](https://github.com/apache/superset/pull/32333) fix(eslint-hook): ensure eslint hook receives arguments (@alveifbklsiu259)
|
||||
- [#32274](https://github.com/apache/superset/pull/32274) fix(sec): resolve Dependabot security alerts (@hainenber)
|
||||
- [#32018](https://github.com/apache/superset/pull/32018) fix: false negative on critical security related to eslint-plugin-translation-vars (@mistercrunch)
|
||||
|
||||
**Others**
|
||||
|
||||
- [#35176](https://github.com/apache/superset/pull/35176) chore: bump sqlglot to 27.15.2 (@betodealmeida)
|
||||
- [#34838](https://github.com/apache/superset/pull/34838) chore: bump FAB to 4.8.1 (@dpgaspar)
|
||||
- [#34800](https://github.com/apache/superset/pull/34800) chore: Add instruction for LLMs to use antd theme tokens (@kgabryje)
|
||||
- [#34693](https://github.com/apache/superset/pull/34693) chore(deps): downgrade pyarrow to v16 (@drummerwolli)
|
||||
- [#34701](https://github.com/apache/superset/pull/34701) docs: CVEs added to 5.0.0 and 4.1.3 documentation (@sha174n)
|
||||
- [#34606](https://github.com/apache/superset/pull/34606) refactor: Migrate ExploreChartPanel to typescript (@justinpark)
|
||||
- [#32663](https://github.com/apache/superset/pull/32663) chore: add more csv tests (@eschutho)
|
||||
- [#34653](https://github.com/apache/superset/pull/34653) chore: Increase memory limit on webpack ts checker plugin (@kgabryje)
|
||||
- [#34460](https://github.com/apache/superset/pull/34460) chore(deps-dev): bump eslint-import-resolver-typescript from 3.7.0 to 4.4.4 in /superset-frontend (@dependabot[bot])
|
||||
- [#34581](https://github.com/apache/superset/pull/34581) chore(deps): bump tmp from 0.2.1 to 0.2.4 in /superset-frontend/cypress-base (@dependabot[bot])
|
||||
- [#34646](https://github.com/apache/superset/pull/34646) chore(deps): bump tmp and inquirer in /superset-frontend (@dependabot[bot])
|
||||
- [#34536](https://github.com/apache/superset/pull/34536) chore: Refactor Menu.Item and cleanup console errors (@geido)
|
||||
- [#34481](https://github.com/apache/superset/pull/34481) chore(deps): bump googleapis from 130.0.0 to 154.1.0 in /superset-frontend (@dependabot[bot])
|
||||
- [#34442](https://github.com/apache/superset/pull/34442) chore: add tests to DatabaseConnectionForm/EncryptedField (@sadpandajoe)
|
||||
- [#34450](https://github.com/apache/superset/pull/34450) chore(deps): bump ws and @types/ws in /superset-websocket (@dependabot[bot])
|
||||
- [#34448](https://github.com/apache/superset/pull/34448) chore(deps-dev): bump @types/node from 22.10.3 to 24.1.0 in /superset-websocket (@dependabot[bot])
|
||||
- [#33889](https://github.com/apache/superset/pull/33889) chore(helm): bump app version to 5.0.0 (@brandon-kaplan)
|
||||
- [#34452](https://github.com/apache/superset/pull/34452) chore(deps-dev): bump globals from 16.0.0 to 16.3.0 in /superset-websocket (@dependabot[bot])
|
||||
- [#34453](https://github.com/apache/superset/pull/34453) chore(deps): update re-resizable requirement from ^6.10.1 to ^6.11.2 in /superset-frontend/packages/superset-ui-core (@dependabot[bot])
|
||||
- [#34468](https://github.com/apache/superset/pull/34468) chore(deps): update @deck.gl/aggregation-layers requirement from ^9.1.13 to ^9.1.14 in /superset-frontend/plugins/legacy-preset-chart-deckgl (@dependabot[bot])
|
||||
- [#34464](https://github.com/apache/superset/pull/34464) chore(deps-dev): bump @babel/runtime-corejs3 from 7.26.7 to 7.28.2 in /superset-frontend (@dependabot[bot])
|
||||
- [#34462](https://github.com/apache/superset/pull/34462) chore(deps-dev): update jest requirement from ^30.0.4 to ^30.0.5 in /superset-frontend/plugins/plugin-chart-pivot-table (@dependabot[bot])
|
||||
- [#34451](https://github.com/apache/superset/pull/34451) chore(deps-dev): update @types/prop-types requirement from ^15.7.2 to ^15.7.15 in /superset-frontend/packages/superset-ui-core (@dependabot[bot])
|
||||
- [#34457](https://github.com/apache/superset/pull/34457) chore(deps-dev): update jest requirement from ^30.0.4 to ^30.0.5 in /superset-frontend/packages/generator-superset (@dependabot[bot])
|
||||
- [#34461](https://github.com/apache/superset/pull/34461) chore(deps): bump @deck.gl/react from 9.1.13 to 9.1.14 in /superset-frontend (@dependabot[bot])
|
||||
- [#34501](https://github.com/apache/superset/pull/34501) chore(deps-dev): update jest requirement from ^30.0.4 to ^30.0.5 in /superset-frontend/plugins/plugin-chart-handlebars (@dependabot[bot])
|
||||
- [#34472](https://github.com/apache/superset/pull/34472) chore(deps): bump @babel/runtime from 7.26.10 to 7.28.2 in /superset-frontend (@dependabot[bot])
|
||||
- [#34454](https://github.com/apache/superset/pull/34454) chore(deps-dev): bump eslint-config-prettier from 10.1.5 to 10.1.8 in /superset-websocket (@dependabot[bot])
|
||||
- [#34474](https://github.com/apache/superset/pull/34474) chore(deps): bump react-draggable from 4.4.6 to 4.5.0 in /superset-frontend (@dependabot[bot])
|
||||
- [#34483](https://github.com/apache/superset/pull/34483) chore(deps): bump react-lines-ellipsis from 0.15.4 to 0.16.1 in /superset-frontend (@dependabot[bot])
|
||||
- [#34492](https://github.com/apache/superset/pull/34492) chore(deps-dev): bump eslint from 9.31.0 to 9.32.0 in /docs (@dependabot[bot])
|
||||
- [#34493](https://github.com/apache/superset/pull/34493) chore(deps-dev): bump typescript-eslint from 8.37.0 to 8.38.0 in /docs (@dependabot[bot])
|
||||
- [#34502](https://github.com/apache/superset/pull/34502) chore(deps): update react requirement from ^19.1.0 to ^19.1.1 in /superset-frontend/plugins/legacy-plugin-chart-chord (@dependabot[bot])
|
||||
- [#34487](https://github.com/apache/superset/pull/34487) chore(deps): bump @rjsf/validator-ajv8 from 5.24.9 to 5.24.12 in /superset-frontend (@dependabot[bot])
|
||||
- [#34489](https://github.com/apache/superset/pull/34489) chore(deps-dev): bump @babel/preset-react from 7.26.3 to 7.27.1 in /superset-frontend (@dependabot[bot])
|
||||
- [#34496](https://github.com/apache/superset/pull/34496) chore(deps-dev): bump eslint-plugin-prettier from 5.5.1 to 5.5.3 in /docs (@dependabot[bot])
|
||||
- [#34544](https://github.com/apache/superset/pull/34544) chore: Rename dataset creation buttons (@Vitor-Avila)
|
||||
- [#34515](https://github.com/apache/superset/pull/34515) chore(core): Add drawer to core ui components (@justinpark)
|
||||
- [#34444](https://github.com/apache/superset/pull/34444) chore(deps): update gh-pages requirement from ^6.2.0 to ^6.3.0 in /superset-frontend/packages/superset-ui-demo (@dependabot[bot])
|
||||
- [#34478](https://github.com/apache/superset/pull/34478) chore(deps-dev): bump @types/classnames from 2.3.0 to 2.3.4 in /superset-frontend (@dependabot[bot])
|
||||
- [#34482](https://github.com/apache/superset/pull/34482) chore(deps): bump dom-to-image-more from 3.5.0 to 3.6.0 in /superset-frontend (@dependabot[bot])
|
||||
- [#34480](https://github.com/apache/superset/pull/34480) chore(deps): bump @deck.gl/core from 9.1.13 to 9.1.14 in /superset-frontend (@dependabot[bot])
|
||||
- [#34484](https://github.com/apache/superset/pull/34484) chore(deps-dev): bump tsx from 4.19.4 to 4.20.3 in /superset-frontend (@dependabot[bot])
|
||||
- [#34485](https://github.com/apache/superset/pull/34485) chore(deps-dev): bump @babel/compat-data from 7.27.2 to 7.28.0 in /superset-frontend (@dependabot[bot])
|
||||
- [#34494](https://github.com/apache/superset/pull/34494) chore(deps): bump less from 4.3.0 to 4.4.0 in /docs (@dependabot[bot])
|
||||
- [#34495](https://github.com/apache/superset/pull/34495) chore(deps): bump antd from 5.26.3 to 5.26.7 in /docs (@dependabot[bot])
|
||||
- [#34497](https://github.com/apache/superset/pull/34497) chore(deps-dev): bump @eslint/js from 9.31.0 to 9.32.0 in /docs (@dependabot[bot])
|
||||
- [#34498](https://github.com/apache/superset/pull/34498) chore(deps): bump swagger-ui-react from 5.26.0 to 5.27.1 in /docs (@dependabot[bot])
|
||||
- [#34499](https://github.com/apache/superset/pull/34499) chore(deps-dev): bump eslint-config-prettier from 10.1.5 to 10.1.8 in /docs (@dependabot[bot])
|
||||
- [#34500](https://github.com/apache/superset/pull/34500) chore(deps-dev): bump webpack from 5.99.9 to 5.101.0 in /docs (@dependabot[bot])
|
||||
- [#34459](https://github.com/apache/superset/pull/34459) chore(deps): bump actions/first-interaction from 1 to 2 (@dependabot[bot])
|
||||
- [#34393](https://github.com/apache/superset/pull/34393) chore: update chart list e2e and component tests (@sadpandajoe)
|
||||
- [#34039](https://github.com/apache/superset/pull/34039) chore(deps-dev): update jest requirement from ^30.0.2 to ^30.0.4 in /superset-frontend/packages/generator-superset (@dependabot[bot])
|
||||
- [#34432](https://github.com/apache/superset/pull/34432) chore: Change button labels to sentence case (@kasiazjc)
|
||||
- [#34429](https://github.com/apache/superset/pull/34429) chore: Add bottom border to top navigation menu (@kasiazjc)
|
||||
- [#30119](https://github.com/apache/superset/pull/30119) build(deps): bump reselect from 4.1.7 to 5.1.1 in /superset-frontend (@dependabot[bot])
|
||||
- [#31534](https://github.com/apache/superset/pull/31534) chore(deps): bump d3-scale from 3.3.0 to 4.0.2 in /superset-frontend/packages/superset-ui-core (@dependabot[bot])
|
||||
- [#34391](https://github.com/apache/superset/pull/34391) docs(development): fix comment in the dockerfile (@harikirank)
|
||||
- [#34387](https://github.com/apache/superset/pull/34387) docs(development): fix typo in the dockerfile (@harikirank)
|
||||
- [#34335](https://github.com/apache/superset/pull/34335) chore(deps): bump cookie and @types/cookie in /superset-websocket (@dependabot[bot])
|
||||
- [#34326](https://github.com/apache/superset/pull/34326) build(deps): update `ag-grid` to non-breaking major v34 (@hainenber)
|
||||
- [#34341](https://github.com/apache/superset/pull/34341) docs(development): fix 2 typos in the dockerfile (@harikirank)
|
||||
- [#34371](https://github.com/apache/superset/pull/34371) chore: bump BigQuery dialect to 1.15.0 (@betodealmeida)
|
||||
- [#34317](https://github.com/apache/superset/pull/34317) style(FastVizSwitcher): Adjust padding for FastVizSwitcher selector (@EnxDev)
|
||||
- [#34311](https://github.com/apache/superset/pull/34311) style(chart): restyle table pagination (@imcewen02)
|
||||
- [#34302](https://github.com/apache/superset/pull/34302) chore: bump sqlglot to latest version (27.3.0) (@betodealmeida)
|
||||
- [#34270](https://github.com/apache/superset/pull/34270) chore: improve sqlglot parsing (@betodealmeida)
|
||||
- [#34288](https://github.com/apache/superset/pull/34288) chore: remove supposedly dev dep `html-webpack-plugin` from lockfile (@hainenber)
|
||||
- [#33997](https://github.com/apache/superset/pull/33997) chore(deps-dev): bump prettier from 3.5.3 to 3.6.2 in /superset-frontend (@dependabot[bot])
|
||||
- [#34285](https://github.com/apache/superset/pull/34285) chore(deps): bump axios from 1.10.0 to 1.11.0 in /docs (@dependabot[bot])
|
||||
- [#34067](https://github.com/apache/superset/pull/34067) style(Button): Vertically align icons across all buttons (@EnxDev)
|
||||
- [#34146](https://github.com/apache/superset/pull/34146) chore(docker): use editable mode in docker images (@mistercrunch)
|
||||
- [#34262](https://github.com/apache/superset/pull/34262) chore(deps-dev): bump form-data from 4.0.0 to 4.0.4 in /superset-embedded-sdk (@dependabot[bot])
|
||||
- [#34263](https://github.com/apache/superset/pull/34263) chore(deps): bump form-data from 4.0.0 to 4.0.4 in /docs (@dependabot[bot])
|
||||
- [#34265](https://github.com/apache/superset/pull/34265) chore(deps): bump form-data from 4.0.1 to 4.0.4 in /superset-frontend (@dependabot[bot])
|
||||
- [#34215](https://github.com/apache/superset/pull/34215) chore(deps): bump on-headers and morgan in /superset-websocket/utils/client-ws-app (@dependabot[bot])
|
||||
- [#34216](https://github.com/apache/superset/pull/34216) chore(deps): bump on-headers and compression in /superset-frontend (@dependabot[bot])
|
||||
- [#34217](https://github.com/apache/superset/pull/34217) chore: Updates files related to 4.1.3 release (@sadpandajoe)
|
||||
- [#33736](https://github.com/apache/superset/pull/33736) style(helm): Minor reformatting of helm chart templates (@dnskr)
|
||||
- [#34179](https://github.com/apache/superset/pull/34179) chore(Oracle): Update oracle column length to 128 (@msyavuz)
|
||||
- [#34163](https://github.com/apache/superset/pull/34163) docs(development): Fix typo in the documentation (@harikirank)
|
||||
- [#34149](https://github.com/apache/superset/pull/34149) chore(Tags): Sort tags by name if possible (@msyavuz)
|
||||
- [#34145](https://github.com/apache/superset/pull/34145) docs: remove duplicated line in `Running tests with act` section (@ongdisheng)
|
||||
- [#34125](https://github.com/apache/superset/pull/34125) build(dev-deps): clean up deprecated Babel proposal plugins (@hainenber)
|
||||
- [#34138](https://github.com/apache/superset/pull/34138) chore(deps): bump flask-cors from 4.0.2 to 6.0.0 (@dependabot[bot])
|
||||
- [#34139](https://github.com/apache/superset/pull/34139) chore: remove unnecessary disables (@betodealmeida)
|
||||
- [#33990](https://github.com/apache/superset/pull/33990) chore(deps): bump react-json-tree from 0.17.0 to 0.20.0 in /superset-frontend (@dependabot[bot])
|
||||
- [#33486](https://github.com/apache/superset/pull/33486) chore(deps): bump react-error-boundary from 5.0.0 to 6.0.0 in /superset-frontend (@dependabot[bot])
|
||||
- [#34093](https://github.com/apache/superset/pull/34093) chore: clean up more flask/jinja html views (@mistercrunch)
|
||||
- [#34097](https://github.com/apache/superset/pull/34097) chore: Improve performance to load chart's save modal (@Vitor-Avila)
|
||||
- [#34079](https://github.com/apache/superset/pull/34079) chore: Improve performance to load the chart properties modal (@Vitor-Avila)
|
||||
- [#34104](https://github.com/apache/superset/pull/34104) chore(deps-dev): bump webpack-dev-server from 4.15.2 to 5.2.1 in /superset-frontend (@dependabot[bot])
|
||||
- [#34057](https://github.com/apache/superset/pull/34057) chore: move auth e2e tests to component tests (@sadpandajoe)
|
||||
- [#34042](https://github.com/apache/superset/pull/34042) chore(deps): bump @fontsource/inter from 5.1.1 to 5.2.6 in /superset-frontend (@dependabot[bot])
|
||||
- [#34029](https://github.com/apache/superset/pull/34029) chore(deps): bump ioredis from 4.28.5 to 5.6.1 in /superset-websocket (@dependabot[bot])
|
||||
- [#34075](https://github.com/apache/superset/pull/34075) chore: Use select_columns on chart's dashboard filter (@Vitor-Avila)
|
||||
- [#34028](https://github.com/apache/superset/pull/34028) chore: refactor react-syntax-highlither to handle dark themes (@mistercrunch)
|
||||
- [#34056](https://github.com/apache/superset/pull/34056) chore: remove some of the deprecated theme.colors.* (@mistercrunch)
|
||||
- [#34059](https://github.com/apache/superset/pull/34059) chore(deps): bump tar-fs from 2.1.2 to 3.1.0 in /superset-frontend (@dependabot[bot])
|
||||
- [#32928](https://github.com/apache/superset/pull/32928) chore(deps): update yeoman-generator requirement from ^7.4.0 to ^7.5.1 in /superset-frontend/packages/generator-superset (@dependabot[bot])
|
||||
- [#32949](https://github.com/apache/superset/pull/32949) chore(deps): bump react from 17.0.2 to 19.1.0 in /superset-frontend/plugins/legacy-plugin-chart-chord (@dependabot[bot])
|
||||
- [#33481](https://github.com/apache/superset/pull/33481) chore(deps-dev): update fork-ts-checker-webpack-plugin requirement from ^9.0.2 to ^9.1.0 in /superset-frontend/packages/superset-ui-demo (@dependabot[bot])
|
||||
- [#33485](https://github.com/apache/superset/pull/33485) chore(deps): update @deck.gl/aggregation-layers requirement from ^9.0.38 to ^9.1.12 in /superset-frontend/plugins/legacy-preset-chart-deckgl (@dependabot[bot])
|
||||
- [#32946](https://github.com/apache/superset/pull/32946) chore(deps-dev): bump webpack-dev-server from 4.15.2 to 5.2.1 in /superset-frontend (@dependabot[bot])
|
||||
- [#33986](https://github.com/apache/superset/pull/33986) chore(deps-dev): bump @types/jest from 29.5.14 to 30.0.0 in /superset-frontend/plugins/plugin-chart-handlebars (@dependabot[bot])
|
||||
- [#33496](https://github.com/apache/superset/pull/33496) chore(deps-dev): bump yeoman-test from 8.3.0 to 10.1.1 in /superset-frontend (@dependabot[bot])
|
||||
- [#33995](https://github.com/apache/superset/pull/33995) chore(deps): bump @storybook/addon-actions from 8.1.11 to 9.0.8 in /superset-frontend/packages/superset-ui-demo (@dependabot[bot])
|
||||
- [#32441](https://github.com/apache/superset/pull/32441) chore(deps): update @types/d3-scale requirement from ^4.0.8 to ^4.0.9 in /superset-frontend/plugins/plugin-chart-word-cloud (@dependabot[bot])
|
||||
- [#32082](https://github.com/apache/superset/pull/32082) chore(deps): update dompurify requirement from ^3.2.4 to ^3.2.6 in /superset-frontend/plugins/legacy-preset-chart-nvd3 (@dependabot[bot])
|
||||
- [#32945](https://github.com/apache/superset/pull/32945) chore(deps): bump remark-gfm from 3.0.1 to 4.0.1 in /superset-frontend (@dependabot[bot])
|
||||
- [#32953](https://github.com/apache/superset/pull/32953) chore(deps): bump @ant-design/icons from 5.6.1 to 6.0.0 in /docs (@dependabot[bot])
|
||||
- [#32439](https://github.com/apache/superset/pull/32439) chore(deps-dev): bump typescript from 5.6.2 to 5.7.3 in /superset-websocket (@dependabot[bot])
|
||||
- [#32080](https://github.com/apache/superset/pull/32080) chore(deps-dev): update @types/lodash requirement from ^4.17.16 to ^4.17.20 in /superset-frontend/packages/superset-ui-core (@dependabot[bot])
|
||||
- [#33991](https://github.com/apache/superset/pull/33991) chore(deps-dev): bump cheerio from 1.0.0-rc.10 to 1.1.0 in /superset-frontend (@dependabot[bot])
|
||||
- [#33989](https://github.com/apache/superset/pull/33989) chore(deps-dev): bump webpack-visualizer-plugin2 from 1.1.0 to 1.2.0 in /superset-frontend (@dependabot[bot])
|
||||
- [#32093](https://github.com/apache/superset/pull/32093) chore(deps-dev): update fs-extra requirement from ^11.2.0 to ^11.3.0 in /superset-frontend/packages/generator-superset (@dependabot[bot])
|
||||
- [#32077](https://github.com/apache/superset/pull/32077) chore(deps): update @types/geojson requirement from ^7946.0.15 to ^7946.0.16 in /superset-frontend/plugins/legacy-preset-chart-deckgl (@dependabot[bot])
|
||||
- [#31560](https://github.com/apache/superset/pull/31560) chore(deps): bump @emotion/styled from 11.3.0 to 11.14.0 in /superset-frontend (@dependabot[bot])
|
||||
- [#34034](https://github.com/apache/superset/pull/34034) chore(deps-dev): update jest requirement from ^30.0.2 to ^30.0.4 in /superset-frontend/plugins/plugin-chart-handlebars (@dependabot[bot])
|
||||
- [#34036](https://github.com/apache/superset/pull/34036) chore(deps-dev): update jest requirement from ^30.0.2 to ^30.0.4 in /superset-frontend/plugins/plugin-chart-pivot-table (@dependabot[bot])
|
||||
- [#34037](https://github.com/apache/superset/pull/34037) chore(deps-dev): update @types/lodash requirement from ^4.17.16 to ^4.17.20 in /superset-frontend/plugins/plugin-chart-handlebars (@dependabot[bot])
|
||||
- [#34038](https://github.com/apache/superset/pull/34038) chore(deps-dev): update @babel/types requirement from ^7.26.9 to ^7.28.0 in /superset-frontend/plugins/plugin-chart-pivot-table (@dependabot[bot])
|
||||
- [#34043](https://github.com/apache/superset/pull/34043) chore(deps): bump ace-builds from 1.43.0 to 1.43.1 in /superset-frontend (@dependabot[bot])
|
||||
- [#34008](https://github.com/apache/superset/pull/34008) chore(deps): bump mapbox-gl from 2.15.0 to 3.13.0 in /superset-frontend (@dependabot[bot])
|
||||
- [#34035](https://github.com/apache/superset/pull/34035) chore(deps-dev): bump @types/lodash from 4.17.13 to 4.17.20 in /superset-websocket (@dependabot[bot])
|
||||
- [#33992](https://github.com/apache/superset/pull/33992) chore(deps): bump @emotion/styled from 11.14.0 to 11.14.1 in /superset-frontend (@dependabot[bot])
|
||||
- [#33987](https://github.com/apache/superset/pull/33987) chore(deps-dev): bump @applitools/eyes-storybook from 3.53.4 to 3.55.6 in /superset-frontend (@dependabot[bot])
|
||||
- [#34033](https://github.com/apache/superset/pull/34033) chore(deps-dev): bump prettier from 3.4.2 to 3.6.2 in /superset-websocket (@dependabot[bot])
|
||||
- [#34041](https://github.com/apache/superset/pull/34041) chore(deps): bump swagger-ui-react from 5.25.3 to 5.26.0 in /docs (@dependabot[bot])
|
||||
- [#33979](https://github.com/apache/superset/pull/33979) build(dev-deps): upgrade Jest to major version v30 (@hainenber)
|
||||
- [#34004](https://github.com/apache/superset/pull/34004) chore(deps): bump hot-shots from 10.2.1 to 11.1.0 in /superset-websocket (@dependabot[bot])
|
||||
- [#34003](https://github.com/apache/superset/pull/34003) chore(deps-dev): bump @docusaurus/tsconfig from 3.8.0 to 3.8.1 in /docs (@dependabot[bot])
|
||||
- [#34002](https://github.com/apache/superset/pull/34002) chore(deps): bump ace-builds from 1.41.0 to 1.43.0 in /superset-frontend (@dependabot[bot])
|
||||
- [#34001](https://github.com/apache/superset/pull/34001) chore(deps): bump swagger-ui-react from 5.25.2 to 5.25.3 in /docs (@dependabot[bot])
|
||||
- [#34000](https://github.com/apache/superset/pull/34000) chore(deps-dev): bump eslint from 9.27.0 to 9.30.0 in /superset-websocket (@dependabot[bot])
|
||||
- [#33985](https://github.com/apache/superset/pull/33985) chore(deps-dev): bump @babel/cli from 7.26.4 to 7.27.2 in /superset-frontend (@dependabot[bot])
|
||||
- [#33999](https://github.com/apache/superset/pull/33999) chore(deps): bump actions/cache from 3 to 4 (@dependabot[bot])
|
||||
- [#33982](https://github.com/apache/superset/pull/33982) chore(deps): bump antd from 5.25.1 to 5.26.3 in /docs (@dependabot[bot])
|
||||
- [#33967](https://github.com/apache/superset/pull/33967) chore: replace `querystring` usage with native `URLSearchParams` API (@hainenber)
|
||||
- [#33972](https://github.com/apache/superset/pull/33972) docs: Fix typo in UPDATING.md regarding translations in version 5.0.0 (@hugo19941994)
|
||||
- [#33887](https://github.com/apache/superset/pull/33887) chore(build): refactor plugin build script to remove unused stanzas (@hainenber)
|
||||
- [#32891](https://github.com/apache/superset/pull/32891) docs: pypi-installation on Ubuntu 24.04 and statsd package for event-logging (@125m125)
|
||||
- [#33934](https://github.com/apache/superset/pull/33934) chore(translations): Update FR language (@Eric-Brison)
|
||||
- [#33927](https://github.com/apache/superset/pull/33927) build(be/deps): upgrade `urllib3` to major v2 (@hainenber)
|
||||
- [#33936](https://github.com/apache/superset/pull/33936) docs(security): add Q&A related to CVE scans to FAQ (@sfirke)
|
||||
- [#33910](https://github.com/apache/superset/pull/33910) chore(superset-embedded-sdk): bump version for theming (@msyavuz)
|
||||
- [#33909](https://github.com/apache/superset/pull/33909) style(AsyncAceEditor): make Ace gutter line color theme-aware (@EnxDev)
|
||||
- [#33872](https://github.com/apache/superset/pull/33872) chore(docs): bump references to docker image versions upon release of 5.0.0 (@sfirke)
|
||||
- [#33869](https://github.com/apache/superset/pull/33869) chore: Updates files related to 5.0.0 release (@michael-s-molina)
|
||||
- [#33868](https://github.com/apache/superset/pull/33868) build(be/deps): replace `importlib_metadata` usage with native Python 3.10+ `importlib.metadata` (@hainenber)
|
||||
- [#33854](https://github.com/apache/superset/pull/33854) build(dev-deps): update `fetch-mock` to v11 (@hainenber)
|
||||
- [#33853](https://github.com/apache/superset/pull/33853) build(deps): remove legacy browser polyfills (@hainenber)
|
||||
- [#33866](https://github.com/apache/superset/pull/33866) chore(Icons): Add UsergroupAddOutlined icon (@EnxDev)
|
||||
- [#33850](https://github.com/apache/superset/pull/33850) style(menu): Reduce bottom border width of menu item (@EnxDev)
|
||||
- [#33848](https://github.com/apache/superset/pull/33848) chore: use mysql dialect for Pinot (@betodealmeida)
|
||||
- [#33790](https://github.com/apache/superset/pull/33790) refactor: rename docker-compose files and update references (@polRk)
|
||||
- [#33670](https://github.com/apache/superset/pull/33670) docs: Update STANDARD_ROLES.md, delete 7 permissions "RowLevelSecurityFiltersModelView" (@xavier-GitHub76)
|
||||
- [#33642](https://github.com/apache/superset/pull/33642) chore(deps-dev): bump @docusaurus/module-type-aliases from 3.7.0 to 3.8.0 in /docs (@dependabot[bot])
|
||||
- [#33818](https://github.com/apache/superset/pull/33818) chore(docs): resolve 3 vulnerabilities (@hainenber)
|
||||
- [#33795](https://github.com/apache/superset/pull/33795) chore(🦾): bump python flask-caching subpackage(s) (@github-actions[bot])
|
||||
- [#33798](https://github.com/apache/superset/pull/33798) chore(🦾): bump python sqlglot 26.17.1 -> 26.28.1 (@github-actions[bot])
|
||||
- [#33792](https://github.com/apache/superset/pull/33792) chore(🦾): bump python flask-session subpackage(s) (@github-actions[bot])
|
||||
- [#33793](https://github.com/apache/superset/pull/33793) chore(🦾): bump python shillelagh subpackage(s) (@github-actions[bot])
|
||||
- [#33799](https://github.com/apache/superset/pull/33799) chore(🦾): bump python flask-wtf subpackage(s) (@github-actions[bot])
|
||||
- [#33797](https://github.com/apache/superset/pull/33797) chore(🦾): bump python flask subpackage(s) (@github-actions[bot])
|
||||
- [#33796](https://github.com/apache/superset/pull/33796) chore(🦾): bump python click 8.2.0 -> 8.2.1 (@github-actions[bot])
|
||||
- [#33800](https://github.com/apache/superset/pull/33800) chore(🦾): bump python flask-compress subpackage(s) (@github-actions[bot])
|
||||
- [#32587](https://github.com/apache/superset/pull/32587) refactor(Menu): Use items prop instead of deprecated Menu.Item HOC (@msyavuz)
|
||||
- [#26803](https://github.com/apache/superset/pull/26803) chore: add pylint rule for SQL importing (SIP-117) (@betodealmeida)
|
||||
- [#33396](https://github.com/apache/superset/pull/33396) chore(Accessibility): Improve keyboard navigation and screen access (@geido)
|
||||
- [#33767](https://github.com/apache/superset/pull/33767) chore: auto-focus modal input when deleting assets (@betodealmeida)
|
||||
- [#33696](https://github.com/apache/superset/pull/33696) chore: Convert alert and report cypress tests to component tests (@sadpandajoe)
|
||||
- [#33643](https://github.com/apache/superset/pull/33643) chore(deps-dev): bump webpack from 5.99.8 to 5.99.9 in /docs (@dependabot[bot])
|
||||
- [#33645](https://github.com/apache/superset/pull/33645) chore(deps-dev): bump @docusaurus/tsconfig from 3.7.0 to 3.8.0 in /docs (@dependabot[bot])
|
||||
- [#33650](https://github.com/apache/superset/pull/33650) chore(deps-dev): bump @typescript-eslint/parser from 8.29.0 to 8.33.0 in /superset-websocket (@dependabot[bot])
|
||||
- [#33721](https://github.com/apache/superset/pull/33721) docs: fix typo and improve alt text in README (@lourduradjou)
|
||||
- [#33715](https://github.com/apache/superset/pull/33715) chore: delete remaining Enzyme tests (@mistercrunch)
|
||||
- [#33714](https://github.com/apache/superset/pull/33714) docs: clarify how `requirements/` should be modified (@mistercrunch)
|
||||
- [#33704](https://github.com/apache/superset/pull/33704) chore: remove unused parameter (@betodealmeida)
|
||||
- [#33701](https://github.com/apache/superset/pull/33701) chore: update sqlglot dialect map (@betodealmeida)
|
||||
- [#33661](https://github.com/apache/superset/pull/33661) chore: simplify query cleanup using dict.pop instead of suppressing exception (@dpgaspar)
|
||||
- [#33568](https://github.com/apache/superset/pull/33568) chore: 100% test coverage for SQL parsing (@betodealmeida)
|
||||
- [#33665](https://github.com/apache/superset/pull/33665) docs: add HPE to users list (@anmol-hpe)
|
||||
- [#33662](https://github.com/apache/superset/pull/33662) docs: CVE-2025-48912 added to 4.1.2 (@sha174n)
|
||||
- [#33619](https://github.com/apache/superset/pull/33619) chore: make DB syntax errors 400 (@betodealmeida)
|
||||
- [#33622](https://github.com/apache/superset/pull/33622) chore(deps-dev): bump fastify from 4.29.0 to 4.29.1 in /superset-frontend (@dependabot[bot])
|
||||
- [#33607](https://github.com/apache/superset/pull/33607) chore: bump FAB to 4.7.0 (@dpgaspar)
|
||||
- [#33474](https://github.com/apache/superset/pull/33474) chore: remove parse_sql (@betodealmeida)
|
||||
- [#33515](https://github.com/apache/superset/pull/33515) chore: sql/parse cleanup (@betodealmeida)
|
||||
- [#33567](https://github.com/apache/superset/pull/33567) chore(alerts & reports): increase Playwright timeout from 30 -> 60 seconds (@sfirke)
|
||||
- [#33566](https://github.com/apache/superset/pull/33566) docs(docker build): add more packages needed for production features (@sfirke)
|
||||
- [#33478](https://github.com/apache/superset/pull/33478) chore(deps-dev): bump eslint-config-prettier from 9.1.0 to 10.1.5 in /superset-websocket (@dependabot[bot])
|
||||
- [#33489](https://github.com/apache/superset/pull/33489) chore(deps-dev): bump babel-loader from 9.2.1 to 10.0.0 in /superset-frontend (@dependabot[bot])
|
||||
- [#33488](https://github.com/apache/superset/pull/33488) chore(deps): bump less-loader from 11.1.4 to 12.3.0 in /docs (@dependabot[bot])
|
||||
- [#33477](https://github.com/apache/superset/pull/33477) chore(deps-dev): bump eslint from 9.17.0 to 9.27.0 in /superset-websocket (@dependabot[bot])
|
||||
- [#33457](https://github.com/apache/superset/pull/33457) chore: remove is_select_query (@betodealmeida)
|
||||
- [#33549](https://github.com/apache/superset/pull/33549) chore: remove useless-suppression (@betodealmeida)
|
||||
- [#33539](https://github.com/apache/superset/pull/33539) chore(Icons): Additional Ant Design Icons (@geido)
|
||||
- [#33469](https://github.com/apache/superset/pull/33469) chore(fab): bumped fab from 4.6.3 to 4.6.4 (@alexandrusoare)
|
||||
- [#33498](https://github.com/apache/superset/pull/33498) chore(deps): bump ace-builds from 1.37.5 to 1.41.0 in /superset-frontend (@dependabot[bot])
|
||||
- [#33476](https://github.com/apache/superset/pull/33476) chore(deps): bump debug from 4.4.0 to 4.4.1 in /superset-websocket/utils/client-ws-app (@dependabot[bot])
|
||||
- [#33491](https://github.com/apache/superset/pull/33491) chore(deps-dev): bump eslint-config-prettier from 10.1.2 to 10.1.5 in /docs (@dependabot[bot])
|
||||
- [#33492](https://github.com/apache/superset/pull/33492) chore(deps-dev): bump webpack from 5.99.7 to 5.99.8 in /docs (@dependabot[bot])
|
||||
- [#33490](https://github.com/apache/superset/pull/33490) chore(deps): bump antd from 5.24.9 to 5.25.1 in /docs (@dependabot[bot])
|
||||
- [#33499](https://github.com/apache/superset/pull/33499) chore(deps-dev): bump @babel/preset-env from 7.26.7 to 7.27.2 in /superset-frontend (@dependabot[bot])
|
||||
- [#33458](https://github.com/apache/superset/pull/33458) docs: added europace to INTHEWILD.md (@Bierbarbar)
|
||||
- [#33472](https://github.com/apache/superset/pull/33472) docs(installation): show example of extending Docker image (@sfirke)
|
||||
- [#32948](https://github.com/apache/superset/pull/32948) chore(deps): bump express from 4.21.2 to 5.1.0 in /superset-websocket/utils/client-ws-app (@dependabot[bot])
|
||||
- [#33278](https://github.com/apache/superset/pull/33278) chore(🦾): bump python shillelagh subpackage(s) (@github-actions[bot])
|
||||
- [#33435](https://github.com/apache/superset/pull/33435) docs: CVEs fixed on 4.1.2 (@sha174n)
|
||||
- [#33397](https://github.com/apache/superset/pull/33397) chore: Add missing ECharts tags (@DamianPendrak)
|
||||
- [#30878](https://github.com/apache/superset/pull/30878) docs: fix for role sync issues in case of custom OAuth2 configuration (@ved-kashyap-samsung)
|
||||
- [#33319](https://github.com/apache/superset/pull/33319) chore(deps): bump antd from 5.24.5 to 5.24.9 in /docs (@dependabot[bot])
|
||||
- [#33378](https://github.com/apache/superset/pull/33378) chore: regenerate `openapi.json` (@betodealmeida)
|
||||
- [#33279](https://github.com/apache/superset/pull/33279) chore(🦾): bump python markdown 3.7 -> 3.8 (@github-actions[bot])
|
||||
- [#33370](https://github.com/apache/superset/pull/33370) chore(🦾): bump python sshtunnel subpackage(s) (@github-actions[bot])
|
||||
- [#33371](https://github.com/apache/superset/pull/33371) chore(🦾): bump python cryptography 44.0.2 -> 44.0.3 (@github-actions[bot])
|
||||
- [#33369](https://github.com/apache/superset/pull/33369) chore(🦾): bump python humanize 4.12.2 -> 4.12.3 (@github-actions[bot])
|
||||
- [#33368](https://github.com/apache/superset/pull/33368) chore(🦾): bump python sqlglot 26.16.2 -> 26.16.4 (@github-actions[bot])
|
||||
- [#33318](https://github.com/apache/superset/pull/33318) chore(deps): bump swagger-ui-react from 5.20.2 to 5.21.0 in /docs (@dependabot[bot])
|
||||
- [#33323](https://github.com/apache/superset/pull/33323) chore(deps-dev): update ts-loader requirement from ^9.5.1 to ^9.5.2 in /superset-frontend/packages/superset-ui-demo (@dependabot[bot])
|
||||
- [#33311](https://github.com/apache/superset/pull/33311) chore(deps): bump uuid from 11.0.2 to 11.1.0 in /superset-websocket (@dependabot[bot])
|
||||
- [#33312](https://github.com/apache/superset/pull/33312) chore(deps-dev): bump @eslint/js from 9.17.0 to 9.25.1 in /superset-websocket (@dependabot[bot])
|
||||
- [#33317](https://github.com/apache/superset/pull/33317) chore(deps): bump less from 4.2.2 to 4.3.0 in /docs (@dependabot[bot])
|
||||
- [#33350](https://github.com/apache/superset/pull/33350) docs(docker-builds.mdx): clarify dockerize images (@jdorel)
|
||||
- [#33315](https://github.com/apache/superset/pull/33315) chore(deps-dev): bump eslint-config-prettier from 10.1.1 to 10.1.2 in /docs (@dependabot[bot])
|
||||
- [#33320](https://github.com/apache/superset/pull/33320) chore(deps-dev): bump typescript from 5.8.2 to 5.8.3 in /docs (@dependabot[bot])
|
||||
- [#33314](https://github.com/apache/superset/pull/33314) chore(deps-dev): bump eslint-plugin-react from 7.37.4 to 7.37.5 in /docs (@dependabot[bot])
|
||||
- [#33316](https://github.com/apache/superset/pull/33316) chore(deps-dev): bump webpack from 5.98.0 to 5.99.7 in /docs (@dependabot[bot])
|
||||
- [#33321](https://github.com/apache/superset/pull/33321) chore(deps): bump @rjsf/validator-ajv8 from 5.24.1 to 5.24.9 in /superset-frontend (@dependabot[bot])
|
||||
- [#33332](https://github.com/apache/superset/pull/33332) chore(deps-dev): bump @babel/plugin-transform-runtime from 7.25.9 to 7.27.1 in /superset-frontend (@dependabot[bot])
|
||||
- [#33333](https://github.com/apache/superset/pull/33333) chore(deps): bump react-intersection-observer from 9.15.1 to 9.16.0 in /superset-frontend (@dependabot[bot])
|
||||
- [#31476](https://github.com/apache/superset/pull/31476) chore(deps): Upgrade pyarrow to 18.1.0 (@phillipleblanc)
|
||||
- [#33277](https://github.com/apache/superset/pull/33277) chore(🦾): bump python importlib-metadata 8.6.1 -> 8.7.0 (@github-actions[bot])
|
||||
- [#33280](https://github.com/apache/superset/pull/33280) chore(🦾): bump python mako 1.3.9 -> 1.3.10 (@github-actions[bot])
|
||||
- [#33281](https://github.com/apache/superset/pull/33281) chore(🦾): bump python pyparsing 3.2.2 -> 3.2.3 (@github-actions[bot])
|
||||
- [#33257](https://github.com/apache/superset/pull/33257) chore(🦾): bump python celery 5.4.0 -> 5.5.2 (@github-actions[bot])
|
||||
- [#33259](https://github.com/apache/superset/pull/33259) chore(🦾): bump python packaging 24.2 -> 25.0 (@github-actions[bot])
|
||||
- [#33260](https://github.com/apache/superset/pull/33260) chore(🦾): bump python deprecation subpackage(s) (@github-actions[bot])
|
||||
- [#33262](https://github.com/apache/superset/pull/33262) chore(🦾): bump python python-dotenv 1.0.1 -> 1.1.0 (@github-actions[bot])
|
||||
- [#33263](https://github.com/apache/superset/pull/33263) chore(🦾): bump python pandas subpackage(s) (@github-actions[bot])
|
||||
- [#33266](https://github.com/apache/superset/pull/33266) chore(🦾): bump python sqlglot 26.11.1 -> 26.16.2 (@github-actions[bot])
|
||||
- [#33265](https://github.com/apache/superset/pull/33265) chore(🦾): bump python gunicorn subpackage(s) (@github-actions[bot])
|
||||
- [#33258](https://github.com/apache/superset/pull/33258) chore(🦾): bump python croniter subpackage(s) (@github-actions[bot])
|
||||
- [#33236](https://github.com/apache/superset/pull/33236) chore: add some utils tests (@eschutho)
|
||||
- [#33137](https://github.com/apache/superset/pull/33137) docs(installation): compare installation methods (@sfirke)
|
||||
- [#33210](https://github.com/apache/superset/pull/33210) docs: Add note on SQL execution security considerations (@sha174n)
|
||||
- [#30047](https://github.com/apache/superset/pull/30047) docs: improve documentation(docs): clarify URL encoding requirement for connection strings (@kalai-logicsoft)
|
||||
- [#33197](https://github.com/apache/superset/pull/33197) chore(deps-dev): bump http-proxy-middleware from 2.0.7 to 2.0.9 in /superset-frontend (@dependabot[bot])
|
||||
- [#33173](https://github.com/apache/superset/pull/33173) docs: add a high-level architecture diagram to the docs (@mistercrunch)
|
||||
- [#33102](https://github.com/apache/superset/pull/33102) chore(deps): bump @babel/runtime from 7.17.2 to 7.27.0 in /superset-frontend/cypress-base (@dependabot[bot])
|
||||
- [#29828](https://github.com/apache/superset/pull/29828) chore(translations): Update PT-BR language (partial) (@felipegranado)
|
||||
- [#33079](https://github.com/apache/superset/pull/33079) chore: Update INTHEWILD.md (@Pedro-Gato)
|
||||
- [#33074](https://github.com/apache/superset/pull/33074) chore: Added Formbricks to INTHEWILD.md (@jobenjada)
|
||||
- [#32941](https://github.com/apache/superset/pull/32941) chore(deps-dev): bump lerna from 8.1.9 to 8.2.1 in /superset-frontend (@dependabot[bot])
|
||||
- [#33045](https://github.com/apache/superset/pull/33045) docs: clarify docker-compose-image-tag instructions (@mistercrunch)
|
||||
- [#33061](https://github.com/apache/superset/pull/33061) chore(helm): bump appVersion to 4.1.2 (@villebro)
|
||||
- [#33028](https://github.com/apache/superset/pull/33028) chore(deps): bump estree-util-value-to-estree from 3.1.1 to 3.3.3 in /docs (@dependabot[bot])
|
||||
- [#33018](https://github.com/apache/superset/pull/33018) docs: add WinWin Network(马上赢) to users list (@Ookong)
|
||||
- [#32890](https://github.com/apache/superset/pull/32890) refactor(IconButton): Refactor IconButton to use Ant Design 5 Card (@Sameerali0)
|
||||
- [#32999](https://github.com/apache/superset/pull/32999) docs: Update documentation about publishing a dashboard (@hverlin)
|
||||
- [#33001](https://github.com/apache/superset/pull/33001) chore(Databricks): Display older Databricks driver as legacy (@Vitor-Avila)
|
||||
- [#32922](https://github.com/apache/superset/pull/32922) chore: bump marshmallow-sqlalchemy to 1.4.0 (@mistercrunch)
|
||||
- [#32952](https://github.com/apache/superset/pull/32952) chore(deps-dev): bump eslint-config-prettier from 10.0.2 to 10.1.1 in /docs (@dependabot[bot])
|
||||
- [#32951](https://github.com/apache/superset/pull/32951) chore(deps): bump antd from 5.24.2 to 5.24.5 in /docs (@dependabot[bot])
|
||||
- [#32950](https://github.com/apache/superset/pull/32950) chore(deps): bump swagger-ui-react from 5.20.0 to 5.20.2 in /docs (@dependabot[bot])
|
||||
- [#32939](https://github.com/apache/superset/pull/32939) chore(deps-dev): bump @babel/compat-data from 7.26.5 to 7.26.8 in /superset-frontend (@dependabot[bot])
|
||||
- [#32937](https://github.com/apache/superset/pull/32937) chore(deps-dev): bump css-minimizer-webpack-plugin from 7.0.0 to 7.0.2 in /superset-frontend (@dependabot[bot])
|
||||
- [#32927](https://github.com/apache/superset/pull/32927) chore(deps): update @types/react-redux requirement from ^7.1.10 to ^7.1.34 in /superset-frontend/plugins/plugin-chart-echarts (@dependabot[bot])
|
||||
- [#32925](https://github.com/apache/superset/pull/32925) chore(deps-dev): bump @typescript-eslint/parser from 8.19.0 to 8.29.0 in /superset-websocket (@dependabot[bot])
|
||||
- [#32924](https://github.com/apache/superset/pull/32924) chore(deps-dev): bump ts-jest from 29.2.5 to 29.3.1 in /superset-websocket (@dependabot[bot])
|
||||
- [#32585](https://github.com/apache/superset/pull/32585) chore(reports): add task for slack channels warm-up (@Usiel)
|
||||
- [#32888](https://github.com/apache/superset/pull/32888) refactor(jinja macro): Update current_user_roles() macro to fetch roles from existing get_user_roles() method (@bmaquet)
|
||||
- [#32901](https://github.com/apache/superset/pull/32901) chore(🦾): bump python grpcio 1.68.0 -> 1.71.0 (@github-actions[bot])
|
||||
- [#32880](https://github.com/apache/superset/pull/32880) refactor(Icons): Add typing support and improve structure (@geido)
|
||||
- [#32860](https://github.com/apache/superset/pull/32860) chore: Removes unused file (@michael-s-molina)
|
||||
- [#32822](https://github.com/apache/superset/pull/32822) docs: added a link to badge releases (@Radovenchyk)
|
||||
- [#32831](https://github.com/apache/superset/pull/32831) chore: updating files for release 4.1.2 (@sadpandajoe)
|
||||
- [#32826](https://github.com/apache/superset/pull/32826) chore(🦾): bump python humanize 4.12.1 -> 4.12.2 (@github-actions[bot])
|
||||
- [#32827](https://github.com/apache/superset/pull/32827) chore(🦾): bump python pyparsing 3.2.1 -> 3.2.2 (@github-actions[bot])
|
||||
- [#32828](https://github.com/apache/superset/pull/32828) chore(🦾): bump python shillelagh subpackage(s) (@github-actions[bot])
|
||||
- [#32825](https://github.com/apache/superset/pull/32825) chore(🦾): bump python click-option-group 0.5.6 -> 0.5.7 (@github-actions[bot])
|
||||
- [#32744](https://github.com/apache/superset/pull/32744) chore(🦾): bump python flask-appbuilder subpackage(s) (@github-actions[bot])
|
||||
- [#32749](https://github.com/apache/superset/pull/32749) chore: replaced the workflow badge link (@Radovenchyk)
|
||||
- [#32742](https://github.com/apache/superset/pull/32742) chore(🦾): bump python slack-sdk 3.34.0 -> 3.35.0 (@github-actions[bot])
|
||||
- [#31255](https://github.com/apache/superset/pull/31255) chore(🦾): bump python shillelagh subpackage(s) (@github-actions[bot])
|
||||
- [#32743](https://github.com/apache/superset/pull/32743) chore(🦾): bump python celery subpackage(s) (@github-actions[bot])
|
||||
- [#32711](https://github.com/apache/superset/pull/32711) chore(lang): update and fix french translations (@CharlesNkdl)
|
||||
- [#31251](https://github.com/apache/superset/pull/31251) chore(🦾): bump python flask-appbuilder subpackage(s) (@github-actions[bot])
|
||||
- [#32112](https://github.com/apache/superset/pull/32112) refactor(Icons): Replaces custom icons with Ant Design 5 icons (@EnxDev)
|
||||
- [#31247](https://github.com/apache/superset/pull/31247) chore(🦾): bump python greenlet (@github-actions[bot])
|
||||
- [#32686](https://github.com/apache/superset/pull/32686) chore(helm): bump postgresql image tag in helm values (@mPyKen)
|
||||
- [#32714](https://github.com/apache/superset/pull/32714) chore(asf): Another `.asf.yaml` touch-up. (@rusackas)
|
||||
- [#32689](https://github.com/apache/superset/pull/32689) chore(docs): touching up AI styling/text (@rusackas)
|
||||
- [#32712](https://github.com/apache/superset/pull/32712) chore(asf): trying to fix `.asf.yaml` again to re-enable Discussions (@rusackas)
|
||||
- [#32710](https://github.com/apache/superset/pull/32710) chore(asf): Removing notifications from `.asf.yaml` - they still don't work :( (@rusackas)
|
||||
- [#32709](https://github.com/apache/superset/pull/32709) chore(asf): fixing(?) `.asf.yaml` (@rusackas)
|
||||
- [#32690](https://github.com/apache/superset/pull/32690) docs(api): correct attribute `name` instead of `table` for GET table_metadata in openapi.json (@hainenber)
|
||||
- [#32688](https://github.com/apache/superset/pull/32688) build(dev-deps): bump prettier to v3.5.3 and follow-up refactor (@hainenber)
|
||||
- [#32697](https://github.com/apache/superset/pull/32697) chore: add Oxylabs to INTHEWILD.md (@rytis-ulys)
|
||||
- [#32407](https://github.com/apache/superset/pull/32407) chore(docs): remove customized "Edit this page on GitHub" button (@hainenber)
|
||||
- [#32580](https://github.com/apache/superset/pull/32580) chore(deps): bump jinja2 from 3.1.5 to 3.1.6 in /superset/translations (@dependabot[bot])
|
||||
- [#32668](https://github.com/apache/superset/pull/32668) docs: add Hometogo to users list (@PedroMartinSteenstrup)
|
||||
- [#32623](https://github.com/apache/superset/pull/32623) chore(examples): Touching up Vehicle Sales a bit (@rusackas)
|
||||
- [#32485](https://github.com/apache/superset/pull/32485) chore: simplify user impersonation (@betodealmeida)
|
||||
- [#32641](https://github.com/apache/superset/pull/32641) chore: add unique option to index migration utils (@villebro)
|
||||
- [#32575](https://github.com/apache/superset/pull/32575) chore(🦾): bump python paramiko 3.5.0 -> 3.5.1 (@github-actions[bot])
|
||||
- [#32639](https://github.com/apache/superset/pull/32639) chore(🦾): bump python croniter 5.0.1 -> 6.0.0 (@github-actions[bot])
|
||||
- [#32637](https://github.com/apache/superset/pull/32637) chore(🦾): bump python flask-session subpackage(s) (@github-actions[bot])
|
||||
- [#32638](https://github.com/apache/superset/pull/32638) chore(🦾): bump python celery subpackage(s) (@github-actions[bot])
|
||||
- [#32636](https://github.com/apache/superset/pull/32636) chore(🦾): bump python importlib-metadata 8.5.0 -> 8.6.1 (@github-actions[bot])
|
||||
- [#32635](https://github.com/apache/superset/pull/32635) chore(🦾): bump python simplejson 3.19.3 -> 3.20.1 (@github-actions[bot])
|
||||
- [#32634](https://github.com/apache/superset/pull/32634) chore(🦾): bump python flask-caching 2.3.0 -> 2.3.1 (@github-actions[bot])
|
||||
- [#32629](https://github.com/apache/superset/pull/32629) chore(🦾): bump python sshtunnel subpackage(s) (@github-actions[bot])
|
||||
- [#32596](https://github.com/apache/superset/pull/32596) chore: fix precommit for eslint (@mistercrunch)
|
||||
- [#32596](https://github.com/apache/superset/pull/32596) chore: fix precommit for eslint (@mistercrunch)
|
||||
- [#32631](https://github.com/apache/superset/pull/32631) chore(🦾): bump python sqlparse 0.5.2 -> 0.5.3 (@github-actions[bot])
|
||||
- [#32628](https://github.com/apache/superset/pull/32628) chore(🦾): bump python greenlet 3.0.3 -> 3.1.1 (@github-actions[bot])
|
||||
- [#32632](https://github.com/apache/superset/pull/32632) chore(🦾): bump python humanize 4.11.0 -> 4.12.1 (@github-actions[bot])
|
||||
- [#32630](https://github.com/apache/superset/pull/32630) chore(🦾): bump python nh3 0.2.19 -> 0.2.21 (@github-actions[bot])
|
||||
- [#32578](https://github.com/apache/superset/pull/32578) chore(🦾): bump python flask-migrate subpackage(s) (@github-actions[bot])
|
||||
- [#32577](https://github.com/apache/superset/pull/32577) chore(🦾): bump python pyparsing 3.2.0 -> 3.2.1 (@github-actions[bot])
|
||||
- [#32581](https://github.com/apache/superset/pull/32581) chore(deps-dev): bump axios from 1.7.7 to 1.8.2 in /superset-embedded-sdk (@dependabot[bot])
|
||||
- [#32582](https://github.com/apache/superset/pull/32582) chore(deps): bump axios from 1.7.8 to 1.8.2 in /docs (@dependabot[bot])
|
||||
- [#32583](https://github.com/apache/superset/pull/32583) chore(deps-dev): bump axios from 1.7.9 to 1.8.2 in /superset-frontend (@dependabot[bot])
|
||||
- [#32603](https://github.com/apache/superset/pull/32603) chore(deps): bump @babel/runtime-corejs3 from 7.26.9 to 7.26.10 in /docs (@dependabot[bot])
|
||||
- [#32598](https://github.com/apache/superset/pull/32598) chore(deps): bump @babel/helpers from 7.24.5 to 7.26.10 in /docs (@dependabot[bot])
|
||||
- [#32604](https://github.com/apache/superset/pull/32604) chore(deps): bump @babel/runtime from 7.26.9 to 7.26.10 in /docs (@dependabot[bot])
|
||||
- [#32607](https://github.com/apache/superset/pull/32607) docs(analytics): actually USING Matomo to track page views/changes (@rusackas)
|
||||
- [#32605](https://github.com/apache/superset/pull/32605) docs: fix typo in ephemeral envs docs (@mistercrunch)
|
||||
- [#32600](https://github.com/apache/superset/pull/32600) docs: add information about ephemeral environments (@mistercrunch)
|
||||
- [#32597](https://github.com/apache/superset/pull/32597) chore: bump postgresql from 15 to 16 (@RealGreenDragon)
|
||||
- [#32602](https://github.com/apache/superset/pull/32602) chore(deps): bump @babel/helpers from 7.17.2 to 7.26.10 in /superset-frontend/cypress-base (@dependabot[bot])
|
||||
- [#32576](https://github.com/apache/superset/pull/32576) chore(🦾): bump python slack-sdk 3.33.4 -> 3.34.0 (@github-actions[bot])
|
||||
- [#32579](https://github.com/apache/superset/pull/32579) chore(🦾): bump python pandas subpackage(s) (@github-actions[bot])
|
||||
- [#32573](https://github.com/apache/superset/pull/32573) chore(🦾): bump python cryptography 43.0.3 -> 44.0.2 (@mistercrunch)
|
||||
- [#32561](https://github.com/apache/superset/pull/32561) chore(docs): Add Flowbird to users list (@EmmanuelCbd)
|
||||
- [#32545](https://github.com/apache/superset/pull/32545) refactor(input): Remove leftover direct usage of Ant Design input (@msyavuz)
|
||||
- [#32550](https://github.com/apache/superset/pull/32550) chore: bump node to v20.18.3 (@villebro)
|
||||
- [#32547](https://github.com/apache/superset/pull/32547) docs: add Canonical to INTHEWILD.md (@personofnorank)
|
||||
- [#32544](https://github.com/apache/superset/pull/32544) chore(Ant Design): Remove unnecessary exports from version 4 (@geido)
|
||||
- [#31770](https://github.com/apache/superset/pull/31770) chore: add logging to index error (@betodealmeida)
|
||||
- [#32529](https://github.com/apache/superset/pull/32529) chore: Caching the Slack channels list (@Vitor-Avila)
|
||||
- [#32527](https://github.com/apache/superset/pull/32527) chore(ci): use npm/yarn lock files where possible (@villebro)
|
||||
- [#32448](https://github.com/apache/superset/pull/32448) chore(deps-dev): bump eslint-config-prettier from 8.10.0 to 10.0.2 in /docs (@dependabot[bot])
|
||||
- [#32437](https://github.com/apache/superset/pull/32437) chore(deps-dev): bump globals from 15.9.0 to 16.0.0 in /superset-websocket (@dependabot[bot])
|
||||
- [#32456](https://github.com/apache/superset/pull/32456) chore(deps): bump markdown-to-jsx from 7.7.3 to 7.7.4 in /superset-frontend (@dependabot[bot])
|
||||
- [#32517](https://github.com/apache/superset/pull/32517) chore(ci): show more failed pre-commit context (@villebro)
|
||||
- [#32470](https://github.com/apache/superset/pull/32470) chore(deps-dev): update @babel/types requirement from ^7.26.3 to ^7.26.9 in /superset-frontend/plugins/plugin-chart-pivot-table (@dependabot[bot])
|
||||
- [#32503](https://github.com/apache/superset/pull/32503) chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.19.0 to 8.26.0 in /superset-websocket (@dependabot[bot])
|
||||
- [#32501](https://github.com/apache/superset/pull/32501) chore: enable dependabot using uv for auto-bumping python packages (@mistercrunch)
|
||||
- [#30657](https://github.com/apache/superset/pull/30657) chore: various markdown warnings resolved (@CodeWithEmad)
|
||||
- [#32453](https://github.com/apache/superset/pull/32453) chore(deps): bump @deck.gl/react from 9.1.0 to 9.1.4 in /superset-frontend (@dependabot[bot])
|
||||
- [#32460](https://github.com/apache/superset/pull/32460) chore(deps-dev): bump @babel/types from 7.26.7 to 7.26.9 in /superset-frontend (@dependabot[bot])
|
||||
- [#32461](https://github.com/apache/superset/pull/32461) chore(deps): bump @rjsf/utils from 5.24.1 to 5.24.3 in /superset-frontend (@dependabot[bot])
|
||||
- [#32462](https://github.com/apache/superset/pull/32462) chore(deps): bump chrono-node from 2.7.7 to 2.7.8 in /superset-frontend (@dependabot[bot])
|
||||
- [#32440](https://github.com/apache/superset/pull/32440) chore(deps-dev): bump @types/jsonwebtoken from 9.0.6 to 9.0.9 in /superset-websocket (@dependabot[bot])
|
||||
- [#32454](https://github.com/apache/superset/pull/32454) chore(deps): bump swagger-ui-react from 5.19.0 to 5.20.0 in /docs (@dependabot[bot])
|
||||
- [#32476](https://github.com/apache/superset/pull/32476) chore(deps-dev): bump @types/lodash from 4.17.14 to 4.17.16 in /superset-frontend (@dependabot[bot])
|
||||
- [#32447](https://github.com/apache/superset/pull/32447) chore(deps): bump antd from 5.24.1 to 5.24.2 in /docs (@dependabot[bot])
|
||||
- [#32449](https://github.com/apache/superset/pull/32449) chore(deps-dev): bump webpack from 5.97.1 to 5.98.0 in /docs (@dependabot[bot])
|
||||
- [#32452](https://github.com/apache/superset/pull/32452) chore(deps-dev): bump typescript from 5.1.6 to 5.8.2 in /docs (@dependabot[bot])
|
||||
- [#32087](https://github.com/apache/superset/pull/32087) chore(deps-dev): bump @docusaurus/tsconfig from 3.6.3 to 3.7.0 in /docs (@dependabot[bot])
|
||||
- [#32421](https://github.com/apache/superset/pull/32421) docs: add SingleStore to the users list (@tjain-singlestore)
|
||||
- [#32385](https://github.com/apache/superset/pull/32385) docs(config): fill in commonly connection string for Oracle, Presto and SQL Server databases (@hainenber)
|
||||
- [#32322](https://github.com/apache/superset/pull/32322) build(deps): bump major versions for `math-expression-evaluator` and `fetch-mock` + clean up obsolete dev/override packages (@hainenber)
|
||||
- [#32393](https://github.com/apache/superset/pull/32393) chore(docs): fix typos (@omahs)
|
||||
- [#32396](https://github.com/apache/superset/pull/32396) docs: add shipmnts to users list (@ekansh-shipmnts)
|
||||
- [#32380](https://github.com/apache/superset/pull/32380) chore(docs): update instructions for pypi distribution (@sadpandajoe)
|
||||
- [#32379](https://github.com/apache/superset/pull/32379) docs(intro): broaden link to installation options (@sfirke)
|
||||
- [#32334](https://github.com/apache/superset/pull/32334) chore: Upgrade AG Grid to use tree shaking (@kgabryje)
|
||||
- [#32365](https://github.com/apache/superset/pull/32365) chore(cleanup): removing accidentally committed package/lock files. (@rusackas)
|
||||
- [#32313](https://github.com/apache/superset/pull/32313) refactor(DrillDetailTableControls): Upgrade DrillDetailTableControls component to Ant Design 5 (@EnxDev)
|
||||
- [#32363](https://github.com/apache/superset/pull/32363) chore(tests): converting enzyme to RTL, part 3 (@rusackas)
|
||||
- [#32314](https://github.com/apache/superset/pull/32314) refactor(DatabaseSelector): Changes the imported types from antd-4 to antdv-5 (@EnxDev)
|
||||
- [#32349](https://github.com/apache/superset/pull/32349) chore(docs): Fix typo in security.mdx (@amineBouilzmin)
|
||||
- [#32323](https://github.com/apache/superset/pull/32323) ci(type-checking): run type-checking-frontend hook sequentially (@alveifbklsiu259)
|
||||
- [#32341](https://github.com/apache/superset/pull/32341) chore(build): reduce Lodash usage in `superset-frontend` (@hainenber)
|
||||
- [#32302](https://github.com/apache/superset/pull/32302) chore(duckdb): Bump duckdb-engine, duckdb versions (@guenp)
|
||||
- [#32330](https://github.com/apache/superset/pull/32330) chore(deps): bump swagger-ui-react from 5.18.2 to 5.19.0 in /docs (@dependabot[bot])
|
||||
- [#32329](https://github.com/apache/superset/pull/32329) chore(deps): bump antd from 5.22.7 to 5.24.1 in /docs (@dependabot[bot])
|
||||
- [#32327](https://github.com/apache/superset/pull/32327) chore(deps): bump @docsearch/react from 3.8.2 to 3.9.0 in /docs (@dependabot[bot])
|
||||
- [#32319](https://github.com/apache/superset/pull/32319) chore(readme): updating video on Readme page. (@rusackas)
|
||||
- [#32326](https://github.com/apache/superset/pull/32326) chore(docs): Add RIADVICE to companies using Superset (@GhaziTriki)
|
||||
- [#31921](https://github.com/apache/superset/pull/31921) docs: various enhancements across `/docs` workspace (@hainenber)
|
||||
- [#32066](https://github.com/apache/superset/pull/32066) chore(deps): bump core-js from 3.39.0 to 3.40.0 in /superset-frontend/packages/superset-ui-demo (@dependabot[bot])
|
||||
- [#32088](https://github.com/apache/superset/pull/32088) chore(deps-dev): bump @docusaurus/module-type-aliases from 3.6.3 to 3.7.0 in /docs (@dependabot[bot])
|
||||
- [#32316](https://github.com/apache/superset/pull/32316) chore(code owners): adding @mistercrunch to cypress/e2e code owners (@rusackas)
|
||||
- [#32226](https://github.com/apache/superset/pull/32226) chore(tests): Trying to kill enzyme, part 2 (more RTL!) (@rusackas)
|
||||
- [#32090](https://github.com/apache/superset/pull/32090) chore(deps-dev): bump typescript from 5.7.2 to 5.7.3 in /docs (@dependabot[bot])
|
||||
- [#32103](https://github.com/apache/superset/pull/32103) chore(deps-dev): bump @babel/preset-env from 7.26.0 to 7.26.7 in /superset-frontend (@dependabot[bot])
|
||||
- [#32259](https://github.com/apache/superset/pull/32259) chore(be/deps): add comments for un-greppable Python dependencies (@hainenber)
|
||||
- [#32270](https://github.com/apache/superset/pull/32270) chore(deps): bump dompurify from 3.2.3 to 3.2.4 in /superset-frontend (@dependabot[bot])
|
||||
- [#32243](https://github.com/apache/superset/pull/32243) build(fe/dev-deps): remove unused `esbuild` dev deps (@hainenber)
|
||||
- [#32236](https://github.com/apache/superset/pull/32236) chore(deps): bump cryptography from 43.0.3 to 44.0.1 (@dependabot[bot])
|
||||
- [#32142](https://github.com/apache/superset/pull/32142) docs(api): Improve api documentation for dashboard endpoints(filter_state, permalink, embedded) (@msyavuz)
|
||||
- [#32235](https://github.com/apache/superset/pull/32235) chore(backend): replace insecure `shortid` usage for native filter migration with native `uuid` Python implementation (@hainenber)
|
||||
- [#32207](https://github.com/apache/superset/pull/32207) chore: Working toward killing enzyme and cleaning up test noise. (@rusackas)
|
||||
- [#31634](https://github.com/apache/superset/pull/31634) chore(fe): migrate 4 Enzyme-based tests to RTL (@hainenber)
|
||||
- [#32180](https://github.com/apache/superset/pull/32180) docs: Permissions 'can this form get on UserInfoEditView' and 'can this form get on UserInfoEditView' are not associated with Aplha and Gamma by default (@xavier-GitHub76)
|
||||
- [#32192](https://github.com/apache/superset/pull/32192) chore(ci): consolidate Node version reference in CI to associated `.nvmrc` (@hainenber)
|
||||
- [#32010](https://github.com/apache/superset/pull/32010) chore: migrating easy-to-migrate AntD vanilla components (@mistercrunch)
|
||||
- [#32206](https://github.com/apache/superset/pull/32206) docs(docker-compose): remove extra backticks (@jonathanmv)
|
||||
- [#31973](https://github.com/apache/superset/pull/31973) refactor(Popover): Upgrade Popover to Antd5 (@alexandrusoare)
|
||||
- [#31972](https://github.com/apache/superset/pull/31972) refactor(Dropdown): Migrate Dropdown to Ant Design 5 (@msyavuz)
|
||||
- [#32188](https://github.com/apache/superset/pull/32188) docs(typo): PostgresQL corrected to PostgreSQL (@0xasritha)
|
||||
- [#32157](https://github.com/apache/superset/pull/32157) chore: add query context data tests (@eschutho)
|
||||
- [#32085](https://github.com/apache/superset/pull/32085) chore(deps): bump less from 4.2.1 to 4.2.2 in /docs (@dependabot[bot])
|
||||
- [#32171](https://github.com/apache/superset/pull/32171) docs: fix typo in docker compose (@ChrisChinchilla)
|
||||
- [#31999](https://github.com/apache/superset/pull/31999) docs: incorrect psycopg2 package in k8s install instructions (@bensku)
|
||||
4
LLMS.md
4
LLMS.md
@@ -9,7 +9,9 @@ Apache Superset is a data visualization platform with Flask/Python backend and R
|
||||
### Frontend Modernization
|
||||
- **NO `any` types** - Use proper TypeScript types
|
||||
- **NO JavaScript files** - Convert to TypeScript (.ts/.tsx)
|
||||
- **Use @superset-ui/core** - Don't import Ant Design directly
|
||||
- **Use @superset-ui/core** - Don't import Ant Design directly, prefer Ant Design component wrappers from @superset-ui/core/components
|
||||
- **Use antd theming tokens** - Prefer antd tokens over legacy theming tokens
|
||||
- **Avoid custom css and styles** - Follow antd best practices and avoid styling and custom CSS whenever possible
|
||||
|
||||
### Testing Strategy Migration
|
||||
- **Prefer unit tests** over integration tests
|
||||
|
||||
@@ -22,7 +22,10 @@ under the License.
|
||||
This file documents any backwards-incompatible changes in Superset and
|
||||
assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
## 6.0.0
|
||||
- [33055](https://github.com/apache/superset/pull/33055): Upgrades Flask-AppBuilder to 5.0.0. The AUTH_OID authentication type has been deprecated and is no longer available as an option in Flask-AppBuilder. OpenID (OID) is considered a deprecated authentication protocol - if you are using AUTH_OID, you will need to migrate to an alternative authentication method such as OAuth, LDAP, or database authentication before upgrading.
|
||||
- [34871](https://github.com/apache/superset/pull/34871): Fixed Jest test hanging issue from Ant Design v5 upgrade. MessageChannel is now mocked in test environment to prevent rc-overflow from causing Jest to hang. Test environment only - no production impact.
|
||||
- [34782](https://github.com/apache/superset/pull/34782): Dataset exports now include the dataset ID in their file name (similar to charts and dashboards). If managing assets as code, make sure to rename existing dataset YAMLs to include the ID (and avoid duplicated files).
|
||||
- [34536](https://github.com/apache/superset/pull/34536): The `ENVIRONMENT_TAG_CONFIG` color values have changed to support only Ant Design semantic colors. Update your `superset_config.py`:
|
||||
- Change `"error.base"` to just `"error"` after this PR
|
||||
- Change any hex color values to one of: `"success"`, `"processing"`, `"error"`, `"warning"`, `"default"`
|
||||
|
||||
@@ -26,7 +26,7 @@ gunicorn \
|
||||
--workers ${SERVER_WORKER_AMOUNT:-1} \
|
||||
--worker-class ${SERVER_WORKER_CLASS:-gthread} \
|
||||
--threads ${SERVER_THREADS_AMOUNT:-20} \
|
||||
--log-level "${GUNICORN_LOGLEVEL:info}" \
|
||||
--log-level "${GUNICORN_LOGLEVEL:-info}" \
|
||||
--timeout ${GUNICORN_TIMEOUT:-60} \
|
||||
--keep-alive ${GUNICORN_KEEPALIVE:-2} \
|
||||
--max-requests ${WORKER_MAX_REQUESTS:-0} \
|
||||
|
||||
@@ -363,110 +363,6 @@ CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
|
||||
]
|
||||
```
|
||||
|
||||
### Keycloak-Specific Configuration using Flask-OIDC
|
||||
|
||||
If you are using Keycloak as OpenID Connect 1.0 Provider, the above configuration based on [`Authlib`](https://authlib.org/) might not work. In this case using [`Flask-OIDC`](https://pypi.org/project/flask-oidc/) is a viable option.
|
||||
|
||||
Make sure the pip package [`Flask-OIDC`](https://pypi.org/project/flask-oidc/) is installed on the webserver. This was successfully tested using version 2.2.0. This package requires [`Flask-OpenID`](https://pypi.org/project/Flask-OpenID/) as a dependency.
|
||||
|
||||
The following code defines a new security manager. Add it to a new file named `keycloak_security_manager.py`, placed in the same directory as your `superset_config.py` file.
|
||||
|
||||
```python
|
||||
from flask_appbuilder.security.manager import AUTH_OID
|
||||
from superset.security import SupersetSecurityManager
|
||||
from flask_oidc import OpenIDConnect
|
||||
from flask_appbuilder.security.views import AuthOIDView
|
||||
from flask_login import login_user
|
||||
from urllib.parse import quote
|
||||
from flask_appbuilder.views import ModelView, SimpleFormView, expose
|
||||
from flask import (
|
||||
redirect,
|
||||
request
|
||||
)
|
||||
import logging
|
||||
|
||||
class OIDCSecurityManager(SupersetSecurityManager):
|
||||
|
||||
def __init__(self, appbuilder):
|
||||
super(OIDCSecurityManager, self).__init__(appbuilder)
|
||||
if self.auth_type == AUTH_OID:
|
||||
self.oid = OpenIDConnect(self.appbuilder.get_app)
|
||||
self.authoidview = AuthOIDCView
|
||||
|
||||
class AuthOIDCView(AuthOIDView):
|
||||
|
||||
@expose('/login/', methods=['GET', 'POST'])
|
||||
def login(self, flag=True):
|
||||
sm = self.appbuilder.sm
|
||||
oidc = sm.oid
|
||||
|
||||
@self.appbuilder.sm.oid.require_login
|
||||
def handle_login():
|
||||
user = sm.auth_user_oid(oidc.user_getfield('email'))
|
||||
|
||||
if user is None:
|
||||
info = oidc.user_getinfo(['preferred_username', 'given_name', 'family_name', 'email'])
|
||||
user = sm.add_user(info.get('preferred_username'), info.get('given_name'), info.get('family_name'),
|
||||
info.get('email'), sm.find_role('Gamma'))
|
||||
|
||||
login_user(user, remember=False)
|
||||
return redirect(self.appbuilder.get_url_for_index)
|
||||
|
||||
return handle_login()
|
||||
|
||||
@expose('/logout/', methods=['GET', 'POST'])
|
||||
def logout(self):
|
||||
oidc = self.appbuilder.sm.oid
|
||||
|
||||
oidc.logout()
|
||||
super(AuthOIDCView, self).logout()
|
||||
redirect_url = request.url_root.strip('/') + self.appbuilder.get_url_for_login
|
||||
|
||||
return redirect(
|
||||
oidc.client_secrets.get('issuer') + '/protocol/openid-connect/logout?redirect_uri=' + quote(redirect_url))
|
||||
```
|
||||
|
||||
Then add to your `superset_config.py` file:
|
||||
|
||||
```python
|
||||
from keycloak_security_manager import OIDCSecurityManager
|
||||
from flask_appbuilder.security.manager import AUTH_OID, AUTH_REMOTE_USER, AUTH_DB, AUTH_LDAP, AUTH_OAUTH
|
||||
import os
|
||||
|
||||
AUTH_TYPE = AUTH_OID
|
||||
SECRET_KEY: 'SomethingNotEntirelySecret'
|
||||
OIDC_CLIENT_SECRETS = '/path/to/client_secret.json'
|
||||
OIDC_ID_TOKEN_COOKIE_SECURE = False
|
||||
OIDC_OPENID_REALM: '<myRealm>'
|
||||
OIDC_INTROSPECTION_AUTH_METHOD: 'client_secret_post'
|
||||
CUSTOM_SECURITY_MANAGER = OIDCSecurityManager
|
||||
|
||||
# Will allow user self registration, allowing to create Flask users from Authorized User
|
||||
AUTH_USER_REGISTRATION = True
|
||||
|
||||
# The default user self registration role
|
||||
AUTH_USER_REGISTRATION_ROLE = 'Public'
|
||||
```
|
||||
|
||||
Store your client-specific OpenID information in a file called `client_secret.json`. Create this file in the same directory as `superset_config.py`:
|
||||
|
||||
```json
|
||||
{
|
||||
"<myOpenIDProvider>": {
|
||||
"issuer": "https://<myKeycloakDomain>/realms/<myRealm>",
|
||||
"auth_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/auth",
|
||||
"client_id": "https://<myKeycloakDomain>",
|
||||
"client_secret": "<myClientSecret>",
|
||||
"redirect_uris": [
|
||||
"https://<SupersetWebserver>/oauth-authorized/<myOpenIDProvider>"
|
||||
],
|
||||
"userinfo_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/userinfo",
|
||||
"token_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/token",
|
||||
"token_introspection_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/token/introspect"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## LDAP Authentication
|
||||
|
||||
FAB supports authenticating user credentials against an LDAP server.
|
||||
|
||||
@@ -747,6 +747,26 @@ To run a single test file:
|
||||
npm run test -- path/to/file.js
|
||||
```
|
||||
|
||||
#### Known Issues and Workarounds
|
||||
|
||||
**Jest Test Hanging (MessageChannel Issue)**
|
||||
|
||||
If Jest tests hang with "Jest did not exit one second after the test run has completed", this is likely due to the MessageChannel issue from rc-overflow (Ant Design v5 components).
|
||||
|
||||
**Root Cause**: `rc-overflow@1.4.1` creates MessageChannel handles for responsive overflow detection that remain open after test completion.
|
||||
|
||||
**Current Workaround**: MessageChannel is mocked as undefined in `spec/helpers/jsDomWithFetchAPI.ts`, forcing rc-overflow to use requestAnimationFrame fallback.
|
||||
|
||||
**To verify if still needed**: Remove the MessageChannel mocking lines and run `npm test -- --shard=4/8`. If tests hang, the workaround is still required.
|
||||
|
||||
**Future removal conditions**: This workaround can be removed when:
|
||||
- rc-overflow updates to properly clean up MessagePorts in test environments
|
||||
- Jest updates to handle MessageChannel/MessagePort cleanup better
|
||||
- Ant Design switches away from rc-overflow
|
||||
- We switch away from Ant Design v5
|
||||
|
||||
**See**: [PR #34871](https://github.com/apache/superset/pull/34871) for full technical details.
|
||||
|
||||
### Debugging Server App
|
||||
|
||||
#### Local
|
||||
|
||||
@@ -46,7 +46,7 @@ dependencies = [
|
||||
"cryptography>=42.0.4, <45.0.0",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
"flask>=2.2.5, <3.0.0",
|
||||
"flask-appbuilder>=4.8.0, <5.0.0",
|
||||
"flask-appbuilder>=5.0.0,<6",
|
||||
"flask-caching>=2.1.0, <3",
|
||||
"flask-compress>=1.13, <2.0",
|
||||
"flask-talisman>=1.0.0, <2.0",
|
||||
@@ -73,7 +73,7 @@ dependencies = [
|
||||
"packaging",
|
||||
# --------------------------
|
||||
# pandas and related (wanting pandas[performance] without numba as it's 100+MB and not needed)
|
||||
"pandas[excel]>=2.0.3, <2.1",
|
||||
"pandas[excel]>=2.0.3, <2.2",
|
||||
"bottleneck", # recommended performance dependency for pandas, see https://pandas.pydata.org/docs/getting_started/install.html#performance-dependencies-recommended
|
||||
# --------------------------
|
||||
"parsedatetime",
|
||||
@@ -85,7 +85,7 @@ dependencies = [
|
||||
"python-dateutil",
|
||||
"python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies
|
||||
"python-geohash",
|
||||
"pyarrow>=16.1.0, <17", # before upgrading pyarrow, check that all db dependencies support this, see e.g. https://github.com/apache/superset/pull/34693
|
||||
"pyarrow>=16.1.0, <19", # before upgrading pyarrow, check that all db dependencies support this, see e.g. https://github.com/apache/superset/pull/34693
|
||||
"pyyaml>=6.0.0, <7.0.0",
|
||||
"PyJWT>=2.4.0, <3.0",
|
||||
"redis>=4.6.0, <5.0",
|
||||
@@ -96,7 +96,7 @@ dependencies = [
|
||||
"slack_sdk>=3.19.0, <4",
|
||||
"sqlalchemy>=1.4, <2",
|
||||
"sqlalchemy-utils>=0.38.3, <0.39",
|
||||
"sqlglot>=27.3.0, <28",
|
||||
"sqlglot>=27.15.2, <28",
|
||||
# newer pandas needs 0.9+
|
||||
"tabulate>=0.9.0, <1.0",
|
||||
"typing-extensions>=4, <5",
|
||||
|
||||
@@ -112,9 +112,9 @@ flask==2.3.3
|
||||
# flask-session
|
||||
# flask-sqlalchemy
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.8.0
|
||||
flask-appbuilder==5.0.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-babel==2.0.0
|
||||
flask-babel==3.1.0
|
||||
# via flask-appbuilder
|
||||
flask-caching==2.3.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
@@ -154,6 +154,7 @@ greenlet==3.1.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# shillelagh
|
||||
# sqlalchemy
|
||||
gunicorn==23.0.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
h11==0.16.0
|
||||
@@ -256,7 +257,7 @@ packaging==25.0
|
||||
# limits
|
||||
# marshmallow
|
||||
# shillelagh
|
||||
pandas==2.0.3
|
||||
pandas==2.1.4
|
||||
# via apache-superset (pyproject.toml)
|
||||
paramiko==3.5.1
|
||||
# via
|
||||
@@ -380,7 +381,7 @@ sqlalchemy-utils==0.38.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
sqlglot==27.3.0
|
||||
sqlglot==27.15.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
sshtunnel==0.4.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
|
||||
@@ -195,11 +195,11 @@ flask==2.3.3
|
||||
# flask-sqlalchemy
|
||||
# flask-testing
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.8.0
|
||||
flask-appbuilder==5.0.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
flask-babel==2.0.0
|
||||
flask-babel==3.1.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-appbuilder
|
||||
@@ -313,6 +313,7 @@ greenlet==3.1.1
|
||||
# apache-superset
|
||||
# gevent
|
||||
# shillelagh
|
||||
# sqlalchemy
|
||||
grpcio==1.71.0
|
||||
# via
|
||||
# apache-superset
|
||||
@@ -510,7 +511,7 @@ packaging==25.0
|
||||
# pytest
|
||||
# shillelagh
|
||||
# sqlalchemy-bigquery
|
||||
pandas==2.0.3
|
||||
pandas==2.1.4
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
@@ -804,7 +805,7 @@ sqlalchemy-utils==0.38.3
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
sqlglot==27.3.0
|
||||
sqlglot==27.15.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
|
||||
@@ -1,766 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { Interception } from 'cypress/types/net-stubbing';
|
||||
import { waitForChartLoad } from 'cypress/utils';
|
||||
import { SUPPORTED_CHARTS_DASHBOARD } from 'cypress/utils/urls';
|
||||
import {
|
||||
openTopLevelTab,
|
||||
SUPPORTED_TIER1_CHARTS,
|
||||
SUPPORTED_TIER2_CHARTS,
|
||||
} from './utils';
|
||||
import {
|
||||
interceptExploreJson,
|
||||
interceptV1ChartData,
|
||||
interceptFormDataKey,
|
||||
} from '../explore/utils';
|
||||
|
||||
const interceptDrillInfo = () => {
|
||||
cy.intercept('GET', '**/api/v1/dataset/*/drill_info/*', {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
result: {
|
||||
id: 1,
|
||||
changed_on_humanized: '2 days ago',
|
||||
created_on_humanized: 'a week ago',
|
||||
table_name: 'birth_names',
|
||||
changed_by: {
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
},
|
||||
created_by: {
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
},
|
||||
owners: [
|
||||
{
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
},
|
||||
],
|
||||
columns: [
|
||||
{
|
||||
column_name: 'gender',
|
||||
verbose_name: null,
|
||||
},
|
||||
{
|
||||
column_name: 'state',
|
||||
verbose_name: null,
|
||||
},
|
||||
{
|
||||
column_name: 'name',
|
||||
verbose_name: null,
|
||||
},
|
||||
{
|
||||
column_name: 'ds',
|
||||
verbose_name: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}).as('drillInfo');
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
cy.get('body').then($body => {
|
||||
if ($body.find('[data-test="close-drill-by-modal"]').length) {
|
||||
cy.getBySel('close-drill-by-modal').click({ force: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const openTableContextMenu = (
|
||||
cellContent: string,
|
||||
tableSelector = "[data-test-viz-type='table']",
|
||||
) => {
|
||||
cy.get(tableSelector).scrollIntoView();
|
||||
cy.get(tableSelector).contains(cellContent).first().rightclick();
|
||||
};
|
||||
|
||||
const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
|
||||
if (isLegacy) {
|
||||
interceptExploreJson('legacyData');
|
||||
} else {
|
||||
interceptV1ChartData();
|
||||
}
|
||||
|
||||
cy.get('.ant-dropdown:not(.ant-dropdown-hidden)', { timeout: 15000 })
|
||||
.should('be.visible')
|
||||
.find("[role='menu'] [role='menuitem']")
|
||||
.contains(/^Drill by$/)
|
||||
.trigger('mouseover', { force: true });
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
{ timeout: 15000 },
|
||||
)
|
||||
.should('be.visible')
|
||||
.find('[role="menuitem"]')
|
||||
.contains(new RegExp(`^${targetDrillByColumn}$`))
|
||||
.click();
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
).trigger('mouseout', { clientX: 0, clientY: 0, force: true });
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
).should('not.exist');
|
||||
|
||||
if (isLegacy) {
|
||||
return cy.wait('@legacyData');
|
||||
}
|
||||
return cy.wait('@v1Data');
|
||||
};
|
||||
|
||||
const verifyExpectedFormData = (
|
||||
interceptedRequest: Interception,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expectedFormData: Record<string, any>,
|
||||
) => {
|
||||
const actualFormData = interceptedRequest.request.body?.form_data;
|
||||
Object.entries(expectedFormData).forEach(([key, val]) => {
|
||||
expect(actualFormData?.[key]).to.eql(val);
|
||||
});
|
||||
};
|
||||
|
||||
const testEchart = (
|
||||
vizType: string,
|
||||
chartName: string,
|
||||
drillClickCoordinates: [[number, number], [number, number]],
|
||||
furtherDrillDimension = 'name',
|
||||
) => {
|
||||
cy.get(`[data-test-viz-type='${vizType}'] canvas`).then($canvas => {
|
||||
// click 'boy'
|
||||
cy.wrap($canvas).scrollIntoView();
|
||||
cy.wrap($canvas).trigger(
|
||||
'mouseover',
|
||||
drillClickCoordinates[0][0],
|
||||
drillClickCoordinates[0][1],
|
||||
);
|
||||
cy.wrap($canvas).rightclick(
|
||||
drillClickCoordinates[0][0],
|
||||
drillClickCoordinates[0][1],
|
||||
);
|
||||
|
||||
drillBy('state').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupby: ['state'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.getBySel(`"Drill by: ${chartName}-modal"`).as('drillByModal');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.draggable-trigger')
|
||||
.should('contain', chartName);
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('contain', 'state');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible');
|
||||
|
||||
// further drill
|
||||
cy.get(`[data-test="drill-by-chart"] canvas`).then($canvas => {
|
||||
// click 'other'
|
||||
cy.wrap($canvas).scrollIntoView();
|
||||
cy.wrap($canvas).trigger(
|
||||
'mouseover',
|
||||
drillClickCoordinates[1][0],
|
||||
drillClickCoordinates[1][1],
|
||||
);
|
||||
cy.wrap($canvas).rightclick(
|
||||
drillClickCoordinates[1][0],
|
||||
drillClickCoordinates[1][1],
|
||||
);
|
||||
|
||||
drillBy(furtherDrillDimension).then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupby: [furtherDrillDimension],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'other',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'state',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible');
|
||||
|
||||
// undo - back to drill by state
|
||||
interceptV1ChartData('drillByUndo');
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('contain', 'state (other)')
|
||||
.and('contain', furtherDrillDimension)
|
||||
.contains('state (other)')
|
||||
.click();
|
||||
cy.wait('@drillByUndo').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupby: ['state'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('not.contain', 'state (other)')
|
||||
.and('not.contain', furtherDrillDimension)
|
||||
.and('contain', 'state');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe('Drill by modal', () => {
|
||||
beforeEach(() => {
|
||||
closeModal();
|
||||
});
|
||||
before(() => {
|
||||
interceptDrillInfo();
|
||||
cy.visit(SUPPORTED_CHARTS_DASHBOARD);
|
||||
});
|
||||
|
||||
describe('Modal actions + Table', () => {
|
||||
before(() => {
|
||||
closeModal();
|
||||
interceptDrillInfo();
|
||||
openTopLevelTab('Tier 1');
|
||||
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
|
||||
it.only('opens the modal from the context menu', () => {
|
||||
openTableContextMenu('boy');
|
||||
drillBy('state').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupby: ['state'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.getBySel('"Drill by: Table-modal"').as('drillByModal');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.draggable-trigger')
|
||||
.should('contain', 'Drill by: Table');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="metadata-bar"]')
|
||||
.should('be.visible');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('contain', 'state');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible')
|
||||
.and('contain', 'state')
|
||||
.and('contain', 'sum__num');
|
||||
|
||||
// further drilling
|
||||
openTableContextMenu('CA', '[data-test="drill-by-chart"]');
|
||||
drillBy('name').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupby: ['name'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'CA',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'state',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible')
|
||||
.and('not.contain', 'state')
|
||||
.and('contain', 'name')
|
||||
.and('contain', 'sum__num');
|
||||
|
||||
// undo - back to drill by state
|
||||
interceptV1ChartData('drillByUndo');
|
||||
interceptFormDataKey();
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('contain', 'state (CA)')
|
||||
.and('contain', 'name')
|
||||
.contains('state (CA)')
|
||||
.click();
|
||||
cy.wait('@drillByUndo').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupby: ['state'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible')
|
||||
.and('not.contain', 'name')
|
||||
.and('contain', 'state')
|
||||
.and('contain', 'sum__num');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('not.contain', 'state (CA)')
|
||||
.and('not.contain', 'name')
|
||||
.and('contain', 'state');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-display-toggle"]')
|
||||
.contains('Table')
|
||||
.click();
|
||||
|
||||
cy.getBySel('drill-by-chart').should('not.exist');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-results-table"]')
|
||||
.should('be.visible');
|
||||
|
||||
cy.wait('@formDataKey').then(intercept => {
|
||||
cy.get('@drillByModal')
|
||||
.contains('Edit chart')
|
||||
.should('have.attr', 'href')
|
||||
.and(
|
||||
'contain',
|
||||
`/explore/?form_data_key=${intercept.response?.body?.key}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tier 1 charts', () => {
|
||||
before(() => {
|
||||
closeModal();
|
||||
interceptDrillInfo();
|
||||
openTopLevelTab('Tier 1');
|
||||
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
|
||||
it('Pivot Table', () => {
|
||||
openTableContextMenu('boy', "[data-test-viz-type='pivot_table_v2']");
|
||||
drillBy('name').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupbyRows: ['state'],
|
||||
groupbyColumns: ['name'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.getBySel('"Drill by: Pivot Table-modal"').as('drillByModal');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.draggable-trigger')
|
||||
.should('contain', 'Drill by: Pivot Table');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('contain', 'name');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible')
|
||||
.and('contain', 'state')
|
||||
.and('contain', 'name')
|
||||
.and('contain', 'sum__num')
|
||||
.and('not.contain', 'Gender');
|
||||
|
||||
openTableContextMenu('CA', '[data-test="drill-by-chart"]');
|
||||
drillBy('ds').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupbyColumns: ['name'],
|
||||
groupbyRows: ['ds'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'CA',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'state',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible')
|
||||
.and('contain', 'name')
|
||||
.and('contain', 'ds')
|
||||
.and('contain', 'sum__num')
|
||||
.and('not.contain', 'state');
|
||||
|
||||
interceptV1ChartData('drillByUndo');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('contain', 'name (CA)')
|
||||
.and('contain', 'ds')
|
||||
.contains('name (CA)')
|
||||
.click();
|
||||
cy.wait('@drillByUndo').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupbyRows: ['state'],
|
||||
groupbyColumns: ['name'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible')
|
||||
.and('not.contain', 'ds')
|
||||
.and('contain', 'state')
|
||||
.and('contain', 'name')
|
||||
.and('contain', 'sum__num');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('not.contain', 'name (CA)')
|
||||
.and('not.contain', 'ds')
|
||||
.and('contain', 'name');
|
||||
});
|
||||
|
||||
it('Line chart', () => {
|
||||
testEchart('echarts_timeseries_line', 'Line Chart', [
|
||||
[85, 93],
|
||||
[85, 93],
|
||||
]);
|
||||
});
|
||||
|
||||
it('Area Chart', () => {
|
||||
testEchart('echarts_area', 'Area Chart', [
|
||||
[85, 93],
|
||||
[85, 93],
|
||||
]);
|
||||
});
|
||||
|
||||
it('Scatter Chart', () => {
|
||||
testEchart('echarts_timeseries_scatter', 'Scatter Chart', [
|
||||
[85, 93],
|
||||
[85, 93],
|
||||
]);
|
||||
});
|
||||
|
||||
it.skip('Bar Chart', () => {
|
||||
testEchart('echarts_timeseries_bar', 'Bar Chart', [
|
||||
[85, 94],
|
||||
[490, 68],
|
||||
]);
|
||||
});
|
||||
|
||||
it('Pie Chart', () => {
|
||||
testEchart('pie', 'Pie Chart', [
|
||||
[243, 167],
|
||||
[534, 248],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tier 2 charts', () => {
|
||||
before(() => {
|
||||
closeModal();
|
||||
interceptDrillInfo();
|
||||
openTopLevelTab('Tier 2');
|
||||
SUPPORTED_TIER2_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
|
||||
it('Box Plot Chart', () => {
|
||||
testEchart(
|
||||
'box_plot',
|
||||
'Box Plot Chart',
|
||||
[
|
||||
[139, 277],
|
||||
[787, 441],
|
||||
],
|
||||
'ds',
|
||||
);
|
||||
});
|
||||
|
||||
it('Generic Chart', () => {
|
||||
testEchart('echarts_timeseries', 'Generic Chart', [
|
||||
[85, 93],
|
||||
[85, 93],
|
||||
]);
|
||||
});
|
||||
|
||||
it('Smooth Line Chart', () => {
|
||||
testEchart('echarts_timeseries_smooth', 'Smooth Line Chart', [
|
||||
[85, 93],
|
||||
[85, 93],
|
||||
]);
|
||||
});
|
||||
|
||||
it('Step Line Chart', () => {
|
||||
testEchart('echarts_timeseries_step', 'Step Line Chart', [
|
||||
[85, 93],
|
||||
[85, 93],
|
||||
]);
|
||||
});
|
||||
|
||||
it('Funnel Chart', () => {
|
||||
testEchart('funnel', 'Funnel Chart', [
|
||||
[154, 80],
|
||||
[421, 39],
|
||||
]);
|
||||
});
|
||||
|
||||
it('Gauge Chart', () => {
|
||||
testEchart('gauge_chart', 'Gauge Chart', [
|
||||
[151, 95],
|
||||
[300, 143],
|
||||
]);
|
||||
});
|
||||
|
||||
it.skip('Radar Chart', () => {
|
||||
testEchart('radar', 'Radar Chart', [
|
||||
[182, 49],
|
||||
[423, 91],
|
||||
]);
|
||||
});
|
||||
|
||||
it('Treemap V2 Chart', () => {
|
||||
testEchart('treemap_v2', 'Treemap V2 Chart', [
|
||||
[145, 84],
|
||||
[220, 105],
|
||||
]);
|
||||
});
|
||||
|
||||
it.skip('Mixed Chart', () => {
|
||||
cy.get('[data-test-viz-type="mixed_timeseries"] canvas').then($canvas => {
|
||||
// click 'boy'
|
||||
cy.wrap($canvas).scrollIntoView();
|
||||
cy.wrap($canvas).trigger('mouseover', 85, 93);
|
||||
cy.wrap($canvas).rightclick(85, 93);
|
||||
|
||||
drillBy('name').then(intercepted => {
|
||||
const { queries } = intercepted.request.body;
|
||||
expect(queries[0].columns).to.eql(['name']);
|
||||
expect(queries[0].filters).to.eql([
|
||||
{ col: 'gender', op: '==', val: 'boy' },
|
||||
]);
|
||||
expect(queries[1].columns).to.eql(['state']);
|
||||
expect(queries[1].filters).to.eql([]);
|
||||
});
|
||||
|
||||
cy.getBySel('"Drill by: Mixed Chart-modal"').as('drillByModal');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.draggable-trigger')
|
||||
.should('contain', 'Mixed Chart');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('contain', 'name');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible');
|
||||
|
||||
// further drill
|
||||
cy.get(`[data-test="drill-by-chart"] canvas`).then($canvas => {
|
||||
// click second query
|
||||
cy.wrap($canvas).scrollIntoView();
|
||||
cy.wrap($canvas).trigger('mouseover', 261, 114);
|
||||
cy.wrap($canvas).rightclick(261, 114);
|
||||
|
||||
drillBy('ds').then(intercepted => {
|
||||
const { queries } = intercepted.request.body;
|
||||
expect(queries[0].columns).to.eql(['name']);
|
||||
expect(queries[0].filters).to.eql([
|
||||
{ col: 'gender', op: '==', val: 'boy' },
|
||||
]);
|
||||
expect(queries[1].columns).to.eql(['ds']);
|
||||
expect(queries[1].filters).to.eql([
|
||||
{ col: 'state', op: '==', val: 'other' },
|
||||
]);
|
||||
});
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible');
|
||||
|
||||
// undo - back to drill by state
|
||||
interceptV1ChartData('drillByUndo');
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('contain', 'name (other)')
|
||||
.and('contain', 'ds')
|
||||
.contains('name (other)')
|
||||
.click();
|
||||
|
||||
cy.wait('@drillByUndo').then(intercepted => {
|
||||
const { queries } = intercepted.request.body;
|
||||
expect(queries[0].columns).to.eql(['name']);
|
||||
expect(queries[0].filters).to.eql([
|
||||
{ col: 'gender', op: '==', val: 'boy' },
|
||||
]);
|
||||
expect(queries[1].columns).to.eql(['state']);
|
||||
expect(queries[1].filters).to.eql([]);
|
||||
});
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('not.contain', 'name (other)')
|
||||
.and('not.contain', 'ds')
|
||||
.and('contain', 'name');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -179,13 +179,13 @@ describe.skip('Drill to detail modal', () => {
|
||||
cy.on('uncaught:exception', () => false);
|
||||
cy.wait('@samples');
|
||||
cy.get('.virtual-table-cell').should($rows => {
|
||||
expect($rows).to.contain('Kelly');
|
||||
expect($rows).to.contain('Kimberly');
|
||||
});
|
||||
|
||||
// verify scroll top on pagination
|
||||
cy.getBySelLike('Number-modal').find('.virtual-grid').scrollTo(0, 200);
|
||||
|
||||
cy.get('.virtual-grid').contains('Juan').should('not.be.visible');
|
||||
cy.get('.virtual-grid').contains('Kim').should('not.be.visible');
|
||||
|
||||
cy.get('.ant-pagination-item').eq(0).click();
|
||||
|
||||
|
||||
32
superset-frontend/package-lock.json
generated
32
superset-frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "superset",
|
||||
"version": "0.0.0-dev",
|
||||
"version": "6.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "superset",
|
||||
"version": "0.0.0-dev",
|
||||
"version": "6.0.0",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
@@ -62,7 +62,6 @@
|
||||
"dom-to-image-more": "^3.6.0",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"echarts": "^5.6.0",
|
||||
"emotion-rgba": "0.0.12",
|
||||
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
@@ -187,7 +186,6 @@
|
||||
"@types/react-resizable": "^3.0.8",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@types/react-ultimate-pagination": "^1.2.4",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.4",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/redux-localstorage": "^1.0.8",
|
||||
@@ -265,7 +263,7 @@
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.1",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.1",
|
||||
"webpack-dev-server": "^5.2.2",
|
||||
"webpack-manifest-plugin": "^5.0.1",
|
||||
"webpack-sources": "^3.3.3",
|
||||
"webpack-visualizer-plugin2": "^1.2.0"
|
||||
@@ -16104,16 +16102,6 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-ultimate-pagination": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-ultimate-pagination/-/react-ultimate-pagination-1.2.4.tgz",
|
||||
"integrity": "sha512-1y9jLt3KEFGzFD+99qVpJUI/Eu4cEx48sClB957eGoepWRLVVi+r1UBj0157Mg7HYZcIF4I1/qGZYaBBQWhaqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-virtualized-auto-sizer": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.4.tgz",
|
||||
@@ -24501,12 +24489,6 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/emotion-rgba": {
|
||||
"version": "0.0.12",
|
||||
"resolved": "https://registry.npmjs.org/emotion-rgba/-/emotion-rgba-0.0.12.tgz",
|
||||
"integrity": "sha512-lvtZ52BWisYDtis+HctQMkxcHwmFbzTiZhgMJGFfWXLsBYEzthfKE7nlysOiUwmmAdTM/8YBAPfwQ4MEDwiaWw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodable": {
|
||||
"version": "0.7.8",
|
||||
"resolved": "https://registry.npmjs.org/encodable/-/encodable-0.7.8.tgz",
|
||||
@@ -57102,9 +57084,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.1.tgz",
|
||||
"integrity": "sha512-ml/0HIj9NLpVKOMq+SuBPLHcmbG+TGIjXRHsYfZwocUBIqEvws8NnS/V9AFQ5FKP+tgn5adwVwRrTEpGL33QFQ==",
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz",
|
||||
"integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -57124,7 +57106,7 @@
|
||||
"connect-history-api-fallback": "^2.0.0",
|
||||
"express": "^4.21.2",
|
||||
"graceful-fs": "^4.2.6",
|
||||
"http-proxy-middleware": "^2.0.7",
|
||||
"http-proxy-middleware": "^2.0.9",
|
||||
"ipaddr.js": "^2.1.0",
|
||||
"launch-editor": "^2.6.1",
|
||||
"open": "^10.0.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superset",
|
||||
"version": "0.0.0-dev",
|
||||
"version": "6.0.0",
|
||||
"description": "Superset is a data exploration platform designed to be visual, intuitive, and interactive.",
|
||||
"keywords": [
|
||||
"big",
|
||||
@@ -130,7 +130,6 @@
|
||||
"dom-to-image-more": "^3.6.0",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"echarts": "^5.6.0",
|
||||
"emotion-rgba": "0.0.12",
|
||||
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
@@ -255,7 +254,6 @@
|
||||
"@types/react-resizable": "^3.0.8",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@types/react-ultimate-pagination": "^1.2.4",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.4",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/redux-localstorage": "^1.0.8",
|
||||
@@ -333,7 +331,7 @@
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.1",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.1",
|
||||
"webpack-dev-server": "^5.2.2",
|
||||
"webpack-manifest-plugin": "^5.0.1",
|
||||
"webpack-sources": "^3.3.3",
|
||||
"webpack-visualizer-plugin2": "^1.2.0"
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Popover, type PopoverProps } from '@superset-ui/core/components';
|
||||
import type ReactAce from 'react-ace';
|
||||
import {
|
||||
Popover,
|
||||
type PopoverProps,
|
||||
SQLEditor,
|
||||
} from '@superset-ui/core/components';
|
||||
import { CalculatorOutlined } from '@ant-design/icons';
|
||||
import { css, styled, useTheme, t } from '@superset-ui/core';
|
||||
|
||||
@@ -35,24 +37,10 @@ const StyledCalculatorIcon = styled(CalculatorOutlined)`
|
||||
|
||||
export const SQLPopover = (props: PopoverProps & { sqlExpression: string }) => {
|
||||
const theme = useTheme();
|
||||
const [AceEditor, setAceEditor] = useState<typeof ReactAce | null>(null);
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
import('react-ace'),
|
||||
import('ace-builds/src-min-noconflict/mode-sql'),
|
||||
]).then(([reactAceModule]) => {
|
||||
setAceEditor(() => reactAceModule.default);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!AceEditor) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
<AceEditor
|
||||
mode="sql"
|
||||
<SQLEditor
|
||||
value={props.sqlExpression}
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
setOptions={{
|
||||
@@ -65,7 +53,6 @@ export const SQLPopover = (props: PopoverProps & { sqlExpression: string }) => {
|
||||
wrapEnabled
|
||||
style={{
|
||||
border: `1px solid ${theme.colorBorder}`,
|
||||
background: theme.colorPrimaryBg,
|
||||
maxWidth: theme.sizeUnit * 100,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -43,15 +43,17 @@ export const renameOperator: PostProcessingFactory<PostProcessingRename> = (
|
||||
|
||||
// remove or rename top level of column name(metric name) in the MultiIndex when
|
||||
// 1) at least 1 metric
|
||||
// 2) dimension exist or multiple time shift metrics exist
|
||||
// 3) xAxis exist
|
||||
// 4) truncate_metric in form_data and truncate_metric is true
|
||||
// 2) xAxis exist
|
||||
// 3a) isTimeComparisonValue
|
||||
// 3b-1) dimension exist or multiple time shift metrics exist
|
||||
// 3b-2) truncate_metric in form_data and truncate_metric is true
|
||||
if (
|
||||
metrics.length > 0 &&
|
||||
(columns.length > 0 || timeOffsets.length > 1) &&
|
||||
xAxisLabel &&
|
||||
truncate_metric !== undefined &&
|
||||
!!truncate_metric
|
||||
(isTimeComparisonValue ||
|
||||
((columns.length > 0 || timeOffsets.length > 1) &&
|
||||
truncate_metric !== undefined &&
|
||||
!!truncate_metric))
|
||||
) {
|
||||
const renamePairs: [string, string | null][] = [];
|
||||
if (
|
||||
|
||||
@@ -160,6 +160,44 @@ test('should add renameOperator if exists derived metrics', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should add renameOperator if isTimeComparisonValue without columns', () => {
|
||||
[
|
||||
ComparisonType.Difference,
|
||||
ComparisonType.Ratio,
|
||||
ComparisonType.Percentage,
|
||||
].forEach(type => {
|
||||
expect(
|
||||
renameOperator(
|
||||
{
|
||||
...formData,
|
||||
...{
|
||||
comparison_type: type,
|
||||
time_compare: ['1 year ago'],
|
||||
},
|
||||
},
|
||||
{
|
||||
...queryObject,
|
||||
...{
|
||||
columns: [],
|
||||
metrics: ['sum(val)', 'avg(val2)'],
|
||||
},
|
||||
},
|
||||
),
|
||||
).toEqual({
|
||||
operation: 'rename',
|
||||
options: {
|
||||
columns: {
|
||||
[`${type}__avg(val2)__avg(val2)__1 year ago`]:
|
||||
'avg(val2), 1 year ago',
|
||||
[`${type}__sum(val)__sum(val)__1 year ago`]: 'sum(val), 1 year ago',
|
||||
},
|
||||
inplace: true,
|
||||
level: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should add renameOperator if x_axis does not exist', () => {
|
||||
expect(
|
||||
renameOperator(
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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, waitFor } from '@testing-library/react';
|
||||
|
||||
import {
|
||||
ChartPlugin,
|
||||
ChartMetadata,
|
||||
DatasourceType,
|
||||
getChartComponentRegistry,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
import SuperChartCore from './SuperChartCore';
|
||||
|
||||
const props = {
|
||||
chartType: 'line',
|
||||
};
|
||||
const FakeChart = () => <span>test</span>;
|
||||
|
||||
beforeEach(() => {
|
||||
const metadata = new ChartMetadata({
|
||||
name: 'test-chart',
|
||||
thumbnail: '',
|
||||
});
|
||||
const buildQuery = () => ({
|
||||
datasource: { id: 1, type: DatasourceType.Table },
|
||||
queries: [{ granularity: 'day' }],
|
||||
force: false,
|
||||
result_format: 'json',
|
||||
result_type: 'full',
|
||||
});
|
||||
const controlPanel = { abc: 1 };
|
||||
const plugin = new ChartPlugin({
|
||||
metadata,
|
||||
Chart: FakeChart,
|
||||
buildQuery,
|
||||
controlPanel,
|
||||
});
|
||||
plugin.configure({ key: props.chartType }).register();
|
||||
});
|
||||
|
||||
test('should return the result from cache unless transformProps has changed', async () => {
|
||||
const pre = jest.fn(x => x);
|
||||
const transform = jest.fn(x => x);
|
||||
const post = jest.fn(x => x);
|
||||
expect(getChartComponentRegistry().get(props.chartType)).toBe(FakeChart);
|
||||
|
||||
expect(pre).toHaveBeenCalledTimes(0);
|
||||
const { rerender } = render(
|
||||
<SuperChartCore
|
||||
{...props}
|
||||
preTransformProps={pre}
|
||||
overrideTransformProps={transform}
|
||||
postTransformProps={post}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(pre).toHaveBeenCalledTimes(1));
|
||||
expect(transform).toHaveBeenCalledTimes(1);
|
||||
expect(post).toHaveBeenCalledTimes(1);
|
||||
|
||||
const updatedPost = jest.fn(x => x);
|
||||
|
||||
rerender(
|
||||
<SuperChartCore
|
||||
{...props}
|
||||
preTransformProps={pre}
|
||||
overrideTransformProps={transform}
|
||||
postTransformProps={updatedPost}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => expect(updatedPost).toHaveBeenCalledTimes(1));
|
||||
expect(transform).toHaveBeenCalledTimes(1);
|
||||
expect(pre).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -85,31 +85,74 @@ export default class SuperChartCore extends PureComponent<Props, {}> {
|
||||
container?: HTMLElement | null;
|
||||
|
||||
/**
|
||||
* memoized function so it will not recompute
|
||||
* and return previous value
|
||||
* memoized function so it will not recompute and return previous value
|
||||
* unless one of
|
||||
* - preTransformProps
|
||||
* - transformProps
|
||||
* - postTransformProps
|
||||
* - chartProps
|
||||
* is changed.
|
||||
*/
|
||||
processChartProps = createSelector(
|
||||
preSelector = createSelector(
|
||||
[
|
||||
(input: {
|
||||
chartProps: ChartProps;
|
||||
preTransformProps?: PreTransformProps;
|
||||
transformProps?: TransformProps;
|
||||
postTransformProps?: PostTransformProps;
|
||||
}) => input.chartProps,
|
||||
input => input.preTransformProps,
|
||||
],
|
||||
(chartProps, pre = IDENTITY) => pre(chartProps),
|
||||
);
|
||||
|
||||
/**
|
||||
* memoized function so it will not recompute and return previous value
|
||||
* unless one of the input arguments have changed.
|
||||
*/
|
||||
transformSelector = createSelector(
|
||||
[
|
||||
(input: { chartProps: ChartProps; transformProps?: TransformProps }) =>
|
||||
input.chartProps,
|
||||
input => input.transformProps,
|
||||
],
|
||||
(preprocessedChartProps, transform = IDENTITY) =>
|
||||
transform(preprocessedChartProps),
|
||||
);
|
||||
|
||||
/**
|
||||
* memoized function so it will not recompute and return previous value
|
||||
* unless one of the input arguments have changed.
|
||||
*/
|
||||
postSelector = createSelector(
|
||||
[
|
||||
(input: {
|
||||
chartProps: ChartProps;
|
||||
postTransformProps?: PostTransformProps;
|
||||
}) => input.chartProps,
|
||||
input => input.postTransformProps,
|
||||
],
|
||||
(chartProps, pre = IDENTITY, transform = IDENTITY, post = IDENTITY) =>
|
||||
post(transform(pre(chartProps))),
|
||||
(transformedChartProps, post = IDENTITY) => post(transformedChartProps),
|
||||
);
|
||||
|
||||
/**
|
||||
* Using each memoized function to retrieve the computed chartProps
|
||||
*/
|
||||
processChartProps = ({
|
||||
chartProps,
|
||||
preTransformProps,
|
||||
transformProps,
|
||||
postTransformProps,
|
||||
}: {
|
||||
chartProps: ChartProps;
|
||||
preTransformProps?: PreTransformProps;
|
||||
transformProps?: TransformProps;
|
||||
postTransformProps?: PostTransformProps;
|
||||
}) =>
|
||||
this.postSelector({
|
||||
chartProps: this.transformSelector({
|
||||
chartProps: this.preSelector({ chartProps, preTransformProps }),
|
||||
transformProps,
|
||||
}),
|
||||
postTransformProps,
|
||||
});
|
||||
|
||||
/**
|
||||
* memoized function so it will not recompute
|
||||
* and return previous value
|
||||
|
||||
@@ -393,10 +393,7 @@ export const FullSQLEditor = AsyncAceEditor(
|
||||
},
|
||||
);
|
||||
|
||||
export const MarkdownEditor = AsyncAceEditor([
|
||||
'mode/markdown',
|
||||
'theme/textmate',
|
||||
]);
|
||||
export const MarkdownEditor = AsyncAceEditor(['mode/markdown', 'theme/github']);
|
||||
|
||||
export const TextAreaEditor = AsyncAceEditor([
|
||||
'mode/markdown',
|
||||
|
||||
@@ -48,10 +48,7 @@ export const CollapseLabelInModal: React.FC<CollapseLabelInModalProps> = ({
|
||||
{title}{' '}
|
||||
{validateCheckStatus !== undefined &&
|
||||
(validateCheckStatus ? (
|
||||
<Icons.CheckCircleOutlined
|
||||
iconColor={theme.colorSuccess}
|
||||
aria-label="check-circle"
|
||||
/>
|
||||
<Icons.CheckCircleOutlined iconColor={theme.colorSuccess} />
|
||||
) : (
|
||||
<span
|
||||
css={css`
|
||||
|
||||
@@ -1,66 +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 { fireEvent, render, waitFor } from '@superset-ui/core/spec';
|
||||
import { Button } from '../Button';
|
||||
import { ConfirmStatusChange } from '.';
|
||||
|
||||
const mockedProps = {
|
||||
title: 'please confirm',
|
||||
description: 'are you sure?',
|
||||
onConfirm: jest.fn(),
|
||||
};
|
||||
|
||||
test('opens a confirm modal', () => {
|
||||
const { getByTestId } = render(
|
||||
<ConfirmStatusChange {...mockedProps}>
|
||||
{confirm => (
|
||||
<>
|
||||
<Button data-test="btn1" onClick={confirm} />
|
||||
</>
|
||||
)}
|
||||
</ConfirmStatusChange>,
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('btn1'));
|
||||
|
||||
expect(getByTestId(`${mockedProps.title}-modal`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls the function on confirm', async () => {
|
||||
const { getByTestId, getByRole } = render(
|
||||
<ConfirmStatusChange {...mockedProps}>
|
||||
{confirm => (
|
||||
<>
|
||||
<Button data-test="btn1" onClick={() => confirm('foo')} />
|
||||
</>
|
||||
)}
|
||||
</ConfirmStatusChange>,
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('btn1'));
|
||||
|
||||
const confirmInput = getByTestId('delete-modal-input');
|
||||
fireEvent.change(confirmInput, { target: { value: 'DELETE' } });
|
||||
|
||||
const confirmButton = getByRole('button', { name: 'Delete' });
|
||||
fireEvent.click(confirmButton);
|
||||
|
||||
await waitFor(() => expect(mockedProps.onConfirm).toHaveBeenCalledTimes(1));
|
||||
expect(mockedProps.onConfirm).toHaveBeenCalledWith('foo');
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* 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 { fireEvent, render, waitFor } from '@superset-ui/core/spec';
|
||||
import { Button } from '../Button';
|
||||
import { ConfirmStatusChange } from '.';
|
||||
import type { ConfirmStatusChangeProps } from './types';
|
||||
|
||||
const mockedProps: Omit<ConfirmStatusChangeProps, 'children'> = {
|
||||
title: 'please confirm',
|
||||
description: 'are you sure?',
|
||||
onConfirm: jest.fn(),
|
||||
};
|
||||
|
||||
test('renders children with showConfirm function', () => {
|
||||
const childrenSpy = jest.fn().mockReturnValue(<div>test content</div>);
|
||||
|
||||
render(
|
||||
<ConfirmStatusChange {...mockedProps}>{childrenSpy}</ConfirmStatusChange>,
|
||||
);
|
||||
|
||||
expect(childrenSpy).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
|
||||
test('opens modal when showConfirm is called', () => {
|
||||
const { getByTestId } = render(
|
||||
<ConfirmStatusChange {...mockedProps}>
|
||||
{confirm => <Button data-test="trigger" onClick={confirm} />}
|
||||
</ConfirmStatusChange>,
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('trigger'));
|
||||
|
||||
expect(getByTestId(`${mockedProps.title}-modal`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('stores and passes arguments to onConfirm callback', async () => {
|
||||
const testArgs = ['arg1', { data: 'test' }, 42];
|
||||
const { getByTestId, getByRole } = render(
|
||||
<ConfirmStatusChange {...mockedProps}>
|
||||
{confirm => (
|
||||
<Button data-test="trigger" onClick={() => confirm(...testArgs)} />
|
||||
)}
|
||||
</ConfirmStatusChange>,
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('trigger'));
|
||||
|
||||
const confirmInput = getByTestId('delete-modal-input');
|
||||
fireEvent.change(confirmInput, { target: { value: 'DELETE' } });
|
||||
|
||||
const confirmButton = getByRole('button', { name: 'Delete' });
|
||||
fireEvent.click(confirmButton);
|
||||
|
||||
await waitFor(() => expect(mockedProps.onConfirm).toHaveBeenCalledTimes(1));
|
||||
expect(mockedProps.onConfirm).toHaveBeenCalledWith(...testArgs);
|
||||
});
|
||||
|
||||
test('calls preventDefault on event-like arguments', () => {
|
||||
const mockEvent = {
|
||||
preventDefault: jest.fn(),
|
||||
stopPropagation: jest.fn(),
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<ConfirmStatusChange {...mockedProps}>
|
||||
{confirm => (
|
||||
<Button data-test="trigger" onClick={() => confirm(mockEvent)} />
|
||||
)}
|
||||
</ConfirmStatusChange>,
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('trigger'));
|
||||
|
||||
expect(mockEvent.preventDefault).toHaveBeenCalled();
|
||||
expect(mockEvent.stopPropagation).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('skips event handling on non-event arguments', () => {
|
||||
const regularArg = { someData: 'value' };
|
||||
const mockFunc = jest.fn();
|
||||
|
||||
const { getByTestId } = render(
|
||||
<ConfirmStatusChange {...mockedProps}>
|
||||
{confirm => (
|
||||
<Button
|
||||
data-test="trigger"
|
||||
onClick={() => confirm(regularArg, mockFunc)}
|
||||
/>
|
||||
)}
|
||||
</ConfirmStatusChange>,
|
||||
);
|
||||
|
||||
// Should not throw when processing non-event arguments
|
||||
expect(() => {
|
||||
fireEvent.click(getByTestId('trigger'));
|
||||
}).not.toThrow();
|
||||
|
||||
expect(getByTestId(`${mockedProps.title}-modal`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('ignores null and undefined arguments', () => {
|
||||
const { getByTestId } = render(
|
||||
<ConfirmStatusChange {...mockedProps}>
|
||||
{confirm => (
|
||||
<Button
|
||||
data-test="trigger"
|
||||
onClick={() => confirm(null, undefined, 'valid')}
|
||||
/>
|
||||
)}
|
||||
</ConfirmStatusChange>,
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
fireEvent.click(getByTestId('trigger'));
|
||||
}).not.toThrow();
|
||||
|
||||
expect(getByTestId(`${mockedProps.title}-modal`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('handles partial event objects gracefully', () => {
|
||||
const partialEvent1 = { preventDefault: jest.fn() }; // Only preventDefault
|
||||
const partialEvent2 = { stopPropagation: jest.fn() }; // Only stopPropagation
|
||||
|
||||
const { getByTestId } = render(
|
||||
<ConfirmStatusChange {...mockedProps}>
|
||||
{confirm => (
|
||||
<Button
|
||||
data-test="trigger"
|
||||
onClick={() => confirm(partialEvent1, partialEvent2)}
|
||||
/>
|
||||
)}
|
||||
</ConfirmStatusChange>,
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('trigger'));
|
||||
|
||||
expect(partialEvent1.preventDefault).toHaveBeenCalled();
|
||||
expect(partialEvent2.stopPropagation).toHaveBeenCalled();
|
||||
expect(getByTestId(`${mockedProps.title}-modal`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('closes modal when onHide is called', () => {
|
||||
const { getByTestId, getByRole } = render(
|
||||
<ConfirmStatusChange {...mockedProps}>
|
||||
{confirm => <Button data-test="trigger" onClick={confirm} />}
|
||||
</ConfirmStatusChange>,
|
||||
);
|
||||
|
||||
// Open modal
|
||||
fireEvent.click(getByTestId('trigger'));
|
||||
const modal = getByTestId(`${mockedProps.title}-modal`);
|
||||
expect(modal).toBeInTheDocument();
|
||||
expect(modal).toBeVisible();
|
||||
|
||||
// Close modal
|
||||
const cancelButton = getByRole('button', { name: 'Cancel' });
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
// Modal should be hidden (not visible)
|
||||
expect(modal).not.toBeVisible();
|
||||
});
|
||||
@@ -31,14 +31,11 @@ export function ConfirmStatusChange({
|
||||
const [currentCallbackArgs, setCurrentCallbackArgs] = useState<any[]>([]);
|
||||
|
||||
const showConfirm = (...callbackArgs: any[]) => {
|
||||
// check if any args are DOM events, if so, call persist
|
||||
// check if any args are DOM events, if so, handle them
|
||||
callbackArgs.forEach(arg => {
|
||||
if (!arg) {
|
||||
return;
|
||||
}
|
||||
if (typeof arg.persist === 'function') {
|
||||
arg.persist();
|
||||
}
|
||||
if (typeof arg.preventDefault === 'function') {
|
||||
arg.preventDefault();
|
||||
}
|
||||
|
||||
@@ -34,8 +34,10 @@ const StyledEditableTitle = styled.span<{
|
||||
canEdit: boolean;
|
||||
}>`
|
||||
&.editable-title {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
display: inline;
|
||||
&.editable-title--editing {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
@@ -52,7 +54,6 @@ const StyledEditableTitle = styled.span<{
|
||||
|
||||
input[type='text'],
|
||||
textarea {
|
||||
border: 1px solid ${({ theme }) => theme.colorSplit};
|
||||
color: ${({ theme }) => theme.colorTextTertiary};
|
||||
border-radius: ${({ theme }) => theme.sizeUnit}px;
|
||||
font-size: ${({ theme }) => theme.fontSizeLG}px;
|
||||
|
||||
@@ -60,7 +60,7 @@ const EmptyStateContainer = styled.div`
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: ${theme.colorTextQuaternary};
|
||||
color: ${theme.colorTextTertiary};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: ${theme.sizeUnit * 4}px;
|
||||
@@ -84,7 +84,7 @@ const EmptyStateContainer = styled.div`
|
||||
const Title = styled.p<{ size: EmptyStateSize }>`
|
||||
${({ theme, size }) => css`
|
||||
font-size: ${size === 'large' ? theme.fontSizeLG : theme.fontSize}px;
|
||||
color: ${theme.colorTextQuaternary};
|
||||
color: ${theme.colorTextTertiary};
|
||||
margin-top: ${size === 'large' ? theme.sizeUnit * 4 : theme.sizeUnit * 2}px;
|
||||
font-weight: ${theme.fontWeightStrong};
|
||||
`}
|
||||
@@ -93,7 +93,7 @@ const Title = styled.p<{ size: EmptyStateSize }>`
|
||||
const Description = styled.p<{ size: EmptyStateSize }>`
|
||||
${({ theme, size }) => css`
|
||||
font-size: ${size === 'large' ? theme.fontSize : theme.fontSizeSM}px;
|
||||
color: ${theme.colorTextQuaternary};
|
||||
color: ${theme.colorTextTertiary};
|
||||
margin-top: ${theme.sizeUnit * 2}px;
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -50,17 +50,7 @@ const IconButton: React.FC<IconButtonProps> = ({
|
||||
};
|
||||
|
||||
const renderIcon = () => {
|
||||
const iconContent = icon ? (
|
||||
<img
|
||||
src={icon as string}
|
||||
alt={altText || buttonText}
|
||||
css={css`
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
height: 100px;
|
||||
`}
|
||||
/>
|
||||
) : (
|
||||
const iconContent = (
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
@@ -69,12 +59,19 @@ const IconButton: React.FC<IconButtonProps> = ({
|
||||
height: 100px;
|
||||
`}
|
||||
>
|
||||
<Icons.DatabaseOutlined
|
||||
css={css`
|
||||
font-size: 48px;
|
||||
`}
|
||||
aria-label="default-icon"
|
||||
/>
|
||||
{icon ? (
|
||||
<img
|
||||
src={icon as string}
|
||||
alt={altText || buttonText}
|
||||
css={css`
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
height: 48px;
|
||||
`}
|
||||
/>
|
||||
) : (
|
||||
<Icons.DatabaseOutlined iconSize="xxl" aria-label="default-icon" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ export const IconTooltip = ({
|
||||
placement = 'top',
|
||||
style = {},
|
||||
tooltip = null,
|
||||
mouseEnterDelay = 0.3,
|
||||
mouseLeaveDelay = 0.15,
|
||||
}: IconTooltipProps) => {
|
||||
const iconTooltip = (
|
||||
<Button
|
||||
@@ -47,8 +49,8 @@ export const IconTooltip = ({
|
||||
id="tooltip"
|
||||
title={tooltip}
|
||||
placement={placement}
|
||||
mouseEnterDelay={0.3}
|
||||
mouseLeaveDelay={0.15}
|
||||
mouseEnterDelay={mouseEnterDelay}
|
||||
mouseLeaveDelay={mouseLeaveDelay}
|
||||
>
|
||||
{iconTooltip}
|
||||
</Tooltip>
|
||||
|
||||
@@ -37,4 +37,6 @@ export interface IconTooltipProps {
|
||||
| 'rightBottom';
|
||||
style?: object;
|
||||
tooltip?: string | null;
|
||||
mouseEnterDelay?: number;
|
||||
mouseLeaveDelay?: number;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ import { BaseIconComponent } from './BaseIcon';
|
||||
const AsyncIcon = (props: IconType) => {
|
||||
const [, setLoaded] = useState(false);
|
||||
const ImportedSVG = useRef<FC<SVGProps<SVGSVGElement>>>();
|
||||
const { fileName, ...restProps } = props;
|
||||
const { fileName, customIcons, iconSize, iconColor, viewBox, ...restProps } =
|
||||
props;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -46,6 +47,11 @@ const AsyncIcon = (props: IconType) => {
|
||||
return (
|
||||
<BaseIconComponent
|
||||
component={ImportedSVG.current || TransparentIcon}
|
||||
fileName={fileName}
|
||||
customIcons={customIcons}
|
||||
iconSize={iconSize}
|
||||
iconColor={iconColor}
|
||||
viewBox={viewBox}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ import { AntdIconType, BaseIconProps, CustomIconType, IconType } from './types';
|
||||
|
||||
const genAriaLabel = (fileName: string) => {
|
||||
const name = fileName.replace(/_/g, '-'); // Replace underscores with dashes
|
||||
const words = name.split(/(?=[A-Z])/); // Split at uppercase letters
|
||||
const words = name.split(/(?<=[a-z])(?=[A-Z])/); // Split at lowercase-to-uppercase transitions
|
||||
|
||||
if (words.length === 2) {
|
||||
return words[0].toLowerCase();
|
||||
|
||||
@@ -40,7 +40,14 @@ export const PublishedLabel: React.FC<PublishedLabelProps> = ({
|
||||
const labelType = isPublished ? 'success' : 'primary';
|
||||
|
||||
return (
|
||||
<Label type={labelType} icon={icon} onClick={onClick}>
|
||||
<Label
|
||||
type={labelType}
|
||||
icon={icon}
|
||||
onClick={onClick}
|
||||
style={{
|
||||
color: isPublished ? theme.colorSuccessText : theme.colorPrimaryText,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
);
|
||||
|
||||
@@ -1,37 +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 { render, screen, userEvent } from '@superset-ui/core/spec';
|
||||
import { Ellipsis } from './Ellipsis';
|
||||
|
||||
test('Ellipsis - click when the button is enabled', async () => {
|
||||
const click = jest.fn();
|
||||
render(<Ellipsis onClick={click} />);
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(click).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('Ellipsis - click when the button is disabled', async () => {
|
||||
const click = jest.fn();
|
||||
render(<Ellipsis onClick={click} disabled />);
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
@@ -1,47 +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 { render, screen, userEvent } from '@superset-ui/core/spec';
|
||||
import { Item } from './Item';
|
||||
|
||||
test('Item - click when the item is not active', async () => {
|
||||
const click = jest.fn();
|
||||
render(
|
||||
<Item onClick={click}>
|
||||
<div data-test="test" />
|
||||
</Item>,
|
||||
);
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(click).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByTestId('test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Item - click when the item is active', async () => {
|
||||
const click = jest.fn();
|
||||
render(
|
||||
<Item onClick={click} active>
|
||||
<div data-test="test" />
|
||||
</Item>,
|
||||
);
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
expect(screen.getByTestId('test')).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,45 +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 { ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { PaginationButtonProps } from './types';
|
||||
|
||||
interface PaginationItemButton extends PaginationButtonProps {
|
||||
active?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Item({ active, children, onClick }: PaginationItemButton) {
|
||||
return (
|
||||
<li className={classNames({ active })}>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
if (!active) onClick(e);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +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 { render, screen, userEvent } from '@superset-ui/core/spec';
|
||||
import { Prev } from './Prev';
|
||||
|
||||
test('Prev - click when the button is enabled', async () => {
|
||||
const click = jest.fn();
|
||||
render(<Prev onClick={click} />);
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(click).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('Prev - click when the button is disabled', async () => {
|
||||
const click = jest.fn();
|
||||
render(<Prev onClick={click} disabled />);
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
@@ -1,75 +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 { render, screen, cleanup } from '@superset-ui/core/spec';
|
||||
import Wrapper from './Wrapper';
|
||||
|
||||
// Add cleanup after each test
|
||||
afterEach(async () => {
|
||||
cleanup();
|
||||
// Wait for any pending effects to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
jest.mock('./Next', () => ({
|
||||
Next: () => <div data-test="next" />,
|
||||
}));
|
||||
jest.mock('./Prev', () => ({
|
||||
Prev: () => <div data-test="prev" />,
|
||||
}));
|
||||
jest.mock('./Item', () => ({
|
||||
Item: () => <div data-test="item" />,
|
||||
}));
|
||||
jest.mock('./Ellipsis', () => ({
|
||||
Ellipsis: () => <div data-test="ellipsis" />,
|
||||
}));
|
||||
|
||||
test('Pagination rendering correctly', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<li data-test="test" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Next attribute', async () => {
|
||||
render(<Wrapper.Next onClick={jest.fn()} />);
|
||||
expect(screen.getByTestId('next')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Prev attribute', async () => {
|
||||
render(<Wrapper.Next onClick={jest.fn()} />);
|
||||
expect(screen.getByTestId('next')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Item attribute', async () => {
|
||||
render(
|
||||
<Wrapper.Item onClick={jest.fn()}>
|
||||
<></>
|
||||
</Wrapper.Item>,
|
||||
);
|
||||
expect(screen.getByTestId('item')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Ellipsis attribute', async () => {
|
||||
render(<Wrapper.Ellipsis onClick={jest.fn()} />);
|
||||
expect(screen.getByTestId('ellipsis')).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,90 +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 { styled } from '@superset-ui/core';
|
||||
import { Next } from './Next';
|
||||
import { Prev } from './Prev';
|
||||
import { Item } from './Item';
|
||||
import { Ellipsis } from './Ellipsis';
|
||||
|
||||
interface PaginationProps {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
}
|
||||
|
||||
const PaginationList = styled.ul`
|
||||
${({ theme }) => `
|
||||
display: inline-block;
|
||||
padding: ${theme.sizeUnit * 3}px;
|
||||
|
||||
li {
|
||||
display: inline;
|
||||
margin: 0 4px;
|
||||
|
||||
> span {
|
||||
padding: 8px 12px;
|
||||
text-decoration: none;
|
||||
background-color: ${theme.colorBgContainer};
|
||||
border: 1px solid ${theme.colorBorder};
|
||||
border-radius: ${theme.borderRadius}px;
|
||||
color: ${theme.colorText};
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
z-index: 2;
|
||||
color: ${theme.colorText};
|
||||
background-color: ${theme.colorBgLayout};
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
span {
|
||||
background-color: transparent;
|
||||
cursor: default;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
span {
|
||||
z-index: 3;
|
||||
color: ${theme.colorBgLayout};
|
||||
cursor: default;
|
||||
background-color: ${theme.colorPrimary};
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
function Pagination({ children }: PaginationProps) {
|
||||
return <PaginationList role="navigation">{children}</PaginationList>;
|
||||
}
|
||||
|
||||
Pagination.Next = Next;
|
||||
Pagination.Prev = Prev;
|
||||
Pagination.Item = Item;
|
||||
Pagination.Ellipsis = Ellipsis;
|
||||
|
||||
export default Pagination;
|
||||
@@ -1,47 +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 Pagination from '@superset-ui/core/components/Pagination/Wrapper';
|
||||
import {
|
||||
createUltimatePagination,
|
||||
ITEM_TYPES,
|
||||
} from 'react-ultimate-pagination';
|
||||
|
||||
const ListViewPagination = createUltimatePagination({
|
||||
WrapperComponent: Pagination,
|
||||
itemTypeToComponent: {
|
||||
[ITEM_TYPES.PAGE]: ({ value, isActive, onClick }) => (
|
||||
<Pagination.Item active={isActive} onClick={onClick}>
|
||||
{value}
|
||||
</Pagination.Item>
|
||||
),
|
||||
[ITEM_TYPES.ELLIPSIS]: ({ isActive, onClick }) => (
|
||||
<Pagination.Ellipsis disabled={isActive} onClick={onClick} />
|
||||
),
|
||||
[ITEM_TYPES.PREVIOUS_PAGE_LINK]: ({ isActive, onClick }) => (
|
||||
<Pagination.Prev disabled={isActive} onClick={onClick} />
|
||||
),
|
||||
[ITEM_TYPES.NEXT_PAGE_LINK]: ({ isActive, onClick }) => (
|
||||
<Pagination.Next disabled={isActive} onClick={onClick} />
|
||||
),
|
||||
[ITEM_TYPES.FIRST_PAGE_LINK]: () => null,
|
||||
[ITEM_TYPES.LAST_PAGE_LINK]: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
export default ListViewPagination;
|
||||
@@ -180,6 +180,10 @@ const StyledTable = styled(AntTable as FC<AntTableProps>)<{ height?: number }>(
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
td.ant-table-cell.no-ellipsis {
|
||||
text-overflow: unset;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -100,3 +100,109 @@ test('Should the loading-indicator be visible during loading', () => {
|
||||
|
||||
expect(screen.getByTestId('loading-indicator')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Pagination controls should be rendered when pageSize is provided', () => {
|
||||
const paginationProps = {
|
||||
...defaultProps,
|
||||
pageSize: 2,
|
||||
totalCount: 3,
|
||||
pageIndex: 0,
|
||||
onPageChange: jest.fn(),
|
||||
};
|
||||
render(<TableCollection {...paginationProps} />);
|
||||
|
||||
expect(screen.getByRole('list')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Pagination should call onPageChange when page is changed', async () => {
|
||||
const onPageChange = jest.fn();
|
||||
const paginationProps = {
|
||||
...defaultProps,
|
||||
pageSize: 2,
|
||||
totalCount: 3,
|
||||
pageIndex: 0,
|
||||
onPageChange,
|
||||
};
|
||||
const { rerender } = render(<TableCollection {...paginationProps} />);
|
||||
|
||||
// Simulate pagination change
|
||||
await screen.findByTitle('Next Page');
|
||||
|
||||
// Verify onPageChange would be called with correct arguments
|
||||
// The actual AntD pagination will handle the click internally
|
||||
expect(onPageChange).toBeDefined();
|
||||
|
||||
// Verify that re-rendering with new pageIndex works
|
||||
rerender(<TableCollection {...paginationProps} pageIndex={1} />);
|
||||
expect(screen.getByRole('list')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Pagination callback should be stable across re-renders', () => {
|
||||
const onPageChange = jest.fn();
|
||||
const paginationProps = {
|
||||
...defaultProps,
|
||||
pageSize: 2,
|
||||
totalCount: 3,
|
||||
pageIndex: 0,
|
||||
onPageChange,
|
||||
};
|
||||
|
||||
const { rerender } = render(<TableCollection {...paginationProps} />);
|
||||
|
||||
// Re-render with same props
|
||||
rerender(<TableCollection {...paginationProps} />);
|
||||
|
||||
// onPageChange should not have been called during re-render
|
||||
expect(onPageChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Should display correct page info when showRowCount is true', () => {
|
||||
const paginationProps = {
|
||||
...defaultProps,
|
||||
pageSize: 2,
|
||||
totalCount: 3,
|
||||
pageIndex: 0,
|
||||
onPageChange: jest.fn(),
|
||||
showRowCount: true,
|
||||
};
|
||||
render(<TableCollection {...paginationProps} />);
|
||||
|
||||
// AntD pagination shows page info
|
||||
expect(screen.getByText('1-2 of 3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Should not display page info when showRowCount is false', () => {
|
||||
const paginationProps = {
|
||||
...defaultProps,
|
||||
pageSize: 2,
|
||||
totalCount: 3,
|
||||
pageIndex: 0,
|
||||
onPageChange: jest.fn(),
|
||||
showRowCount: false,
|
||||
};
|
||||
render(<TableCollection {...paginationProps} />);
|
||||
|
||||
// Page info should not be shown
|
||||
expect(screen.queryByText('1-2 of 3')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Bulk selection should work with pagination', () => {
|
||||
const toggleRowSelected = jest.fn();
|
||||
const toggleAllRowsSelected = jest.fn();
|
||||
const selectionProps = {
|
||||
...defaultProps,
|
||||
bulkSelectEnabled: true,
|
||||
selectedFlatRows: [],
|
||||
toggleRowSelected,
|
||||
toggleAllRowsSelected,
|
||||
pageSize: 2,
|
||||
totalCount: 3,
|
||||
pageIndex: 0,
|
||||
onPageChange: jest.fn(),
|
||||
};
|
||||
render(<TableCollection {...selectionProps} />);
|
||||
|
||||
// Check that selection checkboxes are rendered
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
expect(checkboxes.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { HTMLAttributes, memo, useMemo } from 'react';
|
||||
import { HTMLAttributes, memo, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
ColumnInstance,
|
||||
HeaderGroup,
|
||||
@@ -47,15 +47,25 @@ interface TableCollectionProps<T extends object> {
|
||||
toggleAllRowsSelected?: (value?: boolean) => void;
|
||||
sticky?: boolean;
|
||||
size?: TableSize;
|
||||
pageIndex?: number;
|
||||
pageSize?: number;
|
||||
totalCount?: number;
|
||||
onPageChange?: (page: number, pageSize: number) => void;
|
||||
isPaginationSticky?: boolean;
|
||||
showRowCount?: boolean;
|
||||
}
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
${({ theme }) => `
|
||||
const StyledTable = styled(Table)<{
|
||||
isPaginationSticky?: boolean;
|
||||
showRowCount?: boolean;
|
||||
}>`
|
||||
${({ theme, isPaginationSticky, showRowCount }) => `
|
||||
th.ant-column-cell {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.actions {
|
||||
opacity: 0;
|
||||
font-size: ${theme.fontSizeXL}px;
|
||||
@@ -72,16 +82,20 @@ const StyledTable = styled(Table)`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-column-title {
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
.ant-table-row:hover {
|
||||
.actions {
|
||||
opacity: 1;
|
||||
transition: opacity ease-in ${theme.motionDurationMid};
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
max-width: 320px;
|
||||
font-feature-settings: 'tnum' 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
@@ -90,9 +104,41 @@ const StyledTable = styled(Table)`
|
||||
padding-left: ${theme.sizeUnit * 4}px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ant-table-placeholder .ant-table-cell {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&.ant-table-wrapper .ant-table-pagination.ant-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: ${showRowCount ? theme.sizeUnit * 4 : 0}px 0 ${showRowCount ? theme.sizeUnit * 14 : 0}px 0;
|
||||
position: relative;
|
||||
|
||||
.ant-pagination-total-text {
|
||||
color: ${theme.colorTextBase};
|
||||
margin-inline-end: 0;
|
||||
position: absolute;
|
||||
top: ${theme.sizeUnit * 12}px;
|
||||
}
|
||||
|
||||
${
|
||||
isPaginationSticky &&
|
||||
`
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
background-color: ${theme.colorBgElevated};
|
||||
padding: ${theme.sizeUnit * 2}px 0;
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
// Hotfix - antd doesn't apply background color to overflowing cells
|
||||
& table {
|
||||
background-color: ${theme.colorBgContainer};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -110,13 +156,22 @@ function TableCollection<T extends object>({
|
||||
prepareRow,
|
||||
sticky,
|
||||
size = TableSize.Middle,
|
||||
pageIndex = 0,
|
||||
pageSize = 25,
|
||||
totalCount = 0,
|
||||
onPageChange,
|
||||
isPaginationSticky = false,
|
||||
showRowCount = true,
|
||||
}: TableCollectionProps<T>) {
|
||||
const mappedColumns = mapColumns<T>(
|
||||
columns,
|
||||
headerGroups,
|
||||
columnsForWrapText,
|
||||
const mappedColumns = useMemo(
|
||||
() => mapColumns<T>(columns, headerGroups, columnsForWrapText),
|
||||
[columns, headerGroups, columnsForWrapText],
|
||||
);
|
||||
|
||||
const mappedRows = useMemo(
|
||||
() => mapRows(rows, prepareRow),
|
||||
[rows, prepareRow],
|
||||
);
|
||||
const mappedRows = mapRows(rows, prepareRow);
|
||||
|
||||
const selectedRowKeys = useMemo(
|
||||
() => selectedFlatRows?.map(row => row.id) || [],
|
||||
@@ -141,6 +196,68 @@ function TableCollection<T extends object>({
|
||||
toggleRowSelected,
|
||||
toggleAllRowsSelected,
|
||||
]);
|
||||
|
||||
const handlePaginationChange = useCallback(
|
||||
(page: number, size: number) => {
|
||||
const validPage = Math.max(0, (page || 1) - 1);
|
||||
const validSize = size || pageSize;
|
||||
onPageChange?.(validPage, validSize);
|
||||
},
|
||||
[pageSize, onPageChange],
|
||||
);
|
||||
|
||||
const showTotalFunc = useCallback(
|
||||
(total: number, range: [number, number]) =>
|
||||
`${range[0]}-${range[1]} of ${total}`,
|
||||
[],
|
||||
);
|
||||
|
||||
const handleTableChange = useCallback(
|
||||
(_pagination: any, _filters: any, sorter: SorterResult) => {
|
||||
if (sorter && sorter.field) {
|
||||
setSortBy?.([
|
||||
{
|
||||
id: sorter.field,
|
||||
desc: sorter.order === 'descend',
|
||||
},
|
||||
] as SortingRule<T>[]);
|
||||
}
|
||||
},
|
||||
[setSortBy],
|
||||
);
|
||||
|
||||
const paginationConfig = useMemo(() => {
|
||||
if (totalCount === 0) return false;
|
||||
|
||||
const config: any = {
|
||||
pageSize,
|
||||
size: 'default' as const,
|
||||
showSizeChanger: false,
|
||||
showQuickJumper: false,
|
||||
align: 'center' as const,
|
||||
showTotal: showRowCount ? showTotalFunc : undefined,
|
||||
};
|
||||
|
||||
if (onPageChange) {
|
||||
config.current = pageIndex + 1;
|
||||
config.total = totalCount;
|
||||
config.onChange = handlePaginationChange;
|
||||
} else {
|
||||
if (pageIndex > 0) config.defaultCurrent = pageIndex + 1;
|
||||
config.total = totalCount;
|
||||
}
|
||||
|
||||
return config;
|
||||
}, [
|
||||
pageSize,
|
||||
totalCount,
|
||||
showRowCount,
|
||||
showTotalFunc,
|
||||
pageIndex,
|
||||
handlePaginationChange,
|
||||
onPageChange,
|
||||
]);
|
||||
|
||||
return (
|
||||
<StyledTable
|
||||
loading={loading}
|
||||
@@ -149,12 +266,15 @@ function TableCollection<T extends object>({
|
||||
data={mappedRows}
|
||||
size={size}
|
||||
data-test="listview-table"
|
||||
pagination={false}
|
||||
tableLayout="fixed"
|
||||
pagination={paginationConfig}
|
||||
scroll={{ x: 'max-content' }}
|
||||
tableLayout="auto"
|
||||
rowKey="rowId"
|
||||
rowSelection={rowSelection}
|
||||
locale={{ emptyText: null }}
|
||||
sortDirections={['ascend', 'descend', 'ascend']}
|
||||
isPaginationSticky={isPaginationSticky}
|
||||
showRowCount={showRowCount}
|
||||
components={{
|
||||
header: {
|
||||
cell: (props: HTMLAttributes<HTMLTableCellElement>) => (
|
||||
@@ -170,14 +290,7 @@ function TableCollection<T extends object>({
|
||||
),
|
||||
},
|
||||
}}
|
||||
onChange={(_pagination, _filters, sorter: SorterResult) => {
|
||||
setSortBy?.([
|
||||
{
|
||||
id: sorter.field,
|
||||
desc: sorter.order === 'descend',
|
||||
},
|
||||
] as SortingRule<T>[]);
|
||||
}}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ type EnhancedColumnInstance<T extends object = any> = RTColumnInstance<T> &
|
||||
Partial<UseResizeColumnsColumnProps<T>> & {
|
||||
hidden?: boolean;
|
||||
size?: keyof typeof COLUMN_SIZE_MAP;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type EnhancedHeaderGroup<T extends object = any> = RTHeaderGroup<T> & {
|
||||
@@ -94,7 +95,7 @@ export function mapColumns<T extends object>(
|
||||
dataIndex: column.id?.includes('.') ? column.id.split('.') : column.id,
|
||||
hidden: column.hidden,
|
||||
key: column.id,
|
||||
width: column.size ? COLUMN_SIZE_MAP[column.size] : COLUMN_SIZE_MAP.md,
|
||||
width: column.size ? COLUMN_SIZE_MAP[column.size] : undefined,
|
||||
ellipsis: !columnsForWrapText?.includes(column.id),
|
||||
defaultSortOrder: (isSorted
|
||||
? isSortedDesc
|
||||
@@ -122,6 +123,7 @@ export function mapColumns<T extends object>(
|
||||
}
|
||||
return val;
|
||||
},
|
||||
className: column.className,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen, userEvent } from '@superset-ui/core/spec';
|
||||
import { render, screen, userEvent, waitFor } from '@superset-ui/core/spec';
|
||||
import { TableView, TableViewProps } from '.';
|
||||
|
||||
const mockedProps: TableViewProps = {
|
||||
@@ -30,6 +30,7 @@ const mockedProps: TableViewProps = {
|
||||
{
|
||||
accessor: 'age',
|
||||
Header: 'Age',
|
||||
sortable: true,
|
||||
id: 'age',
|
||||
},
|
||||
{
|
||||
@@ -78,10 +79,10 @@ test('should render the cells', () => {
|
||||
|
||||
test('should render the pagination', () => {
|
||||
render(<TableView {...mockedProps} />);
|
||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('button')).toHaveLength(4);
|
||||
expect(screen.getByText('«')).toBeInTheDocument();
|
||||
expect(screen.getByText('»')).toBeInTheDocument();
|
||||
expect(screen.getByRole('list')).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('button')).toHaveLength(2);
|
||||
expect(screen.getByTitle('Previous Page')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('Next Page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show the row count by default', () => {
|
||||
@@ -104,45 +105,63 @@ test('should NOT render the pagination when disabled', () => {
|
||||
withPagination: false,
|
||||
};
|
||||
render(<TableView {...withoutPaginationProps} />);
|
||||
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('list')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should NOT render the pagination when fewer rows than page size', () => {
|
||||
test('should render the pagination even when fewer rows than page size', () => {
|
||||
const withoutPaginationProps = {
|
||||
...mockedProps,
|
||||
pageSize: 3,
|
||||
};
|
||||
render(<TableView {...withoutPaginationProps} />);
|
||||
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('list')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should change page when « and » buttons are clicked', async () => {
|
||||
test('should change page when pagination is clicked', async () => {
|
||||
render(<TableView {...mockedProps} />);
|
||||
const nextBtn = screen.getByText('»');
|
||||
const prevBtn = screen.getByText('«');
|
||||
|
||||
await userEvent.click(nextBtn);
|
||||
expect(screen.getAllByRole('cell')).toHaveLength(3);
|
||||
expect(screen.getByText('321')).toBeInTheDocument();
|
||||
expect(screen.getByText('10')).toBeInTheDocument();
|
||||
expect(screen.getByText('Kate')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Emily')).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(prevBtn);
|
||||
expect(screen.getAllByRole('cell')).toHaveLength(3);
|
||||
expect(screen.getByText('123')).toBeInTheDocument();
|
||||
expect(screen.getByText('27')).toBeInTheDocument();
|
||||
expect(screen.getByText('Emily')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Kate')).not.toBeInTheDocument();
|
||||
|
||||
const page2 = screen.getByRole('listitem', { name: '2' });
|
||||
await userEvent.click(page2);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByRole('cell')).toHaveLength(3);
|
||||
expect(screen.getByText('321')).toBeInTheDocument();
|
||||
expect(screen.getByText('10')).toBeInTheDocument();
|
||||
expect(screen.getByText('Kate')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Emily')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const page1 = screen.getByRole('listitem', { name: '1' });
|
||||
await userEvent.click(page1);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByRole('cell')).toHaveLength(3);
|
||||
expect(screen.getByText('123')).toBeInTheDocument();
|
||||
expect(screen.getByText('27')).toBeInTheDocument();
|
||||
expect(screen.getByText('Emily')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Kate')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should sort by age', async () => {
|
||||
render(<TableView {...mockedProps} />);
|
||||
|
||||
await userEvent.click(screen.getAllByTestId('sort-header')[1]);
|
||||
expect(screen.getAllByTestId('table-row-cell')[1]).toHaveTextContent('10');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('table-row-cell')[1]).toHaveTextContent('10');
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getAllByTestId('sort-header')[1]);
|
||||
expect(screen.getAllByTestId('table-row-cell')[1]).toHaveTextContent('27');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('table-row-cell')[1]).toHaveTextContent('27');
|
||||
});
|
||||
});
|
||||
|
||||
test('should sort by initialSortBy DESC', () => {
|
||||
@@ -208,3 +227,146 @@ test('should render the right wrap content text by columnsForWrapText', () => {
|
||||
'ant-table-cell-ellipsis',
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle server-side pagination', async () => {
|
||||
const onServerPagination = jest.fn();
|
||||
const serverPaginationProps = {
|
||||
...mockedProps,
|
||||
serverPagination: true,
|
||||
onServerPagination,
|
||||
totalCount: 10,
|
||||
pageSize: 2,
|
||||
};
|
||||
render(<TableView {...serverPaginationProps} />);
|
||||
|
||||
// Click next page
|
||||
const page2 = screen.getByRole('listitem', { name: '2' });
|
||||
await userEvent.click(page2);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onServerPagination).toHaveBeenCalledWith({
|
||||
pageIndex: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle server-side sorting', async () => {
|
||||
const onServerPagination = jest.fn();
|
||||
const serverPaginationProps = {
|
||||
...mockedProps,
|
||||
serverPagination: true,
|
||||
onServerPagination,
|
||||
};
|
||||
render(<TableView {...serverPaginationProps} />);
|
||||
|
||||
// Click on sortable column
|
||||
await userEvent.click(screen.getAllByTestId('sort-header')[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onServerPagination).toHaveBeenCalledWith({
|
||||
pageIndex: 0,
|
||||
sortBy: [{ id: 'id', desc: false }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('pagination callbacks should be stable across re-renders', () => {
|
||||
const onServerPagination = jest.fn();
|
||||
const serverPaginationProps = {
|
||||
...mockedProps,
|
||||
serverPagination: true,
|
||||
onServerPagination,
|
||||
totalCount: 10,
|
||||
pageSize: 2,
|
||||
};
|
||||
|
||||
const { rerender } = render(<TableView {...serverPaginationProps} />);
|
||||
|
||||
// Re-render with same props
|
||||
rerender(<TableView {...serverPaginationProps} />);
|
||||
|
||||
// onServerPagination should not have been called during re-render
|
||||
expect(onServerPagination).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should scroll to top when scrollTopOnPagination is true', async () => {
|
||||
const scrollToSpy = jest
|
||||
.spyOn(window, 'scrollTo')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const scrollProps = {
|
||||
...mockedProps,
|
||||
scrollTopOnPagination: true,
|
||||
pageSize: 1,
|
||||
};
|
||||
render(<TableView {...scrollProps} />);
|
||||
|
||||
// Click next page
|
||||
const page2 = screen.getByRole('listitem', { name: '2' });
|
||||
await userEvent.click(page2);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(scrollToSpy).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
scrollToSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('should NOT scroll to top when scrollTopOnPagination is false', async () => {
|
||||
const scrollToSpy = jest
|
||||
.spyOn(window, 'scrollTo')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const scrollProps = {
|
||||
...mockedProps,
|
||||
scrollTopOnPagination: false,
|
||||
pageSize: 1,
|
||||
};
|
||||
render(<TableView {...scrollProps} />);
|
||||
|
||||
// Click next page
|
||||
const page2 = screen.getByRole('listitem', { name: '2' });
|
||||
await userEvent.click(page2);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('321')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(scrollToSpy).not.toHaveBeenCalled();
|
||||
|
||||
scrollToSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('should handle totalCount of 0 correctly', () => {
|
||||
const emptyProps = {
|
||||
...mockedProps,
|
||||
data: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
render(<TableView {...emptyProps} />);
|
||||
|
||||
// Pagination should not be shown when totalCount is 0
|
||||
expect(screen.queryByRole('list')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should handle large datasets with pagination', () => {
|
||||
const largeDataset = Array.from({ length: 100 }, (_, i) => ({
|
||||
id: i,
|
||||
age: 20 + i,
|
||||
name: `Person ${i}`,
|
||||
}));
|
||||
|
||||
const largeDataProps = {
|
||||
...mockedProps,
|
||||
data: largeDataset,
|
||||
pageSize: 10,
|
||||
};
|
||||
render(<TableView {...largeDataProps} />);
|
||||
|
||||
// Should show only first page (10 items)
|
||||
expect(screen.getAllByTestId('table-row')).toHaveLength(10);
|
||||
|
||||
// Should show pagination with correct page count
|
||||
expect(screen.getByRole('list')).toBeInTheDocument();
|
||||
expect(screen.getByText('1-10 of 100')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -16,16 +16,17 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import { memo, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { useFilters, usePagination, useSortBy, useTable } from 'react-table';
|
||||
import { Empty } from '@superset-ui/core/components';
|
||||
import Pagination from '@superset-ui/core/components/Pagination';
|
||||
import TableCollection from '@superset-ui/core/components/TableCollection';
|
||||
import { TableSize } from '@superset-ui/core/components/Table';
|
||||
import { SortByType, ServerPagination } from './types';
|
||||
|
||||
const NOOP_SERVER_PAGINATION = () => {};
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
|
||||
export enum EmptyWrapperType {
|
||||
@@ -96,29 +97,6 @@ const TableViewStyles = styled.div<{
|
||||
}
|
||||
`;
|
||||
|
||||
const PaginationStyles = styled.div<{
|
||||
isPaginationSticky?: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.colorBgElevated};
|
||||
|
||||
${({ isPaginationSticky }) =>
|
||||
isPaginationSticky &&
|
||||
`
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
`};
|
||||
|
||||
.row-count-container {
|
||||
margin-top: ${({ theme }) => theme.sizeUnit * 2}px;
|
||||
color: ${({ theme }) => theme.colorText};
|
||||
}
|
||||
`;
|
||||
|
||||
const RawTableView = ({
|
||||
columns,
|
||||
data,
|
||||
@@ -133,16 +111,21 @@ const RawTableView = ({
|
||||
showRowCount = true,
|
||||
serverPagination = false,
|
||||
columnsForWrapText,
|
||||
onServerPagination = () => {},
|
||||
scrollTopOnPagination = false,
|
||||
onServerPagination = NOOP_SERVER_PAGINATION,
|
||||
scrollTopOnPagination = true,
|
||||
size = TableSize.Middle,
|
||||
...props
|
||||
}: TableViewProps) => {
|
||||
const initialState = {
|
||||
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
|
||||
pageIndex: initialPageIndex ?? 0,
|
||||
sortBy: initialSortBy,
|
||||
};
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
|
||||
const initialState = useMemo(
|
||||
() => ({
|
||||
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
|
||||
pageIndex: initialPageIndex ?? 0,
|
||||
sortBy: initialSortBy,
|
||||
}),
|
||||
[initialPageSize, initialPageIndex, initialSortBy],
|
||||
);
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
@@ -151,10 +134,9 @@ const RawTableView = ({
|
||||
page,
|
||||
rows,
|
||||
prepareRow,
|
||||
pageCount,
|
||||
gotoPage,
|
||||
setSortBy,
|
||||
state: { pageIndex, pageSize, sortBy },
|
||||
state: { pageIndex, sortBy },
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
@@ -162,36 +144,94 @@ const RawTableView = ({
|
||||
initialState,
|
||||
manualPagination: serverPagination,
|
||||
manualSortBy: serverPagination,
|
||||
pageCount: Math.ceil(totalCount / initialState.pageSize),
|
||||
pageCount: serverPagination
|
||||
? Math.ceil(totalCount / initialState.pageSize)
|
||||
: undefined,
|
||||
autoResetSortBy: false,
|
||||
},
|
||||
useFilters,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
...(withPagination ? [usePagination] : []),
|
||||
);
|
||||
|
||||
const content = withPagination ? page : rows;
|
||||
|
||||
let EmptyWrapperComponent;
|
||||
switch (emptyWrapperType) {
|
||||
case EmptyWrapperType.Small:
|
||||
EmptyWrapperComponent = ({ children }: any) => <>{children}</>;
|
||||
break;
|
||||
case EmptyWrapperType.Default:
|
||||
default:
|
||||
EmptyWrapperComponent = ({ children }: any) => (
|
||||
<EmptyWrapper>{children}</EmptyWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const isEmpty = !loading && content.length === 0;
|
||||
const hasPagination = pageCount > 1 && withPagination;
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
const handleGotoPage = (p: number) => {
|
||||
if (scrollTopOnPagination) {
|
||||
tableRef?.current?.scroll(0, 0);
|
||||
const EmptyWrapperComponent = useMemo(() => {
|
||||
switch (emptyWrapperType) {
|
||||
case EmptyWrapperType.Small:
|
||||
return ({ children }: any) => <>{children}</>;
|
||||
case EmptyWrapperType.Default:
|
||||
default:
|
||||
return ({ children }: any) => <EmptyWrapper>{children}</EmptyWrapper>;
|
||||
}
|
||||
gotoPage(p);
|
||||
};
|
||||
}, [emptyWrapperType]);
|
||||
|
||||
const content = useMemo(
|
||||
() => (withPagination ? page : rows),
|
||||
[withPagination, page, rows],
|
||||
);
|
||||
|
||||
const isEmpty = useMemo(
|
||||
() => !loading && content.length === 0,
|
||||
[loading, content.length],
|
||||
);
|
||||
|
||||
const handleScrollToTop = useCallback(() => {
|
||||
if (scrollTopOnPagination) {
|
||||
if (tableRef?.current) {
|
||||
if (typeof tableRef.current.scrollTo === 'function') {
|
||||
tableRef.current.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else if (typeof tableRef.current.scroll === 'function') {
|
||||
tableRef.current.scroll(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && window.scrollTo)
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
}, [scrollTopOnPagination]);
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
(p: number) => {
|
||||
if (scrollTopOnPagination) handleScrollToTop();
|
||||
|
||||
gotoPage(p);
|
||||
},
|
||||
[scrollTopOnPagination, handleScrollToTop, gotoPage],
|
||||
);
|
||||
|
||||
const paginationProps = useMemo(() => {
|
||||
if (!withPagination) {
|
||||
return {
|
||||
pageIndex: 0,
|
||||
pageSize: data.length,
|
||||
totalCount: 0,
|
||||
onPageChange: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (serverPagination) {
|
||||
return {
|
||||
pageIndex,
|
||||
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
|
||||
totalCount,
|
||||
onPageChange: handlePageChange,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pageIndex,
|
||||
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
|
||||
totalCount: data.length,
|
||||
onPageChange: handlePageChange,
|
||||
};
|
||||
}, [
|
||||
withPagination,
|
||||
serverPagination,
|
||||
pageIndex,
|
||||
initialPageSize,
|
||||
totalCount,
|
||||
data.length,
|
||||
handlePageChange,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serverPagination && pageIndex !== initialState.pageIndex) {
|
||||
@@ -199,7 +239,7 @@ const RawTableView = ({
|
||||
pageIndex,
|
||||
});
|
||||
}
|
||||
}, [pageIndex]);
|
||||
}, [initialState.pageIndex, onServerPagination, pageIndex, serverPagination]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serverPagination && !isEqual(sortBy, initialState.sortBy)) {
|
||||
@@ -208,61 +248,38 @@ const RawTableView = ({
|
||||
sortBy,
|
||||
});
|
||||
}
|
||||
}, [sortBy]);
|
||||
}, [initialState.sortBy, onServerPagination, serverPagination, sortBy]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableViewStyles {...props} ref={tableRef}>
|
||||
<TableCollection
|
||||
getTableProps={getTableProps}
|
||||
getTableBodyProps={getTableBodyProps}
|
||||
prepareRow={prepareRow}
|
||||
headerGroups={headerGroups}
|
||||
rows={content}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
setSortBy={setSortBy}
|
||||
size={size}
|
||||
columnsForWrapText={columnsForWrapText}
|
||||
/>
|
||||
{isEmpty && (
|
||||
<EmptyWrapperComponent>
|
||||
{noDataText ? (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={noDataText}
|
||||
/>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
</EmptyWrapperComponent>
|
||||
)}
|
||||
</TableViewStyles>
|
||||
{hasPagination && (
|
||||
<PaginationStyles
|
||||
className="pagination-container"
|
||||
isPaginationSticky={props.isPaginationSticky}
|
||||
>
|
||||
<Pagination
|
||||
totalPages={pageCount || 0}
|
||||
currentPage={pageCount ? pageIndex + 1 : 0}
|
||||
onChange={(p: number) => handleGotoPage(p - 1)}
|
||||
hideFirstAndLastPageLinks
|
||||
/>
|
||||
{showRowCount && (
|
||||
<div className="row-count-container">
|
||||
{!loading &&
|
||||
t(
|
||||
'%s-%s of %s',
|
||||
pageSize * pageIndex + (page.length && 1),
|
||||
pageSize * pageIndex + page.length,
|
||||
totalCount,
|
||||
)}
|
||||
</div>
|
||||
<TableViewStyles {...props} ref={tableRef}>
|
||||
<TableCollection
|
||||
getTableProps={getTableProps}
|
||||
getTableBodyProps={getTableBodyProps}
|
||||
prepareRow={prepareRow}
|
||||
headerGroups={headerGroups}
|
||||
rows={content}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
setSortBy={setSortBy}
|
||||
size={size}
|
||||
columnsForWrapText={columnsForWrapText}
|
||||
isPaginationSticky={props.isPaginationSticky}
|
||||
showRowCount={showRowCount}
|
||||
{...paginationProps}
|
||||
/>
|
||||
{isEmpty && (
|
||||
<EmptyWrapperComponent>
|
||||
{noDataText ? (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={noDataText}
|
||||
/>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
</PaginationStyles>
|
||||
</EmptyWrapperComponent>
|
||||
)}
|
||||
</>
|
||||
</TableViewStyles>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* 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 { fireEvent, render } from '@superset-ui/core/spec';
|
||||
import Tabs, { EditableTabs, LineEditableTabs } from './Tabs';
|
||||
|
||||
describe('Tabs', () => {
|
||||
const defaultItems = [
|
||||
{
|
||||
key: '1',
|
||||
label: 'Tab 1',
|
||||
children: <div data-testid="tab1-content">Tab 1 content</div>,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'Tab 2',
|
||||
children: <div data-testid="tab2-content">Tab 2 content</div>,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: 'Tab 3',
|
||||
children: <div data-testid="tab3-content">Tab 3 content</div>,
|
||||
},
|
||||
];
|
||||
|
||||
describe('Basic Tabs', () => {
|
||||
it('should render tabs with default props', () => {
|
||||
const { getByText, container } = render(<Tabs items={defaultItems} />);
|
||||
|
||||
expect(getByText('Tab 1')).toBeInTheDocument();
|
||||
expect(getByText('Tab 2')).toBeInTheDocument();
|
||||
expect(getByText('Tab 3')).toBeInTheDocument();
|
||||
|
||||
const activeTabContent = container.querySelector(
|
||||
'.ant-tabs-tabpane-active',
|
||||
);
|
||||
|
||||
expect(activeTabContent).toBeDefined();
|
||||
expect(
|
||||
activeTabContent?.querySelector('[data-testid="tab1-content"]'),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render tabs component structure', () => {
|
||||
const { container } = render(<Tabs items={defaultItems} />);
|
||||
const tabsElement = container.querySelector('.ant-tabs');
|
||||
const tabsNav = container.querySelector('.ant-tabs-nav');
|
||||
const tabsContent = container.querySelector('.ant-tabs-content-holder');
|
||||
|
||||
expect(tabsElement).toBeDefined();
|
||||
expect(tabsNav).toBeDefined();
|
||||
expect(tabsContent).toBeDefined();
|
||||
});
|
||||
|
||||
it('should apply default tabBarStyle with padding', () => {
|
||||
const { container } = render(<Tabs items={defaultItems} />);
|
||||
const tabsNav = container.querySelector('.ant-tabs-nav') as HTMLElement;
|
||||
|
||||
// Check that tabBarStyle is applied (default padding is added)
|
||||
expect(tabsNav?.style?.paddingLeft).toBeDefined();
|
||||
});
|
||||
|
||||
it('should merge custom tabBarStyle with defaults', () => {
|
||||
const customStyle = { paddingRight: '20px', backgroundColor: 'red' };
|
||||
const { container } = render(
|
||||
<Tabs items={defaultItems} tabBarStyle={customStyle} />,
|
||||
);
|
||||
const tabsNav = container.querySelector('.ant-tabs-nav') as HTMLElement;
|
||||
|
||||
expect(tabsNav?.style?.paddingLeft).toBeDefined();
|
||||
expect(tabsNav?.style?.paddingRight).toBe('20px');
|
||||
expect(tabsNav?.style?.backgroundColor).toBe('red');
|
||||
});
|
||||
|
||||
it('should handle allowOverflow prop', () => {
|
||||
const { container: allowContainer } = render(
|
||||
<Tabs items={defaultItems} allowOverflow />,
|
||||
);
|
||||
const { container: disallowContainer } = render(
|
||||
<Tabs items={defaultItems} allowOverflow={false} />,
|
||||
);
|
||||
|
||||
expect(allowContainer.querySelector('.ant-tabs')).toBeDefined();
|
||||
expect(disallowContainer.querySelector('.ant-tabs')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should disable animation by default', () => {
|
||||
const { container } = render(<Tabs items={defaultItems} />);
|
||||
const tabsElement = container.querySelector('.ant-tabs');
|
||||
|
||||
expect(tabsElement?.className).not.toContain('ant-tabs-animated');
|
||||
});
|
||||
|
||||
it('should handle tab change events', () => {
|
||||
const onChangeMock = jest.fn();
|
||||
const { getByText } = render(
|
||||
<Tabs items={defaultItems} onChange={onChangeMock} />,
|
||||
);
|
||||
|
||||
fireEvent.click(getByText('Tab 2'));
|
||||
|
||||
expect(onChangeMock).toHaveBeenCalledWith('2');
|
||||
});
|
||||
|
||||
it('should pass through additional props to Antd Tabs', () => {
|
||||
const onTabClickMock = jest.fn();
|
||||
const { getByText } = render(
|
||||
<Tabs
|
||||
items={defaultItems}
|
||||
onTabClick={onTabClickMock}
|
||||
size="large"
|
||||
centered
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(getByText('Tab 2'));
|
||||
|
||||
expect(onTabClickMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('EditableTabs', () => {
|
||||
it('should render with editable features', () => {
|
||||
const { container } = render(<EditableTabs items={defaultItems} />);
|
||||
|
||||
const tabsElement = container.querySelector('.ant-tabs');
|
||||
|
||||
expect(tabsElement?.className).toContain('ant-tabs-card');
|
||||
expect(tabsElement?.className).toContain('ant-tabs-editable-card');
|
||||
});
|
||||
|
||||
it('should handle onEdit callback for add/remove actions', () => {
|
||||
const onEditMock = jest.fn();
|
||||
const itemsWithRemove = defaultItems.map(item => ({
|
||||
...item,
|
||||
closable: true,
|
||||
}));
|
||||
|
||||
const { container } = render(
|
||||
<EditableTabs items={itemsWithRemove} onEdit={onEditMock} />,
|
||||
);
|
||||
|
||||
const removeButton = container.querySelector('.ant-tabs-tab-remove');
|
||||
expect(removeButton).toBeDefined();
|
||||
|
||||
fireEvent.click(removeButton!);
|
||||
expect(onEditMock).toHaveBeenCalledWith(expect.any(String), 'remove');
|
||||
});
|
||||
|
||||
it('should have default props set correctly', () => {
|
||||
expect(EditableTabs.defaultProps?.type).toBe('editable-card');
|
||||
expect(EditableTabs.defaultProps?.animated).toEqual({
|
||||
inkBar: true,
|
||||
tabPane: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('LineEditableTabs', () => {
|
||||
it('should render as line-style editable tabs', () => {
|
||||
const { container } = render(<LineEditableTabs items={defaultItems} />);
|
||||
|
||||
const tabsElement = container.querySelector('.ant-tabs');
|
||||
|
||||
expect(tabsElement?.className).toContain('ant-tabs-card');
|
||||
expect(tabsElement?.className).toContain('ant-tabs-editable-card');
|
||||
});
|
||||
|
||||
it('should render with line-specific styling', () => {
|
||||
const { container } = render(<LineEditableTabs items={defaultItems} />);
|
||||
|
||||
const inkBar = container.querySelector('.ant-tabs-ink-bar');
|
||||
expect(inkBar).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TabPane Legacy Support', () => {
|
||||
it('should support TabPane component access', () => {
|
||||
expect(Tabs.TabPane).toBeDefined();
|
||||
expect(EditableTabs.TabPane).toBeDefined();
|
||||
expect(LineEditableTabs.TabPane).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render using legacy TabPane syntax', () => {
|
||||
const { getByText, container } = render(
|
||||
<Tabs>
|
||||
<Tabs.TabPane tab="Legacy Tab 1" key="1">
|
||||
<div data-testid="legacy-content-1">Legacy content 1</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Legacy Tab 2" key="2">
|
||||
<div data-testid="legacy-content-2">Legacy content 2</div>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>,
|
||||
);
|
||||
|
||||
expect(getByText('Legacy Tab 1')).toBeInTheDocument();
|
||||
expect(getByText('Legacy Tab 2')).toBeInTheDocument();
|
||||
|
||||
const activeTabContent = container.querySelector(
|
||||
'.ant-tabs-tabpane-active [data-testid="legacy-content-1"]',
|
||||
);
|
||||
|
||||
expect(activeTabContent).toBeDefined();
|
||||
expect(activeTabContent?.textContent).toBe('Legacy content 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty items array', () => {
|
||||
const { container } = render(<Tabs items={[]} />);
|
||||
const tabsElement = container.querySelector('.ant-tabs');
|
||||
|
||||
expect(tabsElement).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle undefined items', () => {
|
||||
const { container } = render(<Tabs />);
|
||||
const tabsElement = container.querySelector('.ant-tabs');
|
||||
|
||||
expect(tabsElement).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle tabs with no content', () => {
|
||||
const itemsWithoutContent = [
|
||||
{ key: '1', label: 'Tab 1' },
|
||||
{ key: '2', label: 'Tab 2' },
|
||||
];
|
||||
|
||||
const { getByText } = render(<Tabs items={itemsWithoutContent} />);
|
||||
|
||||
expect(getByText('Tab 1')).toBeInTheDocument();
|
||||
expect(getByText('Tab 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle allowOverflow default value', () => {
|
||||
const { container } = render(<Tabs items={defaultItems} />);
|
||||
expect(container.querySelector('.ant-tabs')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should render with proper ARIA roles', () => {
|
||||
const { container } = render(<Tabs items={defaultItems} />);
|
||||
|
||||
const tablist = container.querySelector('[role="tablist"]');
|
||||
const tabs = container.querySelectorAll('[role="tab"]');
|
||||
|
||||
expect(tablist).toBeDefined();
|
||||
expect(tabs.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should support keyboard navigation', () => {
|
||||
const { container, getByText } = render(<Tabs items={defaultItems} />);
|
||||
|
||||
const firstTab = container.querySelector('[role="tab"]');
|
||||
const secondTab = getByText('Tab 2');
|
||||
|
||||
if (firstTab) {
|
||||
fireEvent.keyDown(firstTab, { key: 'ArrowRight', code: 'ArrowRight' });
|
||||
}
|
||||
|
||||
fireEvent.click(secondTab);
|
||||
|
||||
expect(secondTab).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling Integration', () => {
|
||||
it('should accept and apply custom CSS classes', () => {
|
||||
const { container } = render(
|
||||
<Tabs items={defaultItems} className="custom-tabs-class" />,
|
||||
);
|
||||
|
||||
const tabsElement = container.querySelector('.ant-tabs');
|
||||
|
||||
expect(tabsElement?.className).toContain('custom-tabs-class');
|
||||
});
|
||||
|
||||
it('should accept and apply custom styles', () => {
|
||||
const customStyle = { minHeight: '200px' };
|
||||
const { container } = render(
|
||||
<Tabs items={defaultItems} style={customStyle} />,
|
||||
);
|
||||
|
||||
const tabsElement = container.querySelector('.ant-tabs') as HTMLElement;
|
||||
|
||||
expect(tabsElement?.style?.minHeight).toBe('200px');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -29,14 +29,18 @@ export interface TabsProps extends AntdTabsProps {
|
||||
const StyledTabs = ({
|
||||
animated = false,
|
||||
allowOverflow = true,
|
||||
tabBarStyle,
|
||||
...props
|
||||
}: TabsProps) => {
|
||||
const theme = useTheme();
|
||||
const defaultTabBarStyle = { paddingLeft: theme.sizeUnit * 4 };
|
||||
const mergedStyle = { ...defaultTabBarStyle, ...tabBarStyle };
|
||||
|
||||
return (
|
||||
<AntdTabs
|
||||
animated={animated}
|
||||
{...props}
|
||||
tabBarStyle={{ paddingLeft: theme.sizeUnit * 4 }}
|
||||
tabBarStyle={mergedStyle}
|
||||
css={theme => css`
|
||||
overflow: ${allowOverflow ? 'visible' : 'hidden'};
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ export enum FeatureFlag {
|
||||
DashboardVirtualization = 'DASHBOARD_VIRTUALIZATION',
|
||||
DashboardRbac = 'DASHBOARD_RBAC',
|
||||
DatapanelClosedByDefault = 'DATAPANEL_CLOSED_BY_DEFAULT',
|
||||
DateRangeTimeshiftsEnabled = 'DATE_RANGE_TIMESHIFTS_ENABLED',
|
||||
/** @deprecated */
|
||||
DrillToDetail = 'DRILL_TO_DETAIL',
|
||||
DrillBy = 'DRILL_BY',
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { SupersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||
|
||||
// CRITICAL: Don't import from the mocked path - import directly to avoid global mocks
|
||||
import AsyncIcon from '../../../src/components/Icons/AsyncIcon';
|
||||
|
||||
// Mock only the SVG import to prevent dynamic import issues
|
||||
jest.mock(
|
||||
'!!@svgr/webpack!../../../src/assets/images/icons/slack.svg',
|
||||
() => {
|
||||
const MockSlackSVG = (props: any) => (
|
||||
<svg {...props} viewBox="0 0 24 24" data-testid="slack-svg">
|
||||
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52z" />
|
||||
</svg>
|
||||
);
|
||||
return { default: MockSlackSVG };
|
||||
},
|
||||
{ virtual: true },
|
||||
);
|
||||
|
||||
// Basic theme for testing
|
||||
const mockTheme: SupersetTheme = {
|
||||
fontSize: 16,
|
||||
sizeUnit: 4,
|
||||
} as SupersetTheme;
|
||||
|
||||
describe('AsyncIcon Integration Tests (Real Component)', () => {
|
||||
it('should have data-test and aria-label attributes with real component', () => {
|
||||
const { container } = render(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<AsyncIcon customIcons fileName="slack" iconSize="l" />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
// Don't wait for SVG since it's mocked - just check the span wrapper
|
||||
const spanElement = container.querySelector('span');
|
||||
|
||||
// Test the ACTUAL component behavior (not the mock)
|
||||
expect(spanElement).toHaveAttribute('aria-label', 'slack');
|
||||
expect(spanElement).toHaveAttribute('role', 'img');
|
||||
expect(spanElement).toHaveAttribute('data-test', 'slack');
|
||||
});
|
||||
|
||||
it('should always have aria-label and data-test for testing', () => {
|
||||
const { container } = render(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<AsyncIcon customIcons fileName="slack" iconSize="l" />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
const spanElement = container.querySelector('span');
|
||||
|
||||
// The critical requirement: we MUST have these attributes for accessibility and testing
|
||||
expect(spanElement).toHaveAttribute('aria-label');
|
||||
expect(spanElement).toHaveAttribute('data-test');
|
||||
|
||||
// The values should be consistent
|
||||
const ariaLabel = spanElement?.getAttribute('aria-label');
|
||||
const dataTest = spanElement?.getAttribute('data-test');
|
||||
expect(ariaLabel).toBe('slack');
|
||||
expect(dataTest).toBe('slack');
|
||||
});
|
||||
|
||||
it('should set role to button when onClick is provided in real component', () => {
|
||||
const onClick = jest.fn();
|
||||
const { container } = render(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<AsyncIcon
|
||||
customIcons
|
||||
fileName="slack"
|
||||
iconSize="l"
|
||||
onClick={onClick}
|
||||
/>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
const spanElement = container.querySelector('span');
|
||||
|
||||
expect(spanElement).toHaveAttribute('role', 'button');
|
||||
expect(spanElement).toHaveAttribute('aria-label', 'slack');
|
||||
expect(spanElement).toHaveAttribute('data-test', 'slack');
|
||||
|
||||
// Verify onClick handler actually works
|
||||
fireEvent.click(spanElement!);
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle complex fileName patterns like BaseIcon', () => {
|
||||
const { container } = render(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<AsyncIcon customIcons fileName="slack_notification" iconSize="l" />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
const spanElement = container.querySelector('span');
|
||||
|
||||
// Should follow BaseIcon's genAriaLabel logic:
|
||||
// fileName="slack_notification" -> name="slack-notification" -> "slack-notification" (not just "slack")
|
||||
expect(spanElement).toHaveAttribute('aria-label', 'slack-notification');
|
||||
expect(spanElement).toHaveAttribute('data-test', 'slack-notification');
|
||||
});
|
||||
});
|
||||
@@ -58,11 +58,13 @@ export default styled(CountryMap)`
|
||||
}
|
||||
|
||||
.superset-legacy-chart-country-map text.result-text {
|
||||
fill: ${theme.colorText};
|
||||
font-weight: ${theme.fontWeightLight};
|
||||
font-size: ${theme.fontSizeXL}px;
|
||||
}
|
||||
|
||||
.superset-legacy-chart-country-map text.big-text {
|
||||
fill: ${theme.colorText};
|
||||
font-weight: ${theme.fontWeightStrong};
|
||||
font-size: ${theme.fontSizeLG}px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { render, screen } from '@testing-library/react';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import '@testing-library/jest-dom';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||
import DeckGLPolygon, { getPoints } from './Polygon';
|
||||
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
|
||||
import * as utils from '../../utils';
|
||||
|
||||
// Mock the utils functions
|
||||
const mockGetBuckets = jest.spyOn(utils, 'getBuckets');
|
||||
const mockGetColorBreakpointsBuckets = jest.spyOn(
|
||||
utils,
|
||||
'getColorBreakpointsBuckets',
|
||||
);
|
||||
|
||||
// Mock DeckGL container and Legend
|
||||
jest.mock('../../DeckGLContainer', () => ({
|
||||
DeckGLContainerStyledWrapper: ({ children }: any) => (
|
||||
<div data-testid="deckgl-container">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../components/Legend', () => ({ categories, position }: any) => (
|
||||
<div
|
||||
data-testid="legend"
|
||||
data-categories={JSON.stringify(categories)}
|
||||
data-position={position}
|
||||
>
|
||||
Legend Mock
|
||||
</div>
|
||||
));
|
||||
|
||||
const mockProps = {
|
||||
formData: {
|
||||
// Required QueryFormData properties
|
||||
datasource: 'test_datasource',
|
||||
viz_type: 'deck_polygon',
|
||||
// Polygon-specific properties
|
||||
metric: { label: 'population' },
|
||||
color_scheme_type: COLOR_SCHEME_TYPES.linear_palette,
|
||||
legend_position: 'tr',
|
||||
legend_format: '.2f',
|
||||
autozoom: false,
|
||||
mapbox_style: 'mapbox://styles/mapbox/light-v9',
|
||||
opacity: 80,
|
||||
filled: true,
|
||||
stroked: true,
|
||||
extruded: false,
|
||||
line_width: 1,
|
||||
line_width_unit: 'pixels',
|
||||
multiplier: 1,
|
||||
break_points: [],
|
||||
num_buckets: '5',
|
||||
linear_color_scheme: 'blue_white_yellow',
|
||||
},
|
||||
payload: {
|
||||
data: {
|
||||
features: [
|
||||
{
|
||||
population: 100000,
|
||||
polygon: [
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
[1, 1],
|
||||
[0, 1],
|
||||
],
|
||||
},
|
||||
{
|
||||
population: 200000,
|
||||
polygon: [
|
||||
[2, 2],
|
||||
[3, 2],
|
||||
[3, 3],
|
||||
[2, 3],
|
||||
],
|
||||
},
|
||||
],
|
||||
mapboxApiKey: 'test-key',
|
||||
},
|
||||
form_data: {},
|
||||
},
|
||||
setControlValue: jest.fn(),
|
||||
viewport: { longitude: 0, latitude: 0, zoom: 1 },
|
||||
onAddFilter: jest.fn(),
|
||||
width: 800,
|
||||
height: 600,
|
||||
onContextMenu: jest.fn(),
|
||||
setDataMask: jest.fn(),
|
||||
filterState: undefined,
|
||||
emitCrossFilters: false,
|
||||
};
|
||||
|
||||
describe('DeckGLPolygon bucket generation logic', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockGetBuckets.mockReturnValue({
|
||||
'100000 - 150000': { color: [0, 100, 200], enabled: true },
|
||||
'150000 - 200000': { color: [50, 150, 250], enabled: true },
|
||||
});
|
||||
mockGetColorBreakpointsBuckets.mockReturnValue({});
|
||||
});
|
||||
|
||||
const renderWithTheme = (component: React.ReactElement) =>
|
||||
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
|
||||
|
||||
test('should use getBuckets for linear_palette color scheme', () => {
|
||||
const propsWithLinearPalette = {
|
||||
...mockProps,
|
||||
formData: {
|
||||
...mockProps.formData,
|
||||
color_scheme_type: COLOR_SCHEME_TYPES.linear_palette,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithTheme(<DeckGLPolygon {...propsWithLinearPalette} />);
|
||||
|
||||
// Should call getBuckets, not getColorBreakpointsBuckets
|
||||
expect(mockGetBuckets).toHaveBeenCalled();
|
||||
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should use getBuckets for fixed_color color scheme', () => {
|
||||
const propsWithFixedColor = {
|
||||
...mockProps,
|
||||
formData: {
|
||||
...mockProps.formData,
|
||||
color_scheme_type: COLOR_SCHEME_TYPES.fixed_color,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithTheme(<DeckGLPolygon {...propsWithFixedColor} />);
|
||||
|
||||
// Should call getBuckets, not getColorBreakpointsBuckets
|
||||
expect(mockGetBuckets).toHaveBeenCalled();
|
||||
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should use getColorBreakpointsBuckets for color_breakpoints scheme', () => {
|
||||
const propsWithBreakpoints = {
|
||||
...mockProps,
|
||||
formData: {
|
||||
...mockProps.formData,
|
||||
color_scheme_type: COLOR_SCHEME_TYPES.color_breakpoints,
|
||||
color_breakpoints: [
|
||||
{
|
||||
minValue: 0,
|
||||
maxValue: 100000,
|
||||
color: { r: 255, g: 0, b: 0, a: 100 },
|
||||
},
|
||||
{
|
||||
minValue: 100001,
|
||||
maxValue: 200000,
|
||||
color: { r: 0, g: 255, b: 0, a: 100 },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
mockGetColorBreakpointsBuckets.mockReturnValue({
|
||||
'0 - 100000': { color: [255, 0, 0], enabled: true },
|
||||
'100001 - 200000': { color: [0, 255, 0], enabled: true },
|
||||
});
|
||||
|
||||
renderWithTheme(<DeckGLPolygon {...propsWithBreakpoints} />);
|
||||
|
||||
// Should call getColorBreakpointsBuckets, not getBuckets
|
||||
expect(mockGetColorBreakpointsBuckets).toHaveBeenCalled();
|
||||
expect(mockGetBuckets).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should use getBuckets when color_scheme_type is undefined (backward compatibility)', () => {
|
||||
const propsWithUndefinedScheme = {
|
||||
...mockProps,
|
||||
formData: {
|
||||
...mockProps.formData,
|
||||
color_scheme_type: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithTheme(<DeckGLPolygon {...propsWithUndefinedScheme} />);
|
||||
|
||||
// Should call getBuckets for backward compatibility
|
||||
expect(mockGetBuckets).toHaveBeenCalled();
|
||||
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should use getBuckets for unsupported color schemes (categorical_palette)', () => {
|
||||
const propsWithUnsupportedScheme = {
|
||||
...mockProps,
|
||||
formData: {
|
||||
...mockProps.formData,
|
||||
color_scheme_type: COLOR_SCHEME_TYPES.categorical_palette,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithTheme(<DeckGLPolygon {...propsWithUnsupportedScheme} />);
|
||||
|
||||
// Should fall back to getBuckets for unsupported color schemes
|
||||
expect(mockGetBuckets).toHaveBeenCalled();
|
||||
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeckGLPolygon Error Handling and Edge Cases', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockGetBuckets.mockReturnValue({});
|
||||
mockGetColorBreakpointsBuckets.mockReturnValue({});
|
||||
});
|
||||
|
||||
const renderWithTheme = (component: React.ReactElement) =>
|
||||
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
|
||||
|
||||
test('handles empty features data gracefully', () => {
|
||||
const propsWithEmptyData = {
|
||||
...mockProps,
|
||||
payload: {
|
||||
...mockProps.payload,
|
||||
data: {
|
||||
...mockProps.payload.data,
|
||||
features: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithTheme(<DeckGLPolygon {...propsWithEmptyData} />);
|
||||
|
||||
// Should still call getBuckets with empty data
|
||||
expect(mockGetBuckets).toHaveBeenCalled();
|
||||
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('handles missing color_breakpoints for color_breakpoints scheme', () => {
|
||||
const propsWithMissingBreakpoints = {
|
||||
...mockProps,
|
||||
formData: {
|
||||
...mockProps.formData,
|
||||
color_scheme_type: COLOR_SCHEME_TYPES.color_breakpoints,
|
||||
color_breakpoints: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithTheme(<DeckGLPolygon {...propsWithMissingBreakpoints} />);
|
||||
|
||||
// Should call getColorBreakpointsBuckets even with undefined breakpoints
|
||||
expect(mockGetColorBreakpointsBuckets).toHaveBeenCalledWith(undefined);
|
||||
expect(mockGetBuckets).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('handles null legend_position correctly', () => {
|
||||
const propsWithNullLegendPosition = {
|
||||
...mockProps,
|
||||
formData: {
|
||||
...mockProps.formData,
|
||||
legend_position: null,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithTheme(<DeckGLPolygon {...propsWithNullLegendPosition} />);
|
||||
|
||||
// Legend should not be rendered when position is null
|
||||
expect(screen.queryByTestId('legend')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeckGLPolygon Legend Integration', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockGetBuckets.mockReturnValue({
|
||||
'100000 - 150000': { color: [0, 100, 200], enabled: true },
|
||||
'150000 - 200000': { color: [50, 150, 250], enabled: true },
|
||||
});
|
||||
});
|
||||
|
||||
const renderWithTheme = (component: React.ReactElement) =>
|
||||
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
|
||||
|
||||
test('renders legend with non-empty categories when metric and linear_palette are defined', () => {
|
||||
const { container } = renderWithTheme(<DeckGLPolygon {...mockProps} />);
|
||||
|
||||
// Verify the component renders and calls the correct bucket function
|
||||
expect(mockGetBuckets).toHaveBeenCalled();
|
||||
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
|
||||
|
||||
// Verify the legend mock was rendered with non-empty categories
|
||||
const legendElement = container.querySelector('[data-testid="legend"]');
|
||||
expect(legendElement).toBeTruthy();
|
||||
const categoriesAttr = legendElement?.getAttribute('data-categories');
|
||||
const categoriesData = JSON.parse(categoriesAttr || '{}');
|
||||
expect(Object.keys(categoriesData)).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('does not render legend when metric is null', () => {
|
||||
const propsWithoutMetric = {
|
||||
...mockProps,
|
||||
formData: {
|
||||
...mockProps.formData,
|
||||
metric: null,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithTheme(<DeckGLPolygon {...propsWithoutMetric} />);
|
||||
|
||||
// Legend should not be rendered when no metric is defined
|
||||
expect(screen.queryByTestId('legend')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPoints utility', () => {
|
||||
test('extracts points from polygon data', () => {
|
||||
const data = [
|
||||
{
|
||||
polygon: [
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
[1, 1],
|
||||
[0, 1],
|
||||
],
|
||||
},
|
||||
{
|
||||
polygon: [
|
||||
[2, 2],
|
||||
[3, 2],
|
||||
[3, 3],
|
||||
[2, 3],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const points = getPoints(data);
|
||||
|
||||
expect(points).toHaveLength(8); // 4 points per polygon * 2 polygons
|
||||
expect(points[0]).toEqual([0, 0]);
|
||||
expect(points[4]).toEqual([2, 2]);
|
||||
});
|
||||
});
|
||||
@@ -336,9 +336,10 @@ const DeckGLPolygon = (props: DeckGLPolygonProps) => {
|
||||
const accessor = (d: JsonObject) => d[metricLabel];
|
||||
|
||||
const colorSchemeType = formData.color_scheme_type;
|
||||
const buckets = colorSchemeType
|
||||
? getColorBreakpointsBuckets(formData.color_breakpoints)
|
||||
: getBuckets(formData, payload.data.features, accessor);
|
||||
const buckets =
|
||||
colorSchemeType === COLOR_SCHEME_TYPES.color_breakpoints
|
||||
? getColorBreakpointsBuckets(formData.color_breakpoints)
|
||||
: getBuckets(formData, payload.data.features, accessor);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
|
||||
@@ -72,6 +72,12 @@ export default styled(NVD3)`
|
||||
text.nv-axislabel {
|
||||
font-size: ${({ theme }) => theme.fontSize} !important;
|
||||
}
|
||||
g.nv-axis text {
|
||||
fill: ${({ theme }) => theme.colorText};
|
||||
}
|
||||
g.nv-series text {
|
||||
fill: ${({ theme }) => theme.colorText};
|
||||
}
|
||||
g.solid path,
|
||||
line.solid {
|
||||
stroke-dasharray: unset;
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
getXAxisLabel,
|
||||
Metric,
|
||||
getValueFormatter,
|
||||
supersetTheme,
|
||||
t,
|
||||
tooltipHtml,
|
||||
} from '@superset-ui/core';
|
||||
@@ -281,7 +280,7 @@ export default function transformProps(
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: supersetTheme.colorBgContainer,
|
||||
color: 'transparent',
|
||||
},
|
||||
]),
|
||||
},
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import { t } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
ControlStateMapping,
|
||||
ControlSubSectionHeader,
|
||||
D3_FORMAT_DOCS,
|
||||
D3_FORMAT_OPTIONS,
|
||||
@@ -197,15 +196,6 @@ const config: ControlPanelConfig = {
|
||||
],
|
||||
},
|
||||
],
|
||||
onInit(state: ControlStateMapping) {
|
||||
return {
|
||||
...state,
|
||||
row_limit: {
|
||||
...state.row_limit,
|
||||
value: state.row_limit.default,
|
||||
},
|
||||
};
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
metric: getStandardizedControls().shiftMetric(),
|
||||
|
||||
@@ -324,6 +324,7 @@ export default function transformProps(chartProps: EchartsGanttChartProps) {
|
||||
show: true,
|
||||
position: 'start',
|
||||
formatter: '{b}',
|
||||
color: theme.colorText,
|
||||
},
|
||||
data: categoryLines,
|
||||
},
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
getValueFormatter,
|
||||
rgbToHex,
|
||||
addAlpha,
|
||||
supersetTheme,
|
||||
tooltipHtml,
|
||||
} from '@superset-ui/core';
|
||||
import memoizeOne from 'memoize-one';
|
||||
@@ -78,7 +77,8 @@ export default function transformProps(
|
||||
chartProps: HeatmapChartProps,
|
||||
): HeatmapTransformedProps {
|
||||
const refs: Refs = {};
|
||||
const { width, height, formData, queriesData, datasource } = chartProps;
|
||||
const { width, height, formData, queriesData, datasource, theme } =
|
||||
chartProps;
|
||||
const {
|
||||
bottomMargin,
|
||||
xAxis,
|
||||
@@ -176,9 +176,9 @@ export default function transformProps(
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
borderColor: supersetTheme.colorBgContainer,
|
||||
borderColor: 'transparent',
|
||||
shadowBlur: 10,
|
||||
shadowColor: supersetTheme.colorTextBase,
|
||||
shadowColor: addAlpha(theme.colorText, 0.3),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -129,6 +129,7 @@ export default function transformProps(
|
||||
theme,
|
||||
inContextMenu,
|
||||
emitCrossFilters,
|
||||
legendState,
|
||||
} = chartProps;
|
||||
|
||||
let focusedSeries: string | null = null;
|
||||
@@ -157,6 +158,7 @@ export default function transformProps(
|
||||
timeShiftColor,
|
||||
contributionMode,
|
||||
legendOrientation,
|
||||
legendMargin,
|
||||
legendType,
|
||||
logAxis,
|
||||
logAxisSecondary,
|
||||
@@ -243,6 +245,10 @@ export default function transformProps(
|
||||
const MetricDisplayNameA = getMetricDisplayName(metrics[0], verboseMap);
|
||||
const MetricDisplayNameB = getMetricDisplayName(metricsB[0], verboseMap);
|
||||
|
||||
const dataTypes = getColtypesMapping(queriesData[0]);
|
||||
const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig];
|
||||
const xAxisType = getAxisType(stack, xAxisForceCategorical, xAxisDataType);
|
||||
|
||||
const [rawSeriesA, sortedTotalValuesA] = extractSeries(rebasedDataA, {
|
||||
fillNeighborValue: stack ? 0 : undefined,
|
||||
xAxis: xAxisLabel,
|
||||
@@ -250,6 +256,7 @@ export default function transformProps(
|
||||
sortSeriesAscending,
|
||||
stack,
|
||||
totalStackedValues,
|
||||
xAxisType,
|
||||
});
|
||||
const rebasedDataB = rebaseForecastDatum(data2, verboseMap);
|
||||
const {
|
||||
@@ -267,11 +274,8 @@ export default function transformProps(
|
||||
sortSeriesAscending: sortSeriesAscendingB,
|
||||
stack: Boolean(stackB),
|
||||
totalStackedValues: totalStackedValuesB,
|
||||
xAxisType,
|
||||
});
|
||||
|
||||
const dataTypes = getColtypesMapping(queriesData[0]);
|
||||
const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig];
|
||||
const xAxisType = getAxisType(stack, xAxisForceCategorical, xAxisDataType);
|
||||
const series: SeriesOption[] = [];
|
||||
const formatter = contributionMode
|
||||
? getNumberFormatter(',.0%')
|
||||
@@ -421,6 +425,7 @@ export default function transformProps(
|
||||
{
|
||||
...entry,
|
||||
id: `${displayName || ''}`,
|
||||
name: `${displayName || ''}`,
|
||||
},
|
||||
colorScale,
|
||||
colorScaleKey,
|
||||
@@ -450,6 +455,7 @@ export default function transformProps(
|
||||
showValueIndexes: showValueIndexesA,
|
||||
thresholdValues,
|
||||
timeShiftColor,
|
||||
theme,
|
||||
},
|
||||
);
|
||||
if (transformedSeries) series.push(transformedSeries);
|
||||
@@ -486,6 +492,7 @@ export default function transformProps(
|
||||
{
|
||||
...entry,
|
||||
id: `${displayName || ''}`,
|
||||
name: `${displayName || ''}`,
|
||||
},
|
||||
|
||||
colorScale,
|
||||
@@ -516,6 +523,7 @@ export default function transformProps(
|
||||
showValueIndexes: showValueIndexesB,
|
||||
thresholdValues: thresholdValuesB,
|
||||
timeShiftColor,
|
||||
theme,
|
||||
},
|
||||
);
|
||||
if (transformedSeries) series.push(transformedSeries);
|
||||
@@ -546,7 +554,7 @@ export default function transformProps(
|
||||
legendOrientation,
|
||||
addYAxisTitleOffset,
|
||||
zoomable,
|
||||
null,
|
||||
legendMargin,
|
||||
addXAxisTitleOffset,
|
||||
yAxisTitlePosition,
|
||||
convertInteger(yAxisTitleMargin),
|
||||
@@ -709,6 +717,8 @@ export default function transformProps(
|
||||
showLegend,
|
||||
theme,
|
||||
zoomable,
|
||||
legendState,
|
||||
chartPadding,
|
||||
),
|
||||
// @ts-ignore
|
||||
data: series
|
||||
|
||||
@@ -47,7 +47,10 @@ import {
|
||||
isDerivedSeries,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import type { EChartsCoreOption } from 'echarts/core';
|
||||
import type { LineStyleOption } from 'echarts/types/src/util/types';
|
||||
import type {
|
||||
LineStyleOption,
|
||||
CallbackDataParams,
|
||||
} from 'echarts/types/src/util/types';
|
||||
import type { SeriesOption } from 'echarts';
|
||||
import {
|
||||
EchartsTimeseriesChartProps,
|
||||
@@ -233,6 +236,8 @@ export default function transformProps(
|
||||
);
|
||||
|
||||
const isMultiSeries = groupBy.length || metrics?.length > 1;
|
||||
const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig];
|
||||
const xAxisType = getAxisType(stack, xAxisForceCategorical, xAxisDataType);
|
||||
|
||||
const [rawSeries, sortedTotalValues, minPositiveValue] = extractSeries(
|
||||
rebasedData,
|
||||
@@ -247,6 +252,7 @@ export default function transformProps(
|
||||
sortSeriesAscending,
|
||||
xAxisSortSeries: isMultiSeries ? xAxisSort : undefined,
|
||||
xAxisSortSeriesAscending: isMultiSeries ? xAxisSortAsc : undefined,
|
||||
xAxisType,
|
||||
},
|
||||
);
|
||||
const showValueIndexes = extractShowValueIndexes(rawSeries, {
|
||||
@@ -259,9 +265,6 @@ export default function transformProps(
|
||||
rawSeries.map(series => series.name as string),
|
||||
);
|
||||
const isAreaExpand = stack === StackControlsValue.Expand;
|
||||
const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig];
|
||||
|
||||
const xAxisType = getAxisType(stack, xAxisForceCategorical, xAxisDataType);
|
||||
const series: SeriesOption[] = [];
|
||||
|
||||
const forcePercentFormatter = Boolean(contributionMode || isAreaExpand);
|
||||
@@ -331,6 +334,7 @@ export default function transformProps(
|
||||
lineStyle,
|
||||
timeCompare: array,
|
||||
timeShiftColor,
|
||||
theme,
|
||||
},
|
||||
);
|
||||
if (transformedSeries) {
|
||||
@@ -566,16 +570,31 @@ export default function transformProps(
|
||||
const xValue: number = richTooltip
|
||||
? params[0].value[xIndex]
|
||||
: params.value[xIndex];
|
||||
const forecastValue: any[] = richTooltip ? params : [params];
|
||||
const forecastValue: CallbackDataParams[] = richTooltip
|
||||
? params
|
||||
: [params];
|
||||
const sortedKeys = extractTooltipKeys(
|
||||
forecastValue,
|
||||
yIndex,
|
||||
richTooltip,
|
||||
tooltipSortByMetric,
|
||||
);
|
||||
const filteredForecastValue = forecastValue.filter(
|
||||
(item: CallbackDataParams) =>
|
||||
!annotationLayers.some(
|
||||
(annotation: AnnotationLayer) =>
|
||||
item.seriesName === annotation.name,
|
||||
),
|
||||
);
|
||||
const forecastValues: Record<string, ForecastValue> =
|
||||
extractForecastValuesFromTooltipParams(forecastValue, isHorizontal);
|
||||
|
||||
const filteredForecastValues: Record<string, ForecastValue> =
|
||||
extractForecastValuesFromTooltipParams(
|
||||
filteredForecastValue,
|
||||
isHorizontal,
|
||||
);
|
||||
|
||||
const isForecast = Object.values(forecastValues).some(
|
||||
value =>
|
||||
value.forecastTrend || value.forecastLower || value.forecastUpper,
|
||||
@@ -586,7 +605,7 @@ export default function transformProps(
|
||||
: (getCustomFormatter(customFormatters, metrics) ?? defaultFormatter);
|
||||
|
||||
const rows: string[][] = [];
|
||||
const total = Object.values(forecastValues).reduce(
|
||||
const total = Object.values(filteredForecastValues).reduce(
|
||||
(acc, value) =>
|
||||
value.observation !== undefined ? acc + value.observation : acc,
|
||||
0,
|
||||
@@ -608,7 +627,16 @@ export default function transformProps(
|
||||
seriesName: key,
|
||||
formatter,
|
||||
});
|
||||
if (showPercentage && value.observation !== undefined) {
|
||||
|
||||
const annotationRow = annotationLayers.some(
|
||||
item => item.name === key,
|
||||
);
|
||||
|
||||
if (
|
||||
showPercentage &&
|
||||
value.observation !== undefined &&
|
||||
!annotationRow
|
||||
) {
|
||||
row.push(
|
||||
percentFormatter.format(value.observation / (total || 1)),
|
||||
);
|
||||
|
||||
@@ -168,6 +168,7 @@ export function transformSeries(
|
||||
queryIndex?: number;
|
||||
timeCompare?: string[];
|
||||
timeShiftColor?: boolean;
|
||||
theme?: SupersetTheme;
|
||||
},
|
||||
): SeriesOption | undefined {
|
||||
const { name, data } = series;
|
||||
@@ -197,6 +198,7 @@ export function transformSeries(
|
||||
queryIndex = 0,
|
||||
timeCompare = [],
|
||||
timeShiftColor,
|
||||
theme,
|
||||
} = opts;
|
||||
const contexts = seriesContexts[name || ''] || [];
|
||||
const hasForecast =
|
||||
@@ -323,6 +325,8 @@ export function transformSeries(
|
||||
label: {
|
||||
show: !!showValue,
|
||||
position: isHorizontal ? 'right' : 'top',
|
||||
color: theme?.colorText,
|
||||
textBorderWidth: 0,
|
||||
formatter: (params: any) => {
|
||||
// don't show confidence band value labels, as they're already visible on the tooltip
|
||||
if (
|
||||
|
||||
@@ -54,7 +54,9 @@ const getCrossFilterDataMask =
|
||||
values = [value];
|
||||
}
|
||||
|
||||
const groupbyValues = values.map(value => labelMap[value]);
|
||||
const groupbyValues = values
|
||||
.map(value => labelMap[value])
|
||||
.filter(Boolean) as string[][];
|
||||
|
||||
return {
|
||||
dataMask: {
|
||||
@@ -122,6 +124,9 @@ export const contextMenuEventHandler =
|
||||
const drillFilters: BinaryQueryObjectFilterClause[] = [];
|
||||
if (groupby.length > 0) {
|
||||
const values = labelMap[e.name];
|
||||
if (!values) {
|
||||
return;
|
||||
}
|
||||
groupby.forEach((dimension, i) => {
|
||||
drillFilters.push({
|
||||
col: dimension,
|
||||
|
||||
@@ -272,6 +272,7 @@ export function extractSeries(
|
||||
sortSeriesAscending?: boolean;
|
||||
xAxisSortSeries?: SortSeriesType;
|
||||
xAxisSortSeriesAscending?: boolean;
|
||||
xAxisType?: AxisType;
|
||||
} = {},
|
||||
): [SeriesOption[], number[], number | undefined] {
|
||||
const {
|
||||
@@ -286,11 +287,15 @@ export function extractSeries(
|
||||
sortSeriesAscending,
|
||||
xAxisSortSeries,
|
||||
xAxisSortSeriesAscending,
|
||||
xAxisType,
|
||||
} = opts;
|
||||
if (data.length === 0) return [[], [], undefined];
|
||||
const rows: DataRecord[] = data.map(datum => ({
|
||||
...datum,
|
||||
[xAxis]: datum[xAxis],
|
||||
[xAxis]:
|
||||
datum[xAxis] === null && xAxisType === AxisType.Category
|
||||
? NULL_STRING
|
||||
: datum[xAxis],
|
||||
}));
|
||||
const sortedSeries = sortAndFilterSeries(
|
||||
rows,
|
||||
|
||||
@@ -256,6 +256,7 @@ describe('Gantt transformProps', () => {
|
||||
show: true,
|
||||
position: 'start',
|
||||
formatter: '{b}',
|
||||
color: 'rgba(0,0,0,0.88)',
|
||||
},
|
||||
lineStyle: expect.objectContaining({
|
||||
color: '#00000000',
|
||||
|
||||
@@ -137,6 +137,24 @@ it('should transform chart props for viz with showQueryIdentifiers=false', () =>
|
||||
expect(seriesIds).not.toContain('sum__num (Query A), boy');
|
||||
expect(seriesIds).not.toContain('sum__num (Query B), girl');
|
||||
expect(seriesIds).not.toContain('sum__num (Query B), boy');
|
||||
|
||||
// Check that series name include query identifiers
|
||||
const seriesName = (transformed.echartOptions.series as any[]).map(
|
||||
(s: any) => s.name,
|
||||
);
|
||||
expect(seriesName).toContain('sum__num, girl');
|
||||
expect(seriesName).toContain('sum__num, boy');
|
||||
expect(seriesName).not.toContain('sum__num (Query A), girl');
|
||||
expect(seriesName).not.toContain('sum__num (Query A), boy');
|
||||
expect(seriesName).not.toContain('sum__num (Query B), girl');
|
||||
expect(seriesName).not.toContain('sum__num (Query B), boy');
|
||||
|
||||
expect((transformed.echartOptions.legend as any).data).toEqual([
|
||||
'sum__num, girl',
|
||||
'sum__num, boy',
|
||||
'sum__num, girl',
|
||||
'sum__num, boy',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should transform chart props for viz with showQueryIdentifiers=true', () => {
|
||||
@@ -160,4 +178,85 @@ it('should transform chart props for viz with showQueryIdentifiers=true', () =>
|
||||
expect(seriesIds).toContain('sum__num (Query B), boy');
|
||||
expect(seriesIds).not.toContain('sum__num, girl');
|
||||
expect(seriesIds).not.toContain('sum__num, boy');
|
||||
|
||||
// Check that series name include query identifiers
|
||||
const seriesName = (transformed.echartOptions.series as any[]).map(
|
||||
(s: any) => s.name,
|
||||
);
|
||||
expect(seriesName).toContain('sum__num (Query A), girl');
|
||||
expect(seriesName).toContain('sum__num (Query A), boy');
|
||||
expect(seriesName).toContain('sum__num (Query B), girl');
|
||||
expect(seriesName).toContain('sum__num (Query B), boy');
|
||||
expect(seriesName).not.toContain('sum__num, girl');
|
||||
expect(seriesName).not.toContain('sum__num, boy');
|
||||
|
||||
expect((transformed.echartOptions.legend as any).data).toEqual([
|
||||
'sum__num (Query A), girl',
|
||||
'sum__num (Query A), boy',
|
||||
'sum__num (Query B), girl',
|
||||
'sum__num (Query B), boy',
|
||||
]);
|
||||
});
|
||||
|
||||
it('legend margin: top orientation sets grid.top correctly', () => {
|
||||
const chartPropsConfigWithoutIdentifiers = {
|
||||
...chartPropsConfig,
|
||||
formData: {
|
||||
...formData,
|
||||
legendMargin: 250,
|
||||
showLegend: true,
|
||||
},
|
||||
};
|
||||
const chartProps = new ChartProps(chartPropsConfigWithoutIdentifiers);
|
||||
const transformed = transformProps(chartProps as EchartsMixedTimeseriesProps);
|
||||
|
||||
expect((transformed.echartOptions.grid as any).top).toEqual(270);
|
||||
});
|
||||
|
||||
it('legend margin: bottom orientation sets grid.bottom correctly', () => {
|
||||
const chartPropsConfigWithoutIdentifiers = {
|
||||
...chartPropsConfig,
|
||||
formData: {
|
||||
...formData,
|
||||
legendMargin: 250,
|
||||
showLegend: true,
|
||||
legendOrientation: LegendOrientation.Bottom,
|
||||
},
|
||||
};
|
||||
const chartProps = new ChartProps(chartPropsConfigWithoutIdentifiers);
|
||||
const transformed = transformProps(chartProps as EchartsMixedTimeseriesProps);
|
||||
|
||||
expect((transformed.echartOptions.grid as any).bottom).toEqual(270);
|
||||
});
|
||||
|
||||
it('legend margin: left orientation sets grid.left correctly', () => {
|
||||
const chartPropsConfigWithoutIdentifiers = {
|
||||
...chartPropsConfig,
|
||||
formData: {
|
||||
...formData,
|
||||
legendMargin: 250,
|
||||
showLegend: true,
|
||||
legendOrientation: LegendOrientation.Left,
|
||||
},
|
||||
};
|
||||
const chartProps = new ChartProps(chartPropsConfigWithoutIdentifiers);
|
||||
const transformed = transformProps(chartProps as EchartsMixedTimeseriesProps);
|
||||
|
||||
expect((transformed.echartOptions.grid as any).left).toEqual(270);
|
||||
});
|
||||
|
||||
it('legend margin: right orientation sets grid.right correctly', () => {
|
||||
const chartPropsConfigWithoutIdentifiers = {
|
||||
...chartPropsConfig,
|
||||
formData: {
|
||||
...formData,
|
||||
legendMargin: 270,
|
||||
showLegend: true,
|
||||
legendOrientation: LegendOrientation.Right,
|
||||
},
|
||||
};
|
||||
const chartProps = new ChartProps(chartPropsConfigWithoutIdentifiers);
|
||||
const transformed = transformProps(chartProps as EchartsMixedTimeseriesProps);
|
||||
|
||||
expect((transformed.echartOptions.grid as any).right).toEqual(270);
|
||||
});
|
||||
|
||||
@@ -493,6 +493,43 @@ describe('extractSeries', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should convert NULL x-values to NULL_STRING for categorical axis', () => {
|
||||
const data = [
|
||||
{
|
||||
browser: 'Firefox',
|
||||
count: 5,
|
||||
},
|
||||
{
|
||||
browser: null,
|
||||
count: 10,
|
||||
},
|
||||
{
|
||||
browser: 'Chrome',
|
||||
count: 8,
|
||||
},
|
||||
];
|
||||
expect(
|
||||
extractSeries(data, {
|
||||
xAxis: 'browser',
|
||||
xAxisType: AxisType.Category,
|
||||
}),
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
id: 'count',
|
||||
name: 'count',
|
||||
data: [
|
||||
['Firefox', 5],
|
||||
[NULL_STRING, 10],
|
||||
['Chrome', 8],
|
||||
],
|
||||
},
|
||||
],
|
||||
[],
|
||||
5,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should do missing value imputation', () => {
|
||||
const data = [
|
||||
{
|
||||
|
||||
@@ -17,23 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { debounce } from 'lodash';
|
||||
import { formatSelectOptions } from '@superset-ui/chart-controls';
|
||||
import { Constants } from '@superset-ui/core/components';
|
||||
import { t } from '@superset-ui/core';
|
||||
|
||||
export const PAGE_SIZE_OPTIONS = formatSelectOptions<number>([
|
||||
[0, t('page_size.all')],
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
10,
|
||||
20,
|
||||
50,
|
||||
100,
|
||||
200,
|
||||
]);
|
||||
|
||||
export const debounceFunc = debounce(
|
||||
(func: (val: string) => void, source: string) => func(source),
|
||||
|
||||
@@ -451,9 +451,7 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
{hasGlobalControl ? (
|
||||
<div ref={globalControlRef} className="form-inline dt-controls">
|
||||
<StyledRow className="row">
|
||||
<div
|
||||
className={renderTimeComparisonDropdown ? 'col-sm-4' : 'col-sm-5'}
|
||||
>
|
||||
<StyledSpace size="middle">
|
||||
{hasPagination ? (
|
||||
<SelectPageSize
|
||||
total={resultsSize}
|
||||
@@ -467,23 +465,17 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
onChange={setPageSize}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{searchInput ? (
|
||||
<StyledSpace
|
||||
className={
|
||||
renderTimeComparisonDropdown ? 'col-sm-7' : 'col-sm-8'
|
||||
}
|
||||
>
|
||||
{serverPagination && (
|
||||
<div className="search-select-container">
|
||||
<span className="search-by-label">Search by: </span>
|
||||
<SearchSelectDropdown
|
||||
searchOptions={searchOptions}
|
||||
value={serverPaginationData?.searchColumn || ''}
|
||||
onChange={onSearchColChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{serverPagination && (
|
||||
<div className="search-select-container">
|
||||
<span className="search-by-label">Search by: </span>
|
||||
<SearchSelectDropdown
|
||||
searchOptions={searchOptions}
|
||||
value={serverPaginationData?.searchColumn || ''}
|
||||
onChange={onSearchColChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{searchInput && (
|
||||
<GlobalFilter<D>
|
||||
searchInput={
|
||||
typeof searchInput === 'boolean' ? undefined : searchInput
|
||||
@@ -497,16 +489,11 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
serverPagination={!!serverPagination}
|
||||
rowCount={rowCount}
|
||||
/>
|
||||
</StyledSpace>
|
||||
) : null}
|
||||
{renderTimeComparisonDropdown ? (
|
||||
<div
|
||||
className="col-sm-1"
|
||||
style={{ float: 'right', marginTop: '6px' }}
|
||||
>
|
||||
{renderTimeComparisonDropdown()}
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
{renderTimeComparisonDropdown
|
||||
? renderTimeComparisonDropdown()
|
||||
: null}
|
||||
</StyledSpace>
|
||||
</StyledRow>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -89,6 +89,7 @@ import { formatColumnValue } from './utils/formatValue';
|
||||
import { PAGE_SIZE_OPTIONS, SERVER_PAGE_SIZE_OPTIONS } from './consts';
|
||||
import { updateTableOwnState } from './DataTable/utils/externalAPIs';
|
||||
import getScrollBarSize from './DataTable/utils/getScrollBarSize';
|
||||
import DateWithFormatter from './utils/DateWithFormatter';
|
||||
|
||||
type ValueRange = [number, number];
|
||||
|
||||
@@ -197,9 +198,8 @@ function SearchInput({
|
||||
<Space direction="horizontal" size={4} className="dt-global-filter">
|
||||
{t('Search')}
|
||||
<Input
|
||||
size="small"
|
||||
aria-label={t('Search %s records', count)}
|
||||
placeholder={tn('search.num_records', count)}
|
||||
placeholder={tn('%s record', '%s records...', count, count)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
@@ -937,7 +937,10 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
},
|
||||
className: [
|
||||
className,
|
||||
value == null ? 'dt-is-null' : '',
|
||||
value == null ||
|
||||
(value instanceof DateWithFormatter && value.input == null)
|
||||
? 'dt-is-null'
|
||||
: '',
|
||||
isActiveFilterValue(key, value) ? ' dt-is-active-filter' : '',
|
||||
].join(' '),
|
||||
tabIndex: 0,
|
||||
|
||||
@@ -20,7 +20,7 @@ import { formatSelectOptions } from '@superset-ui/chart-controls';
|
||||
import { t } from '@superset-ui/core';
|
||||
|
||||
export const PAGE_SIZE_OPTIONS = formatSelectOptions<number>([
|
||||
[0, t('page_size.all')],
|
||||
[0, t('All')],
|
||||
10,
|
||||
20,
|
||||
50,
|
||||
|
||||
@@ -31,5 +31,12 @@ export default class FixJSDOMEnvironment extends JSDOMEnvironment {
|
||||
this.global.Response = Response;
|
||||
this.global.AbortSignal = AbortSignal;
|
||||
this.global.AbortController = AbortController;
|
||||
|
||||
// Mock MessageChannel to prevent hanging Jest tests with rc-overflow@1.4.1
|
||||
// Forces rc-overflow to use requestAnimationFrame fallback instead
|
||||
// Can be removed when rc-overflow properly cleans up MessagePorts in test environments
|
||||
// See: https://github.com/apache/superset/pull/34871
|
||||
this.global.MessageChannel = undefined as any;
|
||||
this.global.MessagePort = undefined as any;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,18 +92,27 @@ jest.mock('@superset-ui/core/components/Icons/AsyncIcon', () => ({
|
||||
fileName,
|
||||
role,
|
||||
'aria-label': ariaLabel,
|
||||
onClick,
|
||||
...rest
|
||||
}: {
|
||||
fileName: string;
|
||||
role: string;
|
||||
'aria-label': AriaAttributes['aria-label'];
|
||||
}) => (
|
||||
<span
|
||||
role={role ?? 'img'}
|
||||
aria-label={ariaLabel || fileName.replace('_', '-')}
|
||||
{...rest}
|
||||
/>
|
||||
),
|
||||
role?: string;
|
||||
'aria-label'?: AriaAttributes['aria-label'];
|
||||
onClick?: () => void;
|
||||
}) => {
|
||||
// Simple mock that provides the essential attributes for testing
|
||||
const label = ariaLabel || fileName?.replace(/_/g, '-').toLowerCase() || '';
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<span
|
||||
role={role || (onClick ? 'button' : 'img')}
|
||||
aria-label={label}
|
||||
data-test={label}
|
||||
onClick={onClick}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
},
|
||||
StyledIcon: ({
|
||||
component: Component,
|
||||
role,
|
||||
|
||||
@@ -71,7 +71,6 @@ export const QUERY_EDITOR_SET_FUNCTION_NAMES =
|
||||
export const QUERY_EDITOR_PERSIST_HEIGHT = 'QUERY_EDITOR_PERSIST_HEIGHT';
|
||||
export const QUERY_EDITOR_TOGGLE_LEFT_BAR = 'QUERY_EDITOR_TOGGLE_LEFT_BAR';
|
||||
export const MIGRATE_QUERY_EDITOR = 'MIGRATE_QUERY_EDITOR';
|
||||
export const MIGRATE_TAB_HISTORY = 'MIGRATE_TAB_HISTORY';
|
||||
export const MIGRATE_TABLE = 'MIGRATE_TABLE';
|
||||
export const MIGRATE_QUERY = 'MIGRATE_QUERY';
|
||||
|
||||
@@ -391,7 +390,8 @@ export function runQueryFromSqlEditor(
|
||||
const query = {
|
||||
dbId: qe.dbId,
|
||||
sql: qe.selectedText || qe.sql,
|
||||
sqlEditorId: qe.id,
|
||||
sqlEditorId: qe.tabViewId ?? qe.id,
|
||||
immutableId: qe.immutableId,
|
||||
tab: qe.name,
|
||||
catalog: qe.catalog,
|
||||
schema: qe.schema,
|
||||
@@ -499,26 +499,21 @@ export function syncQueryEditor(queryEditor) {
|
||||
.then(({ json }) => {
|
||||
const newQueryEditor = {
|
||||
...queryEditor,
|
||||
id: json.id.toString(),
|
||||
inLocalStorage: false,
|
||||
loaded: true,
|
||||
tabViewId: json.id.toString(),
|
||||
};
|
||||
dispatch({
|
||||
type: MIGRATE_QUERY_EDITOR,
|
||||
oldQueryEditor: queryEditor,
|
||||
newQueryEditor,
|
||||
});
|
||||
dispatch({
|
||||
type: MIGRATE_TAB_HISTORY,
|
||||
oldId: queryEditor.id,
|
||||
newId: newQueryEditor.id,
|
||||
});
|
||||
return Promise.all([
|
||||
...localStorageTables.map(table =>
|
||||
migrateTable(table, newQueryEditor.id, dispatch),
|
||||
migrateTable(table, newQueryEditor.tabViewId, dispatch),
|
||||
),
|
||||
...localStorageQueries.map(query =>
|
||||
migrateQuery(query.id, newQueryEditor.id, dispatch),
|
||||
migrateQuery(query.id, newQueryEditor.tabViewId, dispatch),
|
||||
),
|
||||
]);
|
||||
})
|
||||
@@ -539,6 +534,7 @@ export function addQueryEditor(queryEditor) {
|
||||
const newQueryEditor = {
|
||||
...queryEditor,
|
||||
id: nanoid(11),
|
||||
immutableId: nanoid(11),
|
||||
loaded: true,
|
||||
inLocalStorage: true,
|
||||
};
|
||||
@@ -685,8 +681,9 @@ export function setTables(tableSchemas) {
|
||||
|
||||
export function fetchQueryEditor(queryEditor, displayLimit) {
|
||||
return function (dispatch) {
|
||||
const queryEditorId = queryEditor.tabViewId ?? queryEditor.id;
|
||||
SupersetClient.get({
|
||||
endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
|
||||
endpoint: encodeURI(`/tabstateview/${queryEditorId}`),
|
||||
})
|
||||
.then(({ json }) => {
|
||||
const loadedQueryEditor = {
|
||||
@@ -756,10 +753,11 @@ export function removeAllOtherQueryEditors(queryEditor) {
|
||||
|
||||
export function removeQuery(query) {
|
||||
return function (dispatch) {
|
||||
const queryEditorId = query.sqlEditorId ?? query.id;
|
||||
const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
|
||||
? SupersetClient.delete({
|
||||
endpoint: encodeURI(
|
||||
`/tabstateview/${query.sqlEditorId}/query/${query.id}`,
|
||||
`/tabstateview/${queryEditorId}/query/${query.id}`,
|
||||
),
|
||||
})
|
||||
: Promise.resolve();
|
||||
@@ -839,9 +837,10 @@ export function saveQuery(query, clientId) {
|
||||
|
||||
export const addSavedQueryToTabState =
|
||||
(queryEditor, savedQuery) => dispatch => {
|
||||
const queryEditorId = queryEditor.tabViewId ?? queryEditor.id;
|
||||
const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
|
||||
? SupersetClient.put({
|
||||
endpoint: `/tabstateview/${queryEditor.id}`,
|
||||
endpoint: `/tabstateview/${queryEditorId}`,
|
||||
postPayload: { saved_query_id: savedQuery.remoteId },
|
||||
})
|
||||
: Promise.resolve();
|
||||
@@ -889,9 +888,10 @@ export function queryEditorSetAndSaveSql(targetQueryEditor, sql, queryId) {
|
||||
const queryEditor = getUpToDateQuery(getState(), targetQueryEditor);
|
||||
// saved query and set tab state use this action
|
||||
dispatch(queryEditorSetSql(queryEditor, sql, queryId));
|
||||
const queryEditorId = queryEditor.tabViewId ?? queryEditor.id;
|
||||
if (isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)) {
|
||||
return SupersetClient.put({
|
||||
endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
|
||||
endpoint: encodeURI(`/tabstateview/${queryEditorId}`),
|
||||
postPayload: { sql, latest_query_id: queryId },
|
||||
}).catch(() =>
|
||||
dispatch(
|
||||
|
||||
@@ -441,6 +441,7 @@ describe('async actions', () => {
|
||||
queryLimit: undefined,
|
||||
maxRow: undefined,
|
||||
id: 'abcd',
|
||||
immutableId: 'abcd',
|
||||
templateParams: undefined,
|
||||
inLocalStorage: true,
|
||||
loaded: true,
|
||||
@@ -570,6 +571,7 @@ describe('async actions', () => {
|
||||
type: actions.ADD_QUERY_EDITOR,
|
||||
queryEditor: {
|
||||
...queryEditor,
|
||||
immutableId: 'abcd',
|
||||
inLocalStorage: true,
|
||||
loaded: true,
|
||||
},
|
||||
@@ -597,6 +599,7 @@ describe('async actions', () => {
|
||||
type: actions.ADD_QUERY_EDITOR,
|
||||
queryEditor: {
|
||||
id: 'abcd',
|
||||
immutableId: 'abcd',
|
||||
sql: expect.stringContaining('SELECT ...'),
|
||||
name: `Untitled Query 7`,
|
||||
dbId: defaultQueryEditor.dbId,
|
||||
@@ -753,6 +756,7 @@ describe('async actions', () => {
|
||||
queryEditor: {
|
||||
...queryEditor,
|
||||
id: 'abcd',
|
||||
immutableId: 'abcd',
|
||||
loaded: true,
|
||||
inLocalStorage: true,
|
||||
},
|
||||
@@ -1252,16 +1256,11 @@ describe('async actions', () => {
|
||||
// new qe has a different id
|
||||
newQueryEditor: {
|
||||
...oldQueryEditor,
|
||||
id: '1',
|
||||
tabViewId: '1',
|
||||
inLocalStorage: false,
|
||||
loaded: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: actions.MIGRATE_TAB_HISTORY,
|
||||
newId: '1',
|
||||
oldId: 'abcd',
|
||||
},
|
||||
{
|
||||
type: actions.MIGRATE_TABLE,
|
||||
oldTable: tables[0],
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { useRef, useEffect, FC } from 'react';
|
||||
import { useRef, useEffect, FC, useMemo } from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { logging } from '@superset-ui/core';
|
||||
@@ -69,6 +69,17 @@ const EditorAutoSync: FC = () => {
|
||||
const queryEditors = useSelector<SqlLabRootState, QueryEditor[]>(
|
||||
state => state.sqlLab.queryEditors,
|
||||
);
|
||||
const queryEditorsById = useMemo(
|
||||
() =>
|
||||
queryEditors.reduce(
|
||||
(acc, queryEditor) => {
|
||||
acc[queryEditor.id] = queryEditor;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, QueryEditor>,
|
||||
),
|
||||
[queryEditors],
|
||||
);
|
||||
const unsavedQueryEditor = useSelector<SqlLabRootState, UnsavedQueryEditor>(
|
||||
state => state.sqlLab.unsavedQueryEditor,
|
||||
);
|
||||
@@ -120,7 +131,10 @@ const EditorAutoSync: FC = () => {
|
||||
!queryEditors.find(({ id }) => id === currentQueryEditorId)
|
||||
?.inLocalStorage
|
||||
) {
|
||||
updateCurrentSqlEditor(currentQueryEditorId).then(() => {
|
||||
const queryEditorId =
|
||||
queryEditorsById[currentQueryEditorId]?.tabViewId ??
|
||||
currentQueryEditorId;
|
||||
updateCurrentSqlEditor(queryEditorId).then(() => {
|
||||
dispatch(setLastUpdatedActiveTab(currentQueryEditorId));
|
||||
});
|
||||
}
|
||||
@@ -129,7 +143,8 @@ const EditorAutoSync: FC = () => {
|
||||
const syncDeletedQueryEditor = useEffectEvent(() => {
|
||||
if (Object.keys(destroyedQueryEditors).length > 0) {
|
||||
Object.keys(destroyedQueryEditors).forEach(id => {
|
||||
deleteSqlEditor(id)
|
||||
const queryEditorId = queryEditorsById[id]?.tabViewId ?? id;
|
||||
deleteSqlEditor(queryEditorId)
|
||||
.then(() => {
|
||||
dispatch(clearDestoryedQueryEditor(id));
|
||||
})
|
||||
|
||||
@@ -20,10 +20,14 @@ import fetchMock from 'fetch-mock';
|
||||
import { FeatureFlag, isFeatureEnabled, QueryState } from '@superset-ui/core';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import QueryHistory from 'src/SqlLab/components/QueryHistory';
|
||||
import { initialState } from 'src/SqlLab/fixtures';
|
||||
import {
|
||||
initialState,
|
||||
defaultQueryEditor,
|
||||
extraQueryEditor3,
|
||||
} from 'src/SqlLab/fixtures';
|
||||
|
||||
const mockedProps = {
|
||||
queryEditorId: 123,
|
||||
queryEditorId: defaultQueryEditor.id,
|
||||
displayLimit: 1000,
|
||||
latestQueryId: 'yhMUZCGb',
|
||||
};
|
||||
@@ -77,6 +81,8 @@ const setup = (overrides = {}) => (
|
||||
<QueryHistory {...mockedProps} {...overrides} />
|
||||
);
|
||||
|
||||
afterEach(() => fetchMock.reset());
|
||||
|
||||
test('Renders an empty state for query history', () => {
|
||||
render(setup(), { useRedux: true, initialState });
|
||||
|
||||
@@ -102,3 +108,28 @@ test('fetches the query history when the persistence mode is enabled', async ()
|
||||
expect(queryResultText).toBeInTheDocument();
|
||||
isFeatureEnabledMock.mockClear();
|
||||
});
|
||||
|
||||
test('fetches the query history by the tabViewId', async () => {
|
||||
const isFeatureEnabledMock = mockedIsFeatureEnabled.mockImplementation(
|
||||
featureFlag => featureFlag === FeatureFlag.SqllabBackendPersistence,
|
||||
);
|
||||
|
||||
const editorQueryApiRoute = `glob:*/api/v1/query/?q=*sql_editor_id*${extraQueryEditor3.tabViewId}*`;
|
||||
fetchMock.get(editorQueryApiRoute, fakeApiResult);
|
||||
render(setup({ queryEditorId: extraQueryEditor3.id }), {
|
||||
useRedux: true,
|
||||
initialState: {
|
||||
...initialState,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
queryEditors: [...initialState.sqlLab.queryEditors, extraQueryEditor3],
|
||||
},
|
||||
},
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(editorQueryApiRoute).length).toBe(1),
|
||||
);
|
||||
const queryResultText = screen.getByText(fakeApiResult.result[0].rows);
|
||||
expect(queryResultText).toBeInTheDocument();
|
||||
isFeatureEnabledMock.mockClear();
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ import QueryTable from 'src/SqlLab/components/QueryTable';
|
||||
import { SqlLabRootState } from 'src/SqlLab/types';
|
||||
import { useEditorQueriesQuery } from 'src/hooks/apiResources/queries';
|
||||
import useEffectEvent from 'src/hooks/useEffectEvent';
|
||||
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
||||
|
||||
interface QueryHistoryProps {
|
||||
queryEditorId: string | number;
|
||||
@@ -63,6 +64,10 @@ const QueryHistory = ({
|
||||
displayLimit,
|
||||
latestQueryId,
|
||||
}: QueryHistoryProps) => {
|
||||
const { id, tabViewId } = useQueryEditor(String(queryEditorId), [
|
||||
'tabViewId',
|
||||
]);
|
||||
const editorId = tabViewId ?? id;
|
||||
const [ref, hasReachedBottom] = useInView({ threshold: 0 });
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const queries = useSelector(
|
||||
@@ -74,7 +79,7 @@ const QueryHistory = ({
|
||||
isLoading,
|
||||
isFetching,
|
||||
} = useEditorQueriesQuery(
|
||||
{ editorId: `${queryEditorId}`, pageIndex },
|
||||
{ editorId, pageIndex },
|
||||
{
|
||||
skip: !isFeatureEnabled(FeatureFlag.SqllabBackendPersistence),
|
||||
},
|
||||
@@ -87,12 +92,12 @@ const QueryHistory = ({
|
||||
queries,
|
||||
data.result.map(({ id }) => id),
|
||||
),
|
||||
queryEditorId,
|
||||
editorId,
|
||||
)
|
||||
.concat(data.result)
|
||||
.reverse()
|
||||
: getEditorQueries(queries, queryEditorId),
|
||||
[queries, data, queryEditorId],
|
||||
: getEditorQueries(queries, editorId),
|
||||
[queries, data, editorId],
|
||||
);
|
||||
|
||||
const loadNext = useEffectEvent(() => {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { type ReactChild } from 'react';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
@@ -45,6 +46,13 @@ jest.mock('src/components/ErrorMessage', () => ({
|
||||
ErrorMessageWithStackTrace: () => <div data-test="error-message">Error</div>,
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'react-virtualized-auto-sizer',
|
||||
() =>
|
||||
({ children }: { children: (params: { height: number }) => ReactChild }) =>
|
||||
children({ height: 500 }),
|
||||
);
|
||||
|
||||
const mockedProps = {
|
||||
cache: true,
|
||||
queryId: queries[0].id,
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
MouseEvent,
|
||||
} from 'react';
|
||||
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { pick } from 'lodash';
|
||||
@@ -116,6 +117,7 @@ const ResultContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: ${({ theme }) => theme.sizeUnit * 2}px;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const ResultlessStyles = styled.div`
|
||||
@@ -194,6 +196,7 @@ const ResultSet = ({
|
||||
'dbId',
|
||||
'tab',
|
||||
'sql',
|
||||
'executedSql',
|
||||
'sqlEditorId',
|
||||
'templateParams',
|
||||
'schema',
|
||||
@@ -221,7 +224,6 @@ const ResultSet = ({
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [cachedData, setCachedData] = useState<Record<string, unknown>[]>([]);
|
||||
const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false);
|
||||
const [alertIsOpen, setAlertIsOpen] = useState(false);
|
||||
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
@@ -256,14 +258,6 @@ const ResultSet = ({
|
||||
}
|
||||
}, [query, cache]);
|
||||
|
||||
const calculateAlertRefHeight = (alertElement: HTMLElement | null) => {
|
||||
if (alertElement) {
|
||||
setAlertIsOpen(true);
|
||||
} else {
|
||||
setAlertIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const popSelectStar = (tempSchema: string | null, tempTable: string) => {
|
||||
const qe = {
|
||||
id: nanoid(11),
|
||||
@@ -471,10 +465,10 @@ const ResultSet = ({
|
||||
return (
|
||||
<>
|
||||
{!limitReached && shouldUseDefaultDropdownAlert && (
|
||||
<div ref={calculateAlertRefHeight}>
|
||||
<div>
|
||||
<Alert
|
||||
closable
|
||||
type="warning"
|
||||
onClose={() => setAlertIsOpen(false)}
|
||||
message={t(
|
||||
'The number of rows displayed is limited to %(rows)d by the dropdown.',
|
||||
{ rows },
|
||||
@@ -483,10 +477,10 @@ const ResultSet = ({
|
||||
</div>
|
||||
)}
|
||||
{limitReached && (
|
||||
<div ref={calculateAlertRefHeight}>
|
||||
<div>
|
||||
<Alert
|
||||
closable
|
||||
type="warning"
|
||||
onClose={() => setAlertIsOpen(false)}
|
||||
message={
|
||||
isAdmin
|
||||
? displayMaxRowsReachedMessage.withAdmin
|
||||
@@ -532,7 +526,6 @@ const ResultSet = ({
|
||||
);
|
||||
};
|
||||
|
||||
const limitReached = query?.results?.displayLimitReached;
|
||||
let sql;
|
||||
let exploreDBId = query.dbId;
|
||||
if (database?.explore_database_id) {
|
||||
@@ -563,6 +556,7 @@ const ResultSet = ({
|
||||
sql = (
|
||||
<HighlightedSql
|
||||
sql={query.sql}
|
||||
rawSql={query.executedSql}
|
||||
{...(showSqlInline && { maxLines: 1, maxWidth: 60 })}
|
||||
/>
|
||||
);
|
||||
@@ -646,17 +640,6 @@ const ResultSet = ({
|
||||
|
||||
if (query.state === QueryState.Success && query.results) {
|
||||
const { results } = query;
|
||||
// Accounts for offset needed for height of ResultSetRowsReturned component if !limitReached
|
||||
const rowMessageHeight = !limitReached ? 32 : 0;
|
||||
// Accounts for offset needed for height of Alert if this.state.alertIsOpen
|
||||
const alertContainerHeight = 70;
|
||||
// We need to calculate the height of this.renderRowsReturned()
|
||||
// if we want results panel to be proper height because the
|
||||
// FilterTable component needs an explicit height to render
|
||||
// the Table component
|
||||
const rowsHeight = alertIsOpen
|
||||
? height - alertContainerHeight
|
||||
: height - rowMessageHeight;
|
||||
let data;
|
||||
if (cache && query.cached) {
|
||||
data = cachedData;
|
||||
@@ -712,15 +695,27 @@ const ResultSet = ({
|
||||
{sql}
|
||||
</>
|
||||
)}
|
||||
<ResultTable
|
||||
data={data}
|
||||
queryId={query.id}
|
||||
orderedColumnKeys={results.columns.map(col => col.column_name)}
|
||||
height={rowsHeight}
|
||||
filterText={searchText}
|
||||
expandedColumns={expandedColumns}
|
||||
allowHTML={allowHTML}
|
||||
/>
|
||||
<div
|
||||
css={css`
|
||||
flex: 1 1 auto;
|
||||
`}
|
||||
>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<ResultTable
|
||||
data={data}
|
||||
queryId={query.id}
|
||||
orderedColumnKeys={results.columns.map(
|
||||
col => col.column_name,
|
||||
)}
|
||||
height={height}
|
||||
filterText={searchText}
|
||||
expandedColumns={expandedColumns}
|
||||
allowHTML={allowHTML}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
</ResultContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -245,4 +245,104 @@ describe('SavedQuery', () => {
|
||||
expect(overwriteCombobox).toBeInTheDocument();
|
||||
expect(overwritePlaceholderText).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('modal stays open while save is in progress and closes after completion', async () => {
|
||||
let resolveSave: () => void;
|
||||
const savePromise = new Promise<void>(resolve => {
|
||||
resolveSave = resolve;
|
||||
});
|
||||
|
||||
const mockOnSave = jest.fn().mockImplementation(() => savePromise);
|
||||
|
||||
render(<SaveQuery {...mockedProps} onSave={mockOnSave} />, {
|
||||
useRedux: true,
|
||||
store: mockStore(mockState),
|
||||
});
|
||||
|
||||
// Open the modal
|
||||
const saveBtn = screen.getByRole('button', { name: /save/i });
|
||||
userEvent.click(saveBtn);
|
||||
|
||||
// Verify modal is open
|
||||
expect(
|
||||
screen.getByRole('heading', { name: /save query/i }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Click save button in the modal
|
||||
const modalSaveBtn = screen.getAllByRole('button', { name: /save/i })[1];
|
||||
userEvent.click(modalSaveBtn);
|
||||
|
||||
// Modal should still be open while save is in progress
|
||||
expect(
|
||||
screen.getByRole('heading', { name: /save query/i }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Resolve the save promise
|
||||
resolveSave!();
|
||||
|
||||
// Wait for modal to close after save completes
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: /save query/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('handles save with a new tab that has no changes', async () => {
|
||||
const mockOnSave = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
// Mock state for a new tab with default SQL
|
||||
const newTabState = {
|
||||
...mockState,
|
||||
sqlLab: {
|
||||
...mockState.sqlLab,
|
||||
queryEditors: [
|
||||
{
|
||||
id: mockedProps.queryEditorId,
|
||||
dbId: 1,
|
||||
catalog: null,
|
||||
schema: 'main',
|
||||
sql: 'SELECT ...', // Default SQL for new tabs
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
render(<SaveQuery {...mockedProps} onSave={mockOnSave} />, {
|
||||
useRedux: true,
|
||||
store: mockStore(newTabState),
|
||||
});
|
||||
|
||||
// Open the modal
|
||||
const saveBtn = screen.getByRole('button', { name: /save/i });
|
||||
userEvent.click(saveBtn);
|
||||
|
||||
// Modal should open
|
||||
expect(
|
||||
screen.getByRole('heading', { name: /save query/i }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// The name field should have "Undefined" as default
|
||||
const nameInput = screen.getAllByRole('textbox')[0] as HTMLInputElement;
|
||||
expect(nameInput).toHaveValue('Undefined');
|
||||
|
||||
// Click save button
|
||||
const modalSaveBtn = screen.getAllByRole('button', { name: /save/i })[1];
|
||||
userEvent.click(modalSaveBtn);
|
||||
|
||||
// Wait for save to complete and modal to close
|
||||
await waitFor(() => {
|
||||
expect(mockOnSave).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: /save query/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -147,14 +147,14 @@ const SaveQuery = ({
|
||||
|
||||
const close = () => setShowSave(false);
|
||||
|
||||
const onSaveWrapper = () => {
|
||||
const onSaveWrapper = async () => {
|
||||
logAction(LOG_ACTIONS_SQLLAB_SAVE_QUERY, {});
|
||||
onSave(queryPayload(), query.id);
|
||||
await onSave(queryPayload(), query.id);
|
||||
close();
|
||||
};
|
||||
|
||||
const onUpdateWrapper = () => {
|
||||
onUpdate(queryPayload(), query.id);
|
||||
const onUpdateWrapper = async () => {
|
||||
await onUpdate(queryPayload(), query.id);
|
||||
close();
|
||||
};
|
||||
|
||||
@@ -220,9 +220,7 @@ const SaveQuery = ({
|
||||
/>
|
||||
<Modal
|
||||
className="save-query-modal"
|
||||
onHandledPrimaryAction={onSaveWrapper}
|
||||
onHide={close}
|
||||
primaryButtonName={isSaved ? t('Save') : t('Save as')}
|
||||
width="620px"
|
||||
show={showSave}
|
||||
name={t('Save query')}
|
||||
|
||||
@@ -27,6 +27,7 @@ import { removeTables, setActiveSouthPaneTab } from 'src/SqlLab/actions/sqlLab';
|
||||
import { Label } from '@superset-ui/core/components';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { SqlLabRootState } from 'src/SqlLab/types';
|
||||
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
||||
import QueryHistory from '../QueryHistory';
|
||||
import {
|
||||
STATUS_OPTIONS,
|
||||
@@ -96,6 +97,8 @@ const SouthPane = ({
|
||||
displayLimit,
|
||||
defaultQueryLimit,
|
||||
}: SouthPaneProps) => {
|
||||
const { id, tabViewId } = useQueryEditor(queryEditorId, ['tabViewId']);
|
||||
const editorId = tabViewId ?? id;
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { offline, tables } = useSelector(
|
||||
@@ -111,11 +114,8 @@ const SouthPane = ({
|
||||
) ?? 'Results';
|
||||
|
||||
const pinnedTables = useMemo(
|
||||
() =>
|
||||
tables.filter(
|
||||
({ queryEditorId: qeId }) => String(queryEditorId) === qeId,
|
||||
),
|
||||
[queryEditorId, tables],
|
||||
() => tables.filter(({ queryEditorId: qeId }) => String(editorId) === qeId),
|
||||
[editorId, tables],
|
||||
);
|
||||
const pinnedTableKeys = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -50,7 +50,7 @@ import type {
|
||||
CursorPosition,
|
||||
} from 'src/SqlLab/types';
|
||||
import type { DatabaseObject } from 'src/features/databases/types';
|
||||
import { debounce, throttle, isEmpty } from 'lodash';
|
||||
import { debounce, isEmpty } from 'lodash';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import {
|
||||
Alert,
|
||||
@@ -98,7 +98,6 @@ import {
|
||||
INITIAL_NORTH_PERCENT,
|
||||
INITIAL_SOUTH_PERCENT,
|
||||
SET_QUERY_EDITOR_SQL_DEBOUNCE_MS,
|
||||
WINDOW_RESIZE_THROTTLE_MS,
|
||||
} from 'src/SqlLab/constants';
|
||||
import {
|
||||
getItem,
|
||||
@@ -300,7 +299,6 @@ const SqlEditor: FC<Props> = ({
|
||||
|
||||
const logAction = useLogAction({ queryEditorId: queryEditor.id });
|
||||
const isActive = currentQueryEditorId === queryEditor.id;
|
||||
const [height, setHeight] = useState(0);
|
||||
const [autorun, setAutorun] = useState(queryEditor.autorun);
|
||||
const [ctas, setCtas] = useState('');
|
||||
const [northPercent, setNorthPercent] = useState(
|
||||
@@ -328,8 +326,6 @@ const SqlEditor: FC<Props> = ({
|
||||
|
||||
const SqlFormExtension = extensionsRegistry.get('sqleditor.extension.form');
|
||||
|
||||
const isTempId = (value: unknown): boolean => Number.isNaN(Number(value));
|
||||
|
||||
const startQuery = useCallback(
|
||||
(ctasArg = false, ctas_method = CtasEnum.Table) => {
|
||||
if (!database) {
|
||||
@@ -586,21 +582,12 @@ const SqlEditor: FC<Props> = ({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// We need to measure the height of the sql editor post render to figure the height of
|
||||
// the south pane so it gets rendered properly
|
||||
setHeight(getSqlEditorHeight());
|
||||
const handleWindowResizeWithThrottle = throttle(
|
||||
() => setHeight(getSqlEditorHeight()),
|
||||
WINDOW_RESIZE_THROTTLE_MS,
|
||||
);
|
||||
if (isActive) {
|
||||
loadQueryEditor();
|
||||
window.addEventListener('resize', handleWindowResizeWithThrottle);
|
||||
window.addEventListener('beforeunload', onBeforeUnload);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleWindowResizeWithThrottle);
|
||||
window.removeEventListener('beforeunload', onBeforeUnload);
|
||||
};
|
||||
// TODO: Remove useEffectEvent deps once https://github.com/facebook/react/pull/25881 is released
|
||||
@@ -982,6 +969,7 @@ const SqlEditor: FC<Props> = ({
|
||||
);
|
||||
|
||||
const queryPane = () => {
|
||||
const height = getSqlEditorHeight();
|
||||
const { aceEditorHeight, southPaneHeight } =
|
||||
getAceEditorAndSouthPaneHeights(height, northPercent, southPercent);
|
||||
return (
|
||||
@@ -1009,7 +997,7 @@ const SqlEditor: FC<Props> = ({
|
||||
{queryEditor.isDataset && renderDatasetWarning()}
|
||||
{isActive && (
|
||||
<AceEditorWrapper
|
||||
autocomplete={autocompleteEnabled && !isTempId(queryEditor.id)}
|
||||
autocomplete={autocompleteEnabled}
|
||||
onBlur={onSqlChanged}
|
||||
onChange={onSqlChanged}
|
||||
queryEditorId={queryEditor.id}
|
||||
|
||||
@@ -188,6 +188,7 @@ export const table = {
|
||||
export const defaultQueryEditor = {
|
||||
version: LatestQueryEditorVersion,
|
||||
id: 'dfsadfs',
|
||||
immutableId: 'immutable-id',
|
||||
autorun: false,
|
||||
dbId: 1,
|
||||
latestQueryId: null,
|
||||
@@ -204,6 +205,7 @@ export const defaultQueryEditor = {
|
||||
export const extraQueryEditor1 = {
|
||||
...defaultQueryEditor,
|
||||
id: 'diekd23',
|
||||
immutableId: 'immutable-id',
|
||||
sql: 'SELECT *\nFROM\nWHERE\nLIMIT',
|
||||
name: 'Untitled Query 2',
|
||||
selectedText: 'SELECT',
|
||||
@@ -212,10 +214,20 @@ export const extraQueryEditor1 = {
|
||||
export const extraQueryEditor2 = {
|
||||
...defaultQueryEditor,
|
||||
id: 'owkdi998',
|
||||
immutableId: 'immutable-id',
|
||||
sql: '',
|
||||
name: 'Untitled Query 3',
|
||||
};
|
||||
|
||||
export const extraQueryEditor3 = {
|
||||
...defaultQueryEditor,
|
||||
id: 'kvk23',
|
||||
immutableId: 'immutable-id',
|
||||
sql: '',
|
||||
name: 'Untitled Query 4',
|
||||
tabViewId: 37,
|
||||
};
|
||||
|
||||
export const queries = [
|
||||
{
|
||||
dbId: 1,
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { pick } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import { SqlLabRootState, QueryEditor } from 'src/SqlLab/types';
|
||||
|
||||
@@ -24,11 +25,20 @@ export default function useQueryEditor<T extends keyof QueryEditor>(
|
||||
sqlEditorId: string,
|
||||
attributes: ReadonlyArray<T>,
|
||||
) {
|
||||
const queryEditors = useSelector<SqlLabRootState, QueryEditor[]>(
|
||||
({ sqlLab: { queryEditors } }) => queryEditors,
|
||||
shallowEqual,
|
||||
);
|
||||
const queryEditorsById = useMemo(
|
||||
() => Object.fromEntries(queryEditors.map(editor => [editor.id, editor])),
|
||||
[queryEditors.map(({ id }) => id).join(',')],
|
||||
);
|
||||
|
||||
return useSelector<SqlLabRootState, Pick<QueryEditor, T | 'id'>>(
|
||||
({ sqlLab: { unsavedQueryEditor, queryEditors } }) =>
|
||||
({ sqlLab: { unsavedQueryEditor } }) =>
|
||||
pick(
|
||||
{
|
||||
...queryEditors.find(({ id }) => id === sqlEditorId),
|
||||
...queryEditorsById[sqlEditorId],
|
||||
...(sqlEditorId === unsavedQueryEditor?.id && unsavedQueryEditor),
|
||||
},
|
||||
['id'].concat(attributes),
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@superset-ui/core';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { BootstrapData } from 'src/types/bootstrapTypes';
|
||||
import type { InitialState } from 'src/hooks/apiResources/sqlLab';
|
||||
import {
|
||||
@@ -55,6 +56,7 @@ export default function getInitialState({
|
||||
let queryEditors: Record<string, QueryEditor> = {};
|
||||
const defaultQueryEditor = {
|
||||
version: LatestQueryEditorVersion,
|
||||
immutableId: nanoid(11),
|
||||
loaded: true,
|
||||
name: t('Untitled query'),
|
||||
sql: '',
|
||||
@@ -78,6 +80,7 @@ export default function getInitialState({
|
||||
queryEditor = {
|
||||
version: activeTab.extra_json?.version ?? QueryEditorVersion.V1,
|
||||
id: id.toString(),
|
||||
immutableId: activeTab.extra_json?.immutableId ?? nanoid(11),
|
||||
loaded: true,
|
||||
name: activeTab.label,
|
||||
sql: activeTab.sql || '',
|
||||
@@ -100,6 +103,7 @@ export default function getInitialState({
|
||||
queryEditor = {
|
||||
...defaultQueryEditor,
|
||||
id: id.toString(),
|
||||
immutableId: nanoid(11),
|
||||
loaded: false,
|
||||
name: label,
|
||||
dbId: undefined,
|
||||
@@ -163,7 +167,10 @@ export default function getInitialState({
|
||||
if (localStorageData && sqlLabCacheData?.sqlLab) {
|
||||
const { sqlLab } = sqlLabCacheData;
|
||||
|
||||
if (sqlLab.queryEditors.length === 0) {
|
||||
if (
|
||||
sqlLab.queryEditors.length === 0 &&
|
||||
Object.keys(sqlLab.destroyedQueryEditors ?? {}).length === 0
|
||||
) {
|
||||
// migration was successful
|
||||
localStorage.removeItem('redux');
|
||||
} else {
|
||||
@@ -171,8 +178,9 @@ export default function getInitialState({
|
||||
// add query editors and tables to state with a special flag so they can
|
||||
// be migrated if the `SQLLAB_BACKEND_PERSISTENCE` feature flag is on
|
||||
sqlLab.queryEditors.forEach(qe => {
|
||||
const hasConflictFromBackend = Boolean(queryEditors[qe.id]);
|
||||
const unsavedUpdatedAt = queryEditors[qe.id]?.updatedAt;
|
||||
const sqlEditorId = qe.tabViewId ?? qe.id;
|
||||
const hasConflictFromBackend = Boolean(queryEditors[sqlEditorId]);
|
||||
const unsavedUpdatedAt = queryEditors[sqlEditorId]?.updatedAt;
|
||||
const hasUnsavedUpdateSinceLastSave =
|
||||
qe.updatedAt &&
|
||||
(!unsavedUpdatedAt || qe.updatedAt > unsavedUpdatedAt);
|
||||
@@ -180,13 +188,13 @@ export default function getInitialState({
|
||||
!hasConflictFromBackend || hasUnsavedUpdateSinceLastSave ? qe : {};
|
||||
queryEditors = {
|
||||
...queryEditors,
|
||||
[qe.id]: {
|
||||
...queryEditors[qe.id],
|
||||
[sqlEditorId]: {
|
||||
...queryEditors[sqlEditorId],
|
||||
...cachedQueryEditor,
|
||||
name:
|
||||
cachedQueryEditor.title ||
|
||||
cachedQueryEditor.name ||
|
||||
queryEditors[qe.id]?.name,
|
||||
queryEditors[sqlEditorId]?.name,
|
||||
...(cachedQueryEditor.id &&
|
||||
unsavedQueryEditor.id === qe.id &&
|
||||
unsavedQueryEditor),
|
||||
@@ -220,18 +228,21 @@ export default function getInitialState({
|
||||
});
|
||||
}
|
||||
if (sqlLab.tabHistory) {
|
||||
tabHistory.push(...sqlLab.tabHistory);
|
||||
tabHistory.push(
|
||||
...sqlLab.tabHistory.filter(
|
||||
tabId => !sqlLab.destroyedQueryEditors?.[tabId],
|
||||
),
|
||||
);
|
||||
}
|
||||
lastUpdatedActiveTab = tabHistory.slice(tabHistory.length - 1)[0] || '';
|
||||
|
||||
if (sqlLab.destroyedQueryEditors) {
|
||||
Object.entries(sqlLab.destroyedQueryEditors).forEach(([id, ts]) => {
|
||||
destroyedQueryEditors[id] = ts;
|
||||
if (queryEditors[id]) {
|
||||
destroyedQueryEditors[id] = ts;
|
||||
delete queryEditors[id];
|
||||
}
|
||||
});
|
||||
}
|
||||
lastUpdatedActiveTab = tabHistory.slice(tabHistory.length - 1)[0] || '';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -135,7 +135,7 @@ export default function sqlLabReducer(state = {}, action) {
|
||||
};
|
||||
let newState = removeFromArr(state, 'queryEditors', queryEditor);
|
||||
// List of remaining queryEditor ids
|
||||
const qeIds = newState.queryEditors.map(qe => qe.id);
|
||||
const qeIds = newState.queryEditors.map(qe => qe.tabViewId ?? qe.id);
|
||||
|
||||
const queries = {};
|
||||
Object.keys(state.queries).forEach(k => {
|
||||
@@ -150,7 +150,8 @@ export default function sqlLabReducer(state = {}, action) {
|
||||
|
||||
// Remove associated table schemas
|
||||
const tables = state.tables.filter(
|
||||
table => table.queryEditorId !== queryEditor.id,
|
||||
table =>
|
||||
table.queryEditorId !== (queryEditor.tabViewId ?? queryEditor.id),
|
||||
);
|
||||
|
||||
newState = {
|
||||
@@ -167,7 +168,9 @@ export default function sqlLabReducer(state = {}, action) {
|
||||
},
|
||||
destroyedQueryEditors: {
|
||||
...newState.destroyedQueryEditors,
|
||||
[queryEditor.id]: Date.now(),
|
||||
...(!queryEditor.inLocalStorage && {
|
||||
[queryEditor.tabViewId ?? queryEditor.id]: Date.now(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
return newState;
|
||||
@@ -317,10 +320,17 @@ export default function sqlLabReducer(state = {}, action) {
|
||||
},
|
||||
[actions.START_QUERY]() {
|
||||
let newState = { ...state };
|
||||
let sqlEditorId;
|
||||
if (action.query.sqlEditorId) {
|
||||
const queryEditorByTabId = getFromArr(
|
||||
state.queryEditors,
|
||||
action.query.sqlEditorId,
|
||||
'tabViewId',
|
||||
);
|
||||
sqlEditorId = queryEditorByTabId?.id ?? action.query.sqlEditorId;
|
||||
const qe = {
|
||||
...getFromArr(state.queryEditors, action.query.sqlEditorId),
|
||||
...(action.query.sqlEditorId === state.unsavedQueryEditor.id &&
|
||||
...getFromArr(state.queryEditors, sqlEditorId),
|
||||
...(sqlEditorId === state.unsavedQueryEditor.id &&
|
||||
state.unsavedQueryEditor),
|
||||
};
|
||||
if (qe.latestQueryId && state.queries[qe.latestQueryId]) {
|
||||
@@ -343,7 +353,7 @@ export default function sqlLabReducer(state = {}, action) {
|
||||
{
|
||||
latestQueryId: action.query.id,
|
||||
},
|
||||
action.query.sqlEditorId,
|
||||
sqlEditorId,
|
||||
action.query.isDataPreview,
|
||||
),
|
||||
};
|
||||
@@ -382,6 +392,7 @@ export default function sqlLabReducer(state = {}, action) {
|
||||
results: action.results,
|
||||
rows: action?.results?.query?.rows || 0,
|
||||
state: QueryState.Success,
|
||||
executedSql: action?.results?.query?.executedSql,
|
||||
limitingFactor: action?.results?.query?.limitingFactor,
|
||||
tempSchema: action?.results?.query?.tempSchema,
|
||||
tempTable: action?.results?.query?.tempTable,
|
||||
@@ -495,12 +506,6 @@ export default function sqlLabReducer(state = {}, action) {
|
||||
action.newTable,
|
||||
);
|
||||
},
|
||||
[actions.MIGRATE_TAB_HISTORY]() {
|
||||
const tabHistory = state.tabHistory.map(tabId =>
|
||||
tabId === action.oldId ? action.newId : tabId,
|
||||
);
|
||||
return { ...state, tabHistory };
|
||||
},
|
||||
[actions.MIGRATE_QUERY]() {
|
||||
const query = {
|
||||
...state.queries[action.queryId],
|
||||
|
||||
@@ -240,11 +240,13 @@ describe('sqlLabReducer', () => {
|
||||
);
|
||||
});
|
||||
it('should migrate query editor by new query editor id', () => {
|
||||
const { length } = newState.queryEditors;
|
||||
const index = newState.queryEditors.findIndex(({ id }) => id === qe.id);
|
||||
const newQueryEditor = {
|
||||
...qe,
|
||||
id: 'updatedNewId',
|
||||
tabViewId: 'updatedNewId',
|
||||
schema: 'updatedSchema',
|
||||
inLocalStorage: false,
|
||||
};
|
||||
const action = {
|
||||
type: actions.MIGRATE_QUERY_EDITOR,
|
||||
@@ -252,20 +254,18 @@ describe('sqlLabReducer', () => {
|
||||
newQueryEditor,
|
||||
};
|
||||
newState = sqlLabReducer(newState, action);
|
||||
expect(newState.queryEditors[index].id).toEqual('updatedNewId');
|
||||
expect(newState.queryEditors[index].id).toEqual(qe.id);
|
||||
expect(newState.queryEditors[index].tabViewId).toEqual('updatedNewId');
|
||||
expect(newState.queryEditors[index]).toEqual(newQueryEditor);
|
||||
});
|
||||
it('should migrate tab history by new query editor id', () => {
|
||||
expect(newState.tabHistory).toContain(qe.id);
|
||||
const action = {
|
||||
type: actions.MIGRATE_TAB_HISTORY,
|
||||
oldId: qe.id,
|
||||
newId: 'updatedNewId',
|
||||
const removeAction = {
|
||||
type: actions.REMOVE_QUERY_EDITOR,
|
||||
queryEditor: newQueryEditor,
|
||||
};
|
||||
newState = sqlLabReducer(newState, action);
|
||||
|
||||
expect(newState.tabHistory).toContain('updatedNewId');
|
||||
expect(newState.tabHistory).not.toContain(qe.id);
|
||||
newState = sqlLabReducer(newState, removeAction);
|
||||
expect(newState.queryEditors).toHaveLength(length - 1);
|
||||
expect(Object.keys(newState.destroyedQueryEditors)).toContain(
|
||||
newQueryEditor.tabViewId,
|
||||
);
|
||||
});
|
||||
it('should clear the destroyed query editors', () => {
|
||||
const expectedQEId = '1233289';
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface CursorPosition {
|
||||
export interface QueryEditor {
|
||||
version: QueryEditorVersion;
|
||||
id: string;
|
||||
immutableId: string;
|
||||
dbId?: number;
|
||||
name: string;
|
||||
title?: string; // keep it optional for backward compatibility
|
||||
@@ -70,6 +71,7 @@ export interface QueryEditor {
|
||||
updatedAt?: number;
|
||||
cursorPosition?: CursorPosition;
|
||||
isDataset?: boolean;
|
||||
tabViewId?: string;
|
||||
}
|
||||
|
||||
export type toastState = {
|
||||
|
||||
@@ -45,6 +45,7 @@ const PERSISTENT_QUERY_EDITOR_KEYS = new Set([
|
||||
'dbId',
|
||||
'height',
|
||||
'id',
|
||||
'immutableId',
|
||||
'latestQueryId',
|
||||
'northPercent',
|
||||
'queryLimit',
|
||||
|
||||
@@ -161,6 +161,7 @@ const ChartContextMenu = (
|
||||
formData.datasource,
|
||||
dashboardId,
|
||||
formData,
|
||||
!canDrillToDetail && !canDrillBy,
|
||||
);
|
||||
|
||||
const isLoadingDataset = datasetResource.status === ResourceStatus.Loading;
|
||||
|
||||
@@ -22,9 +22,15 @@ import { renderHook } from '@testing-library/react-hooks';
|
||||
import mockState from 'spec/fixtures/mockState';
|
||||
import { sliceId } from 'spec/fixtures/mockChartQueries';
|
||||
import { noOp } from 'src/utils/common';
|
||||
import { cachedSupersetGet } from 'src/utils/cachedSupersetGet';
|
||||
import { useContextMenu } from './useContextMenu';
|
||||
import { ContextMenuItem } from './ChartContextMenu';
|
||||
|
||||
jest.mock('src/utils/cachedSupersetGet');
|
||||
|
||||
const mockCachedSupersetGet = cachedSupersetGet as jest.MockedFunction<
|
||||
typeof cachedSupersetGet
|
||||
>;
|
||||
const CONTEXT_MENU_TEST_ID = 'chart-context-menu';
|
||||
|
||||
// @ts-ignore
|
||||
@@ -73,6 +79,19 @@ const setup = ({
|
||||
return result;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockCachedSupersetGet.mockClear();
|
||||
mockCachedSupersetGet.mockResolvedValue({
|
||||
response: {} as Response,
|
||||
json: {
|
||||
result: {
|
||||
columns: [],
|
||||
metrics: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Context menu renders', () => {
|
||||
const result = setup();
|
||||
expect(screen.queryByTestId(CONTEXT_MENU_TEST_ID)).not.toBeInTheDocument();
|
||||
@@ -271,3 +290,42 @@ test('Context menu does not show "Drill to detail" with `can_drill`, `can_explor
|
||||
result.current.onContextMenu(0, 0, {});
|
||||
expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Dataset drill info API call is made when user has drill permissions', async () => {
|
||||
const result = setup({
|
||||
roles: {
|
||||
Admin: [
|
||||
['can_explore', 'Superset'],
|
||||
['can_samples', 'Datasource'],
|
||||
['can_write', 'ExploreFormDataRestApi'],
|
||||
['can_get_drill_info', 'Dataset'],
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
result.current.onContextMenu(0, 0, {});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
expect(mockCachedSupersetGet).toHaveBeenCalledWith({
|
||||
endpoint: expect.stringContaining(
|
||||
'/api/v1/dataset/1/drill_info/?q=(dashboard_id:',
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
test('Dataset drill info API call is not made when user lacks drill permissions', async () => {
|
||||
const result = setup({
|
||||
roles: {
|
||||
Admin: [['invalid_permission', 'Dashboard']],
|
||||
},
|
||||
});
|
||||
|
||||
result.current.onContextMenu(0, 0, {});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
expect(mockCachedSupersetGet).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText('Drill by')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -167,7 +167,8 @@ class ChartRenderer extends Component {
|
||||
nextProps.formData.subcategories !==
|
||||
this.props.formData.subcategories ||
|
||||
nextProps.cacheBusterProp !== this.props.cacheBusterProp ||
|
||||
nextProps.emitCrossFilters !== this.props.emitCrossFilters
|
||||
nextProps.emitCrossFilters !== this.props.emitCrossFilters ||
|
||||
nextProps.postTransformProps !== this.props.postTransformProps
|
||||
);
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -27,8 +27,10 @@ import { ChartSource } from 'src/types/ChartSource';
|
||||
|
||||
jest.mock('@superset-ui/core', () => ({
|
||||
...jest.requireActual('@superset-ui/core'),
|
||||
SuperChart: ({ formData }) => (
|
||||
<div data-test="mock-super-chart">{JSON.stringify(formData)}</div>
|
||||
SuperChart: ({ postTransformProps = x => x, ...props }) => (
|
||||
<div data-test="mock-super-chart">
|
||||
{JSON.stringify(postTransformProps(props).formData)}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -91,3 +93,20 @@ test('should not render chart context menu if the context menu is suppressed for
|
||||
);
|
||||
expect(queryByTestId('mock-chart-context-menu')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should detect changes in postTransformProps', () => {
|
||||
const postTransformProps = jest.fn(x => x);
|
||||
const initialProps = {
|
||||
...requiredProps,
|
||||
queriesResponse: [{ data: 'initial' }],
|
||||
chartStatus: 'success',
|
||||
};
|
||||
const { rerender } = render(<ChartRenderer {...initialProps} />);
|
||||
const updatedProps = {
|
||||
...initialProps,
|
||||
postTransformProps,
|
||||
};
|
||||
expect(postTransformProps).toHaveBeenCalledTimes(0);
|
||||
rerender(<ChartRenderer {...updatedProps} />);
|
||||
expect(postTransformProps).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -356,3 +356,215 @@ describe('Embedded mode behavior', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Table view with pagination', () => {
|
||||
beforeEach(() => {
|
||||
// Mock a large dataset response for pagination testing
|
||||
const mockLargeDataset = {
|
||||
result: [
|
||||
{
|
||||
data: Array.from({ length: 100 }, (_, i) => ({
|
||||
state: `State${i}`,
|
||||
sum__num: 1000 + i,
|
||||
})),
|
||||
colnames: ['state', 'sum__num'],
|
||||
coltypes: [1, 0],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
fetchMock.post(CHART_DATA_ENDPOINT, mockLargeDataset, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
test('should render table view when Table radio is selected', async () => {
|
||||
await renderModal({
|
||||
column: { column_name: 'state', verbose_name: null },
|
||||
drillByConfig: {
|
||||
filters: [{ col: 'gender', op: '==', val: 'boy' }],
|
||||
groupbyFieldName: 'groupby',
|
||||
},
|
||||
});
|
||||
|
||||
// Switch to table view
|
||||
const tableRadio = await screen.findByRole('radio', { name: /table/i });
|
||||
userEvent.click(tableRadio);
|
||||
|
||||
// Wait for table to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Check that pagination is rendered (there's also a breadcrumb list)
|
||||
const lists = screen.getAllByRole('list');
|
||||
const paginationList = lists.find(list =>
|
||||
list.className?.includes('pagination'),
|
||||
);
|
||||
expect(paginationList).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should handle pagination in table view', async () => {
|
||||
await renderModal({
|
||||
column: { column_name: 'state', verbose_name: null },
|
||||
drillByConfig: {
|
||||
filters: [{ col: 'gender', op: '==', val: 'boy' }],
|
||||
groupbyFieldName: 'groupby',
|
||||
},
|
||||
});
|
||||
|
||||
// Switch to table view
|
||||
const tableRadio = await screen.findByRole('radio', { name: /table/i });
|
||||
userEvent.click(tableRadio);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Check that first page data is shown
|
||||
expect(screen.getByText('State0')).toBeInTheDocument();
|
||||
|
||||
// Check pagination controls exist
|
||||
const nextPageButton = screen.getByTitle('Next Page');
|
||||
expect(nextPageButton).toBeInTheDocument();
|
||||
|
||||
// Click next page
|
||||
userEvent.click(nextPageButton);
|
||||
|
||||
// Verify page changed (State0 should not be visible on page 2)
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('State0')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should maintain table state when switching between Chart and Table views', async () => {
|
||||
await renderModal({
|
||||
column: { column_name: 'state', verbose_name: null },
|
||||
drillByConfig: {
|
||||
filters: [{ col: 'gender', op: '==', val: 'boy' }],
|
||||
groupbyFieldName: 'groupby',
|
||||
},
|
||||
});
|
||||
|
||||
const chartRadio = screen.getByRole('radio', { name: /chart/i });
|
||||
const tableRadio = screen.getByRole('radio', { name: /table/i });
|
||||
|
||||
// Switch to table view
|
||||
userEvent.click(tableRadio);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Switch back to chart view
|
||||
userEvent.click(chartRadio);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('drill-by-chart')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Switch back to table view - should maintain state
|
||||
userEvent.click(tableRadio);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should not cause infinite re-renders with pagination', async () => {
|
||||
// Mock console.error to catch potential infinite loop warnings
|
||||
const originalError = console.error;
|
||||
const consoleErrorSpy = jest.fn();
|
||||
console.error = consoleErrorSpy;
|
||||
|
||||
await renderModal({
|
||||
column: { column_name: 'state', verbose_name: null },
|
||||
drillByConfig: {
|
||||
filters: [{ col: 'gender', op: '==', val: 'boy' }],
|
||||
groupbyFieldName: 'groupby',
|
||||
},
|
||||
});
|
||||
|
||||
// Switch to table view
|
||||
const tableRadio = await screen.findByRole('radio', { name: /table/i });
|
||||
userEvent.click(tableRadio);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Check that no infinite loop errors were logged
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('Maximum update depth exceeded'),
|
||||
);
|
||||
|
||||
console.error = originalError;
|
||||
});
|
||||
|
||||
test('should handle empty results in table view', async () => {
|
||||
// Mock empty dataset response
|
||||
fetchMock.post(
|
||||
CHART_DATA_ENDPOINT,
|
||||
{
|
||||
result: [
|
||||
{
|
||||
data: [],
|
||||
colnames: ['state', 'sum__num'],
|
||||
coltypes: [1, 0],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
|
||||
await renderModal({
|
||||
column: { column_name: 'state', verbose_name: null },
|
||||
drillByConfig: {
|
||||
filters: [{ col: 'gender', op: '==', val: 'boy' }],
|
||||
groupbyFieldName: 'groupby',
|
||||
},
|
||||
});
|
||||
|
||||
// Switch to table view
|
||||
const tableRadio = await screen.findByRole('radio', { name: /table/i });
|
||||
userEvent.click(tableRadio);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Should show empty state
|
||||
expect(screen.getByText('No data')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should handle sorting in table view', async () => {
|
||||
await renderModal({
|
||||
column: { column_name: 'state', verbose_name: null },
|
||||
drillByConfig: {
|
||||
filters: [{ col: 'gender', op: '==', val: 'boy' }],
|
||||
groupbyFieldName: 'groupby',
|
||||
},
|
||||
});
|
||||
|
||||
// Switch to table view
|
||||
const tableRadio = await screen.findByRole('radio', { name: /table/i });
|
||||
userEvent.click(tableRadio);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Find sortable column header
|
||||
const sortableHeaders = screen.getAllByTestId('sort-header');
|
||||
expect(sortableHeaders.length).toBeGreaterThan(0);
|
||||
|
||||
// Click to sort
|
||||
userEvent.click(sortableHeaders[0]);
|
||||
|
||||
// Table should still be rendered without crashes
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,11 +32,11 @@ export type Dataset = {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
};
|
||||
changed_on_humanized: string;
|
||||
created_on_humanized: string;
|
||||
description: string;
|
||||
table_name: string;
|
||||
owners: {
|
||||
changed_on_humanized?: string;
|
||||
created_on_humanized?: string;
|
||||
description?: string;
|
||||
table_name?: string;
|
||||
owners?: {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
}[];
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { Component, cloneElement, ReactElement } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { t, css, SupersetTheme } from '@superset-ui/core';
|
||||
import copyTextToClipboard from 'src/utils/copy';
|
||||
import { Tooltip } from '@superset-ui/core/components';
|
||||
import withToasts from '../MessageToasts/withToasts';
|
||||
@@ -104,7 +104,14 @@ class CopyToClip extends Component<CopyToClipboardProps> {
|
||||
return (
|
||||
<span css={{ display: 'inline-flex', alignItems: 'center' }}>
|
||||
{this.props.shouldShowText && this.props.text && (
|
||||
<span data-test="short-url">{this.props.text}</span>
|
||||
<span
|
||||
data-test="short-url"
|
||||
css={(theme: SupersetTheme) => css`
|
||||
margin-right: ${theme.sizeUnit}px;
|
||||
`}
|
||||
>
|
||||
{this.props.text}
|
||||
</span>
|
||||
)}
|
||||
{this.renderTooltip('pointer')}
|
||||
</span>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { useThemeContext } from 'src/theme/ThemeProvider';
|
||||
import { Theme } from '@superset-ui/core';
|
||||
import { Loading } from '@superset-ui/core/components';
|
||||
|
||||
interface CrudThemeProviderProps {
|
||||
children: ReactNode;
|
||||
@@ -62,11 +63,16 @@ export default function CrudThemeProvider({
|
||||
}
|
||||
}, [themeId, globalThemeContext]);
|
||||
|
||||
// If no dashboard theme, just render children (they use global theme)
|
||||
if (!themeId || !dashboardTheme) {
|
||||
// If no themeId, just render children (they use global theme)
|
||||
if (!themeId) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// If themeId exists, but theme is not loaded yet, return null to prevent re-mounting children
|
||||
if (!dashboardTheme) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
// Render children with the dashboard theme provider from controller
|
||||
return (
|
||||
<dashboardTheme.SupersetThemeProvider>
|
||||
|
||||
@@ -16,7 +16,13 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FunctionComponent, useState, useRef } from 'react';
|
||||
import {
|
||||
FunctionComponent,
|
||||
useState,
|
||||
useRef,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
styled,
|
||||
@@ -32,6 +38,7 @@ import {
|
||||
Icons,
|
||||
Alert,
|
||||
Button,
|
||||
Checkbox,
|
||||
Modal,
|
||||
AsyncEsmComponent,
|
||||
} from '@superset-ui/core/components';
|
||||
@@ -92,6 +99,8 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [currentDatasource, setCurrentDatasource] = useState(datasource);
|
||||
const syncColumnsRef = useRef(false);
|
||||
const [confirmModal, setConfirmModal] = useState<any>(null);
|
||||
const currencies = useSelector<
|
||||
{
|
||||
common: {
|
||||
@@ -183,10 +192,9 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
||||
const onConfirmSave = async () => {
|
||||
// Pull out extra fields into the extra object
|
||||
setIsSaving(true);
|
||||
const overrideColumns = datasource.sql !== currentDatasource.sql;
|
||||
try {
|
||||
await SupersetClient.put({
|
||||
endpoint: `/api/v1/dataset/${currentDatasource.id}?override_columns=${overrideColumns}`,
|
||||
endpoint: `/api/v1/dataset/${currentDatasource.id}?override_columns=${syncColumnsRef.current}`,
|
||||
jsonPayload: buildPayload(currentDatasource),
|
||||
});
|
||||
|
||||
@@ -238,34 +246,89 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
||||
setErrors(err);
|
||||
};
|
||||
|
||||
const renderSaveDialog = () => (
|
||||
<div>
|
||||
<Alert
|
||||
css={theme => ({
|
||||
marginTop: theme.sizeUnit * 4,
|
||||
marginBottom: theme.sizeUnit * 4,
|
||||
})}
|
||||
type="warning"
|
||||
showIcon
|
||||
message={t(`The dataset configuration exposed here
|
||||
const getSaveDialog = useCallback(
|
||||
() => (
|
||||
<div>
|
||||
<Alert
|
||||
css={theme => ({
|
||||
marginTop: theme.marginMD,
|
||||
marginBottom: theme.marginSM,
|
||||
})}
|
||||
type="warning"
|
||||
showIcon={false}
|
||||
message={t(`The dataset configuration exposed here
|
||||
affects all the charts using this dataset.
|
||||
Be mindful that changing settings
|
||||
here may affect other charts
|
||||
in undesirable ways.`)}
|
||||
/>
|
||||
{t('Are you sure you want to save and apply changes?')}
|
||||
</div>
|
||||
/>
|
||||
{datasource.sql !== currentDatasource.sql && (
|
||||
<div
|
||||
css={theme => ({
|
||||
marginBottom: theme.marginMD,
|
||||
})}
|
||||
>
|
||||
<Alert
|
||||
css={theme => ({
|
||||
marginBottom: theme.marginSM,
|
||||
})}
|
||||
type="info"
|
||||
showIcon={false}
|
||||
message={t(`The dataset columns will be automatically synced
|
||||
based on the changes in your SQL query. If your changes don't
|
||||
impact the column definitions, you might want to skip this step.`)}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={syncColumnsRef.current}
|
||||
onChange={() => {
|
||||
syncColumnsRef.current = !syncColumnsRef.current;
|
||||
if (confirmModal) {
|
||||
confirmModal.update({
|
||||
content: getSaveDialog(),
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
css={theme => ({
|
||||
marginLeft: theme.marginXS,
|
||||
})}
|
||||
>
|
||||
{t('Automatically sync columns')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{t('Are you sure you want to save and apply changes?')}
|
||||
</div>
|
||||
),
|
||||
[currentDatasource.sql, datasource.sql, confirmModal],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (confirmModal) {
|
||||
confirmModal.update({
|
||||
content: getSaveDialog(),
|
||||
});
|
||||
}
|
||||
}, [confirmModal, getSaveDialog]);
|
||||
|
||||
useEffect(() => {
|
||||
if (datasource.sql !== currentDatasource.sql) {
|
||||
syncColumnsRef.current = true;
|
||||
}
|
||||
}, [datasource.sql, currentDatasource.sql]);
|
||||
|
||||
const onClickSave = () => {
|
||||
dialog.current = modal.confirm({
|
||||
const modalInstance = modal.confirm({
|
||||
title: t('Confirm save'),
|
||||
content: renderSaveDialog(),
|
||||
content: getSaveDialog(),
|
||||
onOk: onConfirmSave,
|
||||
icon: null,
|
||||
okText: t('OK'),
|
||||
cancelText: t('Cancel'),
|
||||
});
|
||||
setConfirmModal(modalInstance);
|
||||
dialog.current = modalInstance;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { useMemo, useRef, useCallback } from 'react';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { GridSize } from 'src/components/GridTable/constants';
|
||||
import { GridTable } from 'src/components/GridTable';
|
||||
import { type ColDef } from 'src/components/GridTable/types';
|
||||
@@ -31,11 +30,6 @@ import type { FilterableTableProps, Datum, CellDataType } from './types';
|
||||
// See https://stackoverflow.com/a/30987109 for more details
|
||||
const ONLY_NUMBER_REGEX = /^(NaN|-?((\d*\.\d+|\d+)([Ee][+-]?\d+)?|Infinity))$/;
|
||||
|
||||
const StyledFilterableTable = styled.div`
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const parseNumberFromString = (value: string | number | null) => {
|
||||
if (typeof value === 'string' && ONLY_NUMBER_REGEX.test(value)) {
|
||||
return parseFloat(value);
|
||||
@@ -126,14 +120,10 @@ export const FilterableTable = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StyledFilterableTable
|
||||
className="filterable-table-container"
|
||||
data-test="table-container"
|
||||
>
|
||||
<div className="filterable-table-container" data-test="table-container">
|
||||
<GridTable
|
||||
size={GridSize.Small}
|
||||
height={height}
|
||||
usePagination={false}
|
||||
columns={columns}
|
||||
data={data}
|
||||
externalFilter={keywordFilter}
|
||||
@@ -142,7 +132,7 @@ export const FilterableTable = ({
|
||||
enableActions
|
||||
columnReorderable
|
||||
/>
|
||||
</StyledFilterableTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen, within } from 'spec/helpers/testing-library';
|
||||
import { render, screen, within, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
import thunk from 'redux-thunk';
|
||||
@@ -172,23 +172,28 @@ describe('ListView', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Update pagination control tests to use button role
|
||||
// Update pagination control tests for Ant Design pagination
|
||||
it('renders pagination controls', () => {
|
||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '«' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '»' })).toBeInTheDocument();
|
||||
const paginationList = screen.getByRole('list');
|
||||
expect(paginationList).toBeInTheDocument();
|
||||
|
||||
const pageOneItem = screen.getByRole('listitem', { name: '1' });
|
||||
expect(pageOneItem).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls fetchData on page change', async () => {
|
||||
const nextButton = screen.getByRole('button', { name: '»' });
|
||||
await userEvent.click(nextButton);
|
||||
const pageTwoItem = screen.getByRole('listitem', { name: '2' });
|
||||
await userEvent.click(pageTwoItem);
|
||||
|
||||
// Remove sortBy expectation since it's not part of the initial state
|
||||
expect(mockedProps.fetchData).toHaveBeenCalledWith({
|
||||
filters: [],
|
||||
pageIndex: 1,
|
||||
pageSize: 1,
|
||||
sortBy: [],
|
||||
await waitFor(() => {
|
||||
const { calls } = mockedProps.fetchData.mock;
|
||||
const pageChangeCall = calls.find(
|
||||
call =>
|
||||
call[0].pageIndex === 1 &&
|
||||
call[0].filters.length === 0 &&
|
||||
call[0].pageSize === 1,
|
||||
);
|
||||
expect(pageChangeCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user