Compare commits

...

89 Commits

Author SHA1 Message Date
Beto Dealmeida
f97b0ead9c WIP 2025-04-02 20:27:18 -04:00
Usiel Riedl
662f0fa8f4 chore(reports): add task for slack channels warm-up (#32585) 2025-03-31 14:30:21 -03:00
JUST.in DO IT
56bf17f879 fix(sqllab): Invalid display of table column keys (#32763) 2025-03-31 14:26:31 -03:00
Kamil Gabryjelski
b92909d621 feat: Enable passing a permalink to cache_dashboard_screenshot endpoint (#32900) 2025-03-31 10:40:36 +02:00
mkramer5454
8f35a3ec8c feat(plugins): Make comparison values on BigNumberPeriodOverPeriod toggleable (#28605) 2025-03-30 22:05:53 -06:00
Vladislav Korenkov
a4a092794a feat(chart controls): Add "%d.%m.%Y" time format option (#32814) 2025-03-30 22:02:58 -06:00
bmaquet
174750c9dd refactor(jinja macro): Update current_user_roles() macro to fetch roles from existing get_user_roles() method (#32888) 2025-03-28 20:50:53 -07:00
Levis Mbote
f2c0686346 feat: Add Aggregation Method for Big Number with Trendline (#32767) 2025-03-29 05:34:23 +02:00
github-actions[bot]
c2afae51cb chore(🦾): bump python grpcio 1.68.0 -> 1.71.0 (#32901)
Co-authored-by: GitHub Action <action@github.com>
2025-03-28 17:36:41 -07:00
Geido
6e1d1ad18b refactor(Icons): Add typing support and improve structure (#32880) 2025-03-28 17:16:31 -07:00
Vitor Avila
ab22bb1878 fix(Jinja): Emit time grain to table charts even if they don't have a temporal column (#32871) 2025-03-28 13:48:49 -03:00
Đỗ Trọng Hải
e0ed652ed8 fix(backend/async_events): allow user to configure username for Redis authentication in GLOBAL_ASYNC_QUERIES_CACHE_BACKEND (#32372)
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>
2025-03-27 19:39:05 -07:00
Luke Hart
103fedaf92 fix: use role_model from security manager (#32873) 2025-03-27 10:01:14 -07:00
Joe Li
50fe7483ae chore: update migrations to use utils (#32852) 2025-03-26 10:29:04 -07:00
SBIN2010
37f626f5e2 fix(ColorPickerControl): change color picker control width (#32851) 2025-03-26 10:28:07 -07:00
Michael S. Molina
b1693f625a chore: Removes unused file (#32860) 2025-03-26 13:35:14 -03:00
Vitor Avila
f0dc1e7527 fix(table-chart): Do not show comparison columns config if time_compare is set to [] (#32863) 2025-03-26 13:28:22 -03:00
Christiaan Baartse
6c7f089ebb fix(translation): Dutch translations for Current datetime filter (#31869) 2025-03-26 22:13:50 +07:00
Beto Dealmeida
68a81c3989 fix: update dataset/query catalog on DB changes (#32829) 2025-03-26 08:56:02 -04:00
Vitor Avila
5222f940cc fix(echarts): Sort series by name using naturalCompare (#32850) 2025-03-26 08:17:43 -03:00
Radovenchyk
45ea11c1b6 docs: added a link to badge releases (#32822)
Co-authored-by: Maxime Beauchemin <maximebeauchemin@gmail.com>
2025-03-25 12:49:48 -07:00
Michael S. Molina
b624919d2f fix: Bump FAB to 4.6.1 (#32848) 2025-03-25 15:29:19 -03:00
Joe Li
b5cb5f4525 chore: updating files for release 4.1.2 (#32831) 2025-03-25 10:26:37 -07:00
JUST.in DO IT
4a70065e5f fix(log): store navigation path to get correct logging path (#32795) 2025-03-25 10:18:55 -07:00
Fardin Mustaque
7d77dc4fd2 fix: Time Comparison Feature Reverts Metric Labels to Metric Keys in Table Charts (#32665)
Co-authored-by: Fardin Mustaque <fardinmustaque@Fardins-Mac-mini.local>
2025-03-25 14:22:15 +02:00
Chris
6f69c84d10 fix: key error in frontend on disallowed GSheets (#32792) 2025-03-24 15:19:59 -07:00
bmaquet
6b96b37c38 feat: Add current_user_roles() Jinja macro (#32770) 2025-03-24 18:39:07 -03:00
github-actions[bot]
b7435f84f0 chore(🦾): bump python humanize 4.12.1 -> 4.12.2 (#32826)
Co-authored-by: GitHub Action <action@github.com>
2025-03-24 13:44:15 -07:00
github-actions[bot]
7bc349c3c3 chore(🦾): bump python pyparsing 3.2.1 -> 3.2.2 (#32827)
Co-authored-by: GitHub Action <action@github.com>
2025-03-24 13:43:52 -07:00
github-actions[bot]
fd4e45aafc chore(🦾): bump python shillelagh subpackage(s) (#32828)
Co-authored-by: GitHub Action <action@github.com>
2025-03-24 13:43:23 -07:00
github-actions[bot]
b339d7ad20 chore(🦾): bump python click-option-group 0.5.6 -> 0.5.7 (#32825)
Co-authored-by: GitHub Action <action@github.com>
2025-03-24 13:43:01 -07:00
Vitor Avila
cedd186c21 feat(Jinja): to_datetime filter (#32781) 2025-03-24 16:55:37 -03:00
SBIN2010
c6c9114b40 fix: CSV/Excel upload form change column dates description (#32797) 2025-03-24 09:48:39 -07:00
Đỗ Trọng Hải
f4a05a5ffd fix(docs): scrollable table of content right bar in Superset docs (#32801)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2025-03-22 10:52:10 -06:00
Đỗ Trọng Hải
a82f916a71 fix(sec): resolve CVE-2025-29907 and CVE-2025-25977 by pinning jspdf to v3 (#32802)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2025-03-22 10:50:13 -06:00
Đỗ Trọng Hải
ff0529c932 fix(model/helper): represent RLS filter clause in proper textual SQL string (#32406)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2025-03-21 14:36:03 -06:00
CharlesNkdl
c0f83a7467 fix(excel export): big number truncation handling (#32739) 2025-03-21 09:39:59 -07:00
V9 Developer
9bb3a5782d fix(config): correct slack image url in talisman (#32778) 2025-03-21 09:32:51 -07:00
Ruslan
5ec710efc6 fix(css): typos in styles (#28350) 2025-03-20 16:32:02 -06:00
Vladislav Korenkov
5866f3ec83 fix(import): Missing catalog field in saved query schema (#32775)
Co-authored-by: Vladislav Koren'kov <korenkov.vv@dns-shop.ru>
2025-03-20 16:33:51 -04:00
Antonio Rivero
01801e3c36 fix(sqllab): Pass query_id as kwarg so backoff can see it (#32774) 2025-03-20 18:46:31 +01:00
Vladislav Korenkov
d319543377 fix(chart control): Change default of "Y Axis Title Margin" (#32720) 2025-03-20 10:14:09 -03:00
Alexandru Soare
5392bafe28 feat(FormModal): Specialized Modal component for forms (#32721) 2025-03-20 14:28:25 +02:00
Elizabeth Thompson
89ce7ba0b0 fix: do not add calculated columns when syncing (#32761) 2025-03-19 17:33:28 -07:00
Antonio Rivero
376a1f49d3 fix(migrations): fix foreign keys to match FAB 4.6.0 tables (#32759) 2025-03-19 22:47:26 +01:00
Vitor Avila
6042ea8f28 feat(embedded): Force a specific referrerPolicy for the iframe request (#32735) 2025-03-19 15:44:07 -03:00
Giampaolo Capelli
78efb62781 fix: Changing language doesn't affect echarts charts (#31751)
Co-authored-by: Giampaolo Capelli <giampaolo.capelli@docaposte.fr>
2025-03-19 11:38:53 -07:00
github-actions[bot]
e9d5079986 chore(🦾): bump python flask-appbuilder subpackage(s) (#32744)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Maxime Beauchemin <maximebeauchemin@gmail.com>
2025-03-19 11:24:24 -07:00
Radovenchyk
c6e0abbe13 chore: replaced the workflow badge link (#32749) 2025-03-19 11:42:47 -06:00
github-actions[bot]
4f166a03f5 chore(🦾): bump python slack-sdk 3.34.0 -> 3.35.0 (#32742)
Co-authored-by: GitHub Action <action@github.com>
2025-03-18 20:46:48 -06:00
sowo
29b62f7c0a fix(contextmenu): uncaught TypeError (#28203) 2025-03-18 20:42:38 -06:00
github-actions[bot]
09ee3e2a1d chore(🦾): bump python shillelagh subpackage(s) (#31255)
Co-authored-by: GitHub Action <action@github.com>
2025-03-18 18:52:34 -07:00
github-actions[bot]
121e424a7f chore(🦾): bump python celery subpackage(s) (#32743)
Co-authored-by: GitHub Action <action@github.com>
2025-03-18 18:52:19 -07:00
github-actions[bot]
66c1a6a875 chore(🦾): bump python sqlglot 26.1.3 -> 26.11.1 (#32745)
Co-authored-by: GitHub Action <action@github.com>
2025-03-18 18:52:00 -07:00
CharlesNkdl
b26c373f4d chore(lang): update and fix french translations (#32711) 2025-03-18 17:31:23 -07:00
github-actions[bot]
4dd318ca68 chore(🦾): bump python flask-appbuilder subpackage(s) (#31251)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Maxime Beauchemin <maximebeauchemin@gmail.com>
2025-03-18 16:25:15 -07:00
Enzo Martellucci
ce6d5f5551 refactor(Icons): Replaces custom icons with Ant Design 5 icons (#32112)
Replace custom icons with Ant Design 5 icons to standardize the icon
2025-03-18 22:22:41 +01:00
Daniel Höxtermann
9e3052968b fix: ensure datasource permission in explore (#32679) 2025-03-18 17:15:10 -04:00
github-actions[bot]
3f1ef2a283 chore(🦾): bump python greenlet (#31247)
Co-authored-by: GitHub Action <action@github.com>
2025-03-18 13:01:31 -07:00
Paul Rhodes
bc3e19d0a2 fix(import): Ensure import exceptions are logged (#32410) 2025-03-18 12:35:57 -07:00
Vitor Avila
850801f510 feat(where_in): Support returning None if filter_values return None (#32731) 2025-03-18 13:18:51 -06:00
Evan Rusackas
710af87faf Revert "Revert "fix(asf): moving notifications to the top of .asf.yaml"" (#32732) 2025-03-18 13:18:36 -06:00
Evan Rusackas
6612343f33 Revert "fix(asf): moving notifications to the top of .asf.yaml" (#32730) 2025-03-18 12:22:53 -06:00
Evan Rusackas
c399295a4e fix(docs): Another CSP hole for run.app to allow Kapa AI (#32728) 2025-03-18 13:42:07 -04:00
Evan Rusackas
e34644d983 fix(docs): poking ANOTHER hole in the CSP for the AI bot. (#32727) 2025-03-18 13:33:00 -04:00
Evan Rusackas
cc0097c87a fix(asf): moving notifications to the top of .asf.yaml (#32726) 2025-03-18 13:19:47 -04:00
Evan Rusackas
d71e655a4b fix(docs): allow recaptcha in CSP (#32724) 2025-03-18 13:13:51 -04:00
Beto Dealmeida
99e69c32ee fix: coerce datetime conversion errors (#32683) 2025-03-18 13:09:23 -04:00
PyKen
a2c164a77d chore(helm): bump postgresql image tag in helm values (#32686) 2025-03-18 09:49:32 -07:00
Evan Rusackas
78d2a584b7 chore(asf): Another .asf.yaml touch-up. (#32714) 2025-03-18 09:27:27 -06:00
Evan Rusackas
f0c8c12c1a chore(docs): touching up AI styling/text (#32689) 2025-03-17 21:25:48 -06:00
Evan Rusackas
34cd741e9b fix(docs): Fixes scrolling issue with AI widget on docs site (#32713) 2025-03-17 21:25:35 -06:00
Evan Rusackas
1684ddc7e6 chore(asf): trying to fix .asf.yaml again to re-enable Discussions (#32712) 2025-03-17 21:01:54 -06:00
Vitor Avila
e35145c816 feat(file uploads): List only allowed schemas in the file uploads dialog (#32702) 2025-03-17 23:36:16 -03:00
Evan Rusackas
4adf44a43c chore(asf): Removing notifications from .asf.yaml - they still don't work :( (#32710) 2025-03-17 17:12:48 -06:00
JUST.in DO IT
cd5a94305c fix(logging): missing path in event data (#32708) 2025-03-17 14:46:34 -07:00
Evan Rusackas
b4602aaf28 chore(asf): fixing(?) .asf.yaml (#32709) 2025-03-17 15:43:45 -06:00
Evan Rusackas
3e69ba1384 fix(repo): re-enable GitHub Discussions (#32703) 2025-03-17 14:34:50 -06:00
Beto Dealmeida
41bf215367 fix: boolean filters in Explore (#32701) 2025-03-17 14:38:00 -04:00
Evan Rusackas
06deaebe19 fix(docs): poking a CSP hole for Kapa AI widget (#32704) 2025-03-17 11:33:15 -06:00
Sam Firke
6a13ab8920 fix(spreadsheet uploads): make file extension comparisons case-insensitive (#32696) 2025-03-17 11:31:11 -06:00
Đỗ Trọng Hải
f1a222d356 fix(cosmetics): allow toast message to be toggled off when modal is opened (#32691)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2025-03-17 11:28:04 -06:00
Đỗ Trọng Hải
a87bedf31a docs(api): correct attribute name instead of table for GET table_metadata in openapi.json (#32690)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2025-03-17 11:26:47 -06:00
Đỗ Trọng Hải
890b6079b9 build(dev-deps): bump prettier to v3.5.3 and follow-up refactor (#32688)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2025-03-17 11:24:24 -06:00
Rytis Ulys
9c62456487 chore: add Oxylabs to INTHEWILD.md (#32697) 2025-03-17 10:18:52 -07:00
Geido
414cdbf83a fix(no-restricted-imports): Fix overrides and include no-fa-icons-usage (#32571) 2025-03-17 18:52:52 +02:00
Michael S. Molina
df06bdf33b fix: Signature of Celery pruner jobs (#32699) 2025-03-17 13:52:31 -03:00
JUST.in DO IT
449f51aed5 fix(log): Update recent_activity by event name (#32681) 2025-03-17 09:18:47 -07:00
Paul Rhodes
c9e2c7037e feat: Implement sparse import for ImportAssetsCommand (#32670) 2025-03-17 08:44:15 -06:00
600 changed files with 8736 additions and 7810 deletions

View File

@@ -17,6 +17,12 @@
# https://cwiki.apache.org/confluence/display/INFRA/.asf.yaml+features+for+git+repositories
---
notifications:
commits: commits@superset.apache.org
issues: notifications@superset.apache.org
pullrequests: notifications@superset.apache.org
discussions: notifications@superset.apache.org
github:
del_branch_on_merge: true
description: "Apache Superset is a Data Visualization and Data Exploration Platform"
@@ -48,6 +54,8 @@ github:
projects: true
# Enable wiki for documentation
wiki: true
# Enable discussions
discussions: true
enabled_merge_buttons:
squash: true

View File

@@ -41,7 +41,7 @@ body:
label: Superset version
options:
- master / latest-dev
- "4.1.1"
- "4.1.2"
- "4.0.2"
validations:
required: true

View File

@@ -57,7 +57,7 @@ repos:
hooks:
- id: prettier
additional_dependencies:
- prettier@3.3.3
- prettier@3.5.3
args: ["--ignore-path=./superset-frontend/.prettierignore"]
files: "superset-frontend"
- repo: local

50
CHANGELOG/4.1.1.md Normal file
View File

@@ -0,0 +1,50 @@
<!--
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
### 4.1 (Fri Nov 15 22:13:57 2024 +0530)
**Database Migrations**
**Features**
**Fixes**
- [#30886](https://github.com/apache/superset/pull/30886) fix: blocks UI elements on right side (@samarsrivastav)
- [#30859](https://github.com/apache/superset/pull/30859) fix(package.json): Pin luxon version to unblock master (@geido)
- [#30588](https://github.com/apache/superset/pull/30588) fix(explore): column data type tooltip format (@mistercrunch)
- [#29911](https://github.com/apache/superset/pull/29911) fix: Rename database from 'couchbasedb' to 'couchbase' in documentation and db_engine_specs (@ayush-couchbase)
- [#30828](https://github.com/apache/superset/pull/30828) fix(TimezoneSelector): Failing unit tests due to timezone change (@geido)
- [#30875](https://github.com/apache/superset/pull/30875) fix: don't show metadata for embedded dashboards (@sadpandajoe)
- [#30851](https://github.com/apache/superset/pull/30851) fix: Graph chart colors (@michael-s-molina)
- [#29867](https://github.com/apache/superset/pull/29867) fix(capitalization): Capitalizing a button. (@rusackas)
- [#29782](https://github.com/apache/superset/pull/29782) fix(translations): Translate embedded errors (@rusackas)
- [#29772](https://github.com/apache/superset/pull/29772) fix: Fixing incomplete string escaping. (@rusackas)
- [#29725](https://github.com/apache/superset/pull/29725) fix(frontend/docker, ci): fix borked Docker build due to Lerna v8 uplift (@hainenber)
**Others**
- [#30576](https://github.com/apache/superset/pull/30576) chore: add link to Superset when report error (@eschutho)
- [#29786](https://github.com/apache/superset/pull/29786) refactor(Slider): Upgrade Slider to Antd 5 (@geido)
- [#29674](https://github.com/apache/superset/pull/29674) refactor(ChartCreation): Migrate tests to RTL (@rtexelm)
- [#29843](https://github.com/apache/superset/pull/29843) refactor(controls): Migrate AdhocMetricOption.test to RTL (@rtexelm)
- [#29845](https://github.com/apache/superset/pull/29845) refactor(controls): Migrate MetricDefinitionValue.test to RTL (@rtexelm)
- [#28424](https://github.com/apache/superset/pull/28424) docs: Check markdown files for bad links using linkinator (@rusackas)
- [#29768](https://github.com/apache/superset/pull/29768) docs(contributing): fix broken link to translations sub-section (@sfirke)

83
CHANGELOG/4.1.2.md Normal file
View File

@@ -0,0 +1,83 @@
<!--
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
### 4.1.2 (Fri Mar 7 13:28:05 2025 -0800)
**Database Migrations**
- [#32538](https://github.com/apache/superset/pull/32538) fix(migrations): Handle comparator None in old time comparison migration (@Antonio-RiveroMartnez)
- [#32155](https://github.com/apache/superset/pull/32155) fix(migrations): Handle no params in time comparison migration (@Antonio-RiveroMartnez)
- [#31185](https://github.com/apache/superset/pull/31185) fix: check for column before adding in migrations (@betodealmeida)
**Features**
- [#29974](https://github.com/apache/superset/pull/29974) feat(sqllab): Adds refresh button to table metadata in SQL Lab (@Usiel)
**Fixes**
- [#32515](https://github.com/apache/superset/pull/32515) fix(sqllab): Allow clear on schema and catalog (@justinpark)
- [#32500](https://github.com/apache/superset/pull/32500) fix: dashboard, chart and dataset import validation (@dpgaspar)
- [#31353](https://github.com/apache/superset/pull/31353) fix(sqllab): duplicate error message (@betodealmeida)
- [#31407](https://github.com/apache/superset/pull/31407) fix: Big Number side cut fixed (@fardin-developer)
- [#31480](https://github.com/apache/superset/pull/31480) fix(sunburst): Use metric label from verbose map (@gerbermichi)
- [#31427](https://github.com/apache/superset/pull/31427) fix(tags): clean up bulk create api and schema (@villebro)
- [#31334](https://github.com/apache/superset/pull/31334) fix(docs): add custom editUrl path for intro page (@dwgrossberg)
- [#31353](https://github.com/apache/superset/pull/31353) fix(sqllab): duplicate error message (@betodealmeida)
- [#31323](https://github.com/apache/superset/pull/31323) fix: Use clickhouse sqlglot dialect for YDB (@vgvoleg)
- [#31198](https://github.com/apache/superset/pull/31198) fix: add more clickhouse disallowed functions on config (@dpgaspar)
- [#31194](https://github.com/apache/superset/pull/31194) fix(embedded): Hide anchor links in embedded mode (@Vitor-Avila)
- [#31960](https://github.com/apache/superset/pull/31960) fix(sqllab): Missing allowHTML props in ResultTableExtension (@justinpark)
- [#31332](https://github.com/apache/superset/pull/31332) fix: prevent multiple pvm errors on migration (@eschutho)
- [#31437](https://github.com/apache/superset/pull/31437) fix(database import): Gracefully handle error to get catalog schemas (@Vitor-Avila)
- [#31173](https://github.com/apache/superset/pull/31173) fix: cache-warmup fails (@nsivarajan)
- [#30442](https://github.com/apache/superset/pull/30442) fix(fe/src/dashboard): optional chaining for possibly nullable parent attribute in LayoutItem type (@hainenber)
- [#31639](https://github.com/apache/superset/pull/31639) fix(sqllab): unable to update saved queries (@DamianPendrak)
- [#29898](https://github.com/apache/superset/pull/29898) fix: parse pandas pivot null values (@eschutho)
- [#31414](https://github.com/apache/superset/pull/31414) fix(Pivot Table): Fix column width to respect currency config (@Vitor-Avila)
- [#31335](https://github.com/apache/superset/pull/31335) fix(histogram): axis margin padding consistent with other graphs (@tatiana-cherne)
- [#31301](https://github.com/apache/superset/pull/31301) fix(AllEntitiesTable): show Tags (@alexandrusoare)
- [#31329](https://github.com/apache/superset/pull/31329) fix: pass string to `process_template` (@betodealmeida)
- [#31341](https://github.com/apache/superset/pull/31341) fix(pinot): remove query aliases from SELECT and ORDER BY clauses in Pinot (@yuribogomolov)
- [#31308](https://github.com/apache/superset/pull/31308) fix: annotations on horizontal bar chart (@DamianPendrak)
- [#31294](https://github.com/apache/superset/pull/31294) fix(sqllab): Remove update_saved_query_exec_info to reduce lag (@justinpark)
- [#30897](https://github.com/apache/superset/pull/30897) fix: Exception handling for SQL Lab views (@michael-s-molina)
- [#31199](https://github.com/apache/superset/pull/31199) fix(Databricks): Escape catalog and schema names in pre-queries (@Vitor-Avila)
- [#31265](https://github.com/apache/superset/pull/31265) fix(trino): db session error in handle cursor (@justinpark)
- [#31024](https://github.com/apache/superset/pull/31024) fix(dataset): use sqlglot for DML check (@betodealmeida)
- [#29885](https://github.com/apache/superset/pull/29885) fix: add mutator to get_columns_description (@eschutho)
- [#30821](https://github.com/apache/superset/pull/30821) fix: x axis title disappears when editing bar chart (@DamianPendrak)
- [#31181](https://github.com/apache/superset/pull/31181) fix: Time-series Line Chart Display unnecessary total (@michael-s-molina)
- [#31163](https://github.com/apache/superset/pull/31163) fix(Dashboard): Backward compatible shared_label_colors field (@geido)
- [#31156](https://github.com/apache/superset/pull/31156) fix: check orderby (@betodealmeida)
- [#31154](https://github.com/apache/superset/pull/31154) fix: Remove unwanted commit on Trino's handle_cursor (@michael-s-molina)
- [#31151](https://github.com/apache/superset/pull/31151) fix: Revert "feat(trino): Add functionality to upload data (#29164)" (@michael-s-molina)
- [#31031](https://github.com/apache/superset/pull/31031) fix(Dashboard): Ensure shared label colors are updated (@geido)
- [#30967](https://github.com/apache/superset/pull/30967) fix(release validation): scripts now support RSA and EDDSA keys. (@rusackas)
- [#30881](https://github.com/apache/superset/pull/30881) fix(Dashboard): Native & Cross-Filters Scoping Performance (@geido)
- [#30887](https://github.com/apache/superset/pull/30887) fix(imports): import query_context for imports with charts (@lindenh)
- [#31008](https://github.com/apache/superset/pull/31008) fix(explore): verified props is not updated (@justinpark)
- [#30646](https://github.com/apache/superset/pull/30646) fix(Dashboard): Retain colors when color scheme not set (@geido)
- [#30962](https://github.com/apache/superset/pull/30962) fix(Dashboard): Exclude edit param in async screenshot (@geido)
**Others**
- [#32043](https://github.com/apache/superset/pull/32043) chore: Skip the creation of secondary perms during catalog migrations (@Vitor-Avila)
- [#30865](https://github.com/apache/superset/pull/30865) docs: Updating 4.1 Release Notes (@yousoph)

View File

@@ -20,8 +20,8 @@ under the License.
# Superset
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/license/apache-2-0)
[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/apache/superset?sort=semver)](https://github.com/apache/superset/tree/latest)
[![Build Status](https://github.com/apache/superset/workflows/Python/badge.svg)](https://github.com/apache/superset/actions)
[![Latest Release on Github](https://img.shields.io/github/v/release/apache/superset?sort=semver)](https://github.com/apache/superset/releases/latest)
[![Build Status](https://github.com/apache/superset/actions/workflows/superset-python-unittest.yml/badge.svg)](https://github.com/apache/superset/actions)
[![PyPI version](https://badge.fury.io/py/apache-superset.svg)](https://badge.fury.io/py/apache-superset)
[![Coverage Status](https://codecov.io/github/apache/superset/coverage.svg?branch=master)](https://codecov.io/github/apache/superset)
[![PyPI](https://img.shields.io/pypi/pyversions/apache-superset.svg?maxAge=2592000)](https://pypi.python.org/pypi/apache-superset)

View File

@@ -114,6 +114,7 @@ Join our growing community!
- [Ona](https://ona.io) [@pld]
- [Orange](https://www.orange.com) [@icsu]
- [Oslandia](https://oslandia.com)
- [Oxylabs](https://oxylabs.io/) [@rytis-ulys]
- [Peak AI](https://www.peak.ai/) [@azhar22k]
- [PeopleDoc](https://www.people-doc.com) [@rodo]
- [PlaidCloud](https://www.plaidcloud.com)

View File

@@ -33,12 +33,10 @@ assists people when migrating to a new version.
- [31794](https://github.com/apache/superset/pull/31794) Removed the previously deprecated `DASHBOARD_CROSS_FILTERS` feature flag
- [31774](https://github.com/apache/superset/pull/31774): Fixes the spelling of the `USE-ANALAGOUS-COLORS` feature flag. Please update any scripts/configuration item to use the new/corrected `USE-ANALOGOUS-COLORS` flag spelling.
- [31582](https://github.com/apache/superset/pull/31582) Removed the legacy Area, Bar, Event Flow, Heatmap, Histogram, Line, Sankey, and Sankey Loop charts. They were all automatically migrated to their ECharts counterparts with the exception of the Event Flow and Sankey Loop charts which were removed as they were not actively maintained and not widely used. If you were using the Event Flow or Sankey Loop charts, you will need to find an alternative solution.
- [31198](https://github.com/apache/superset/pull/31198) Disallows by default the use of the following ClickHouse functions: "version", "currentDatabase", "hostName".
- [29798](https://github.com/apache/superset/pull/29798) Since 3.1.0, the intial schedule for an alert or report was mistakenly offset by the specified timezone's relation to UTC. The initial schedule should now begin at the correct time.
- [30021](https://github.com/apache/superset/pull/30021) The `dev` layer in our Dockerfile no long includes firefox binaries, only Chromium to reduce bloat/docker-build-time.
- [30099](https://github.com/apache/superset/pull/30099) Translations are no longer included in the default docker image builds. If your environment requires translations, you'll want to set the docker build arg `BUILD_TRANSACTION=true`.
- [31262](https://github.com/apache/superset/pull/31262) NOTE: deprecated `pylint` in favor of `ruff` as our only python linter. Only affect development workflows positively (not the release itself). It should cover most important rules, be much faster, but some things linting rules that were enforced before may not be enforce in the exact same way as before.
- [31173](https://github.com/apache/superset/pull/31173) Modified `fetch_csrf_token` to align with HTTP standards, particularly regarding how cookies are handled. If you encounter any issues related to CSRF functionality, please report them as a new issue and reference this PR for context.
- [31413](https://github.com/apache/superset/pull/31413) Enable the DATE_FORMAT_IN_EMAIL_SUBJECT feature flag to allow users to specify a date format for the email subject, which will then be replaced with the actual date.
- [31385](https://github.com/apache/superset/pull/31385) Significant docker refactor, reducing access levels for the `superset` user, streamlining layer building, ...
- [31503](https://github.com/apache/superset/pull/31503) Deprecating python 3.9.x support, 3.11 is now the recommended version and 3.10 is still supported over the Superset 5.0 lifecycle.
@@ -51,6 +49,11 @@ assists people when migrating to a new version.
### Potential Downtime
## 4.1.2
- [31198](https://github.com/apache/superset/pull/31198) Disallows by default the use of the following ClickHouse functions: "version", "currentDatabase", "hostName".
- [31173](https://github.com/apache/superset/pull/31173) Modified `fetch_csrf_token` to align with HTTP standards, particularly regarding how cookies are handled. If you encounter any issues related to CSRF functionality, please report them as a new issue and reference this PR for context.
## 4.1.0
- [29274](https://github.com/apache/superset/pull/29274): We made it easier to trigger CI on your

View File

@@ -220,6 +220,36 @@ cache key by adding the following parameter to your Jinja code:
{{ current_user_email(add_to_cache_keys=False) }}
```
**Current User Roles**
The `{{ current_user_roles() }}` macro returns an array of roles for the logged in user.
If you have caching enabled in your Superset configuration, then by default the roles value will be used
by Superset when calculating the cache key. A cache key is a unique identifier that determines if there's a
cache hit in the future and Superset can retrieve cached data.
You can disable the inclusion of the roles value in the calculation of the
cache key by adding the following parameter to your Jinja code:
```python
{{ current_user_roles(add_to_cache_keys=False) }}
```
You can json-stringify the array by adding `|tojson` to your Jinja code:
```python
{{ current_user_roles()|tojson }}
```
You can use the `|where_in` filter to use your roles in a SQL statement. For example, if `current_user_roles()` returns `['admin', 'viewer']`, the following template:
```python
SELECT * FROM users WHERE role IN {{ current_user_roles()|where_in }}
```
Will be rendered as:
```sql
SELECT * FROM users WHERE role IN ('admin', 'viewer')
```
**Custom URL Parameters**
The `{{ url_param('custom_variable') }}` macro lets you define arbitrary URL
@@ -461,3 +491,37 @@ This macro avoids copy/paste, allowing users to centralize the metric definition
The `dataset_id` parameter is optional, and if not provided Superset will use the current dataset from context (for example, when using this macro in the Chart Builder, by default the `macro_key` will be searched in the dataset powering the chart).
The parameter can be used in SQL Lab, or when fetching a metric from another dataset.
## Available Filters
Superset supports [builtin filters from the Jinja2 templating package](https://jinja.palletsprojects.com/en/stable/templates/#builtin-filters). Custom filters have also been implemented:
**Where In**
Parses a list into a SQL-compatible statement. This is useful with macros that return an array (for example the `filter_values` macro):
```
Dashboard filter with "First", "Second" and "Third" options selected
{{ filter_values('column') }} => ["First", "Second", "Third"]
{{ filter_values('column')|where_in }} => ('First', 'Second', 'Third')
```
By default, this filter returns `()` (as a string) in case the value is null. The `default_to_none` parameter can be se to `True` to return null in this case:
```
Dashboard filter without any value applied
{{ filter_values('column') }} => ()
{{ filter_values('column')|where_in(default_to_none=True) }} => None
```
**To Datetime**
Loads a string as a `datetime` object. This is useful when performing date operations. For example:
```
{% set from_expr = get_time_filter("dttm", strftime="%Y-%m-%d").from_expr %}
{% set to_expr = get_time_filter("dttm", strftime="%Y-%m-%d").to_expr %}
{% if (to_expr|to_datetime(format="%Y-%m-%d") - from_expr|to_datetime(format="%Y-%m-%d")).days > 100 %}
do something
{% else %}
do something else
{% endif %}
```

View File

@@ -280,6 +280,49 @@ TALISMAN_CONFIG = {
"content_security_policy": { ...
```
#### Configuring Talisman in Superset
Talisman settings in Superset can be modified using superset_config.py. If you need to adjust security policies, you can override the default configuration.
Example: Overriding Talisman Configuration in superset_config.py for loading images form s3 or other external sources.
```python
TALISMAN_CONFIG = {
"content_security_policy": {
"base-uri": ["'self'"],
"default-src": ["'self'"],
"img-src": [
"'self'",
"blob:",
"data:",
"https://apachesuperset.gateway.scarf.sh",
"https://static.scarf.sh/",
# "https://cdn.brandfolder.io", # Uncomment when SLACK_ENABLE_AVATARS is True # noqa: E501
"ows.terrestris.de",
"aws.s3.com", # Add Your Bucket or external data source
],
"worker-src": ["'self'", "blob:"],
"connect-src": [
"'self'",
"https://api.mapbox.com",
"https://events.mapbox.com",
],
"object-src": "'none'",
"style-src": [
"'self'",
"'unsafe-inline'",
],
"script-src": ["'self'", "'strict-dynamic'"],
},
"content_security_policy_nonce_in": ["script-src"],
"force_https": False,
"session_cookie_secure": False,
}
```
# For more information on setting up Talisman, please refer to
https://superset.apache.org/docs/configuration/networking-settings/#changing-flask-talisman-csp
### Reporting Security Vulnerabilities
Apache Software Foundation takes a rigorous standpoint in annihilating the security issues in its

View File

@@ -339,20 +339,19 @@ const config: Config = {
async: true,
'data-website-id': 'c6a8a8b8-3127-48f9-97a7-51e9e10d20d0',
'data-project-name': 'Apache Superset',
'data-project-color': '#1AA1C2',
'data-project-color': '#FFFFFF',
'data-project-logo':
'https://images.seeklogo.com/logo-png/50/2/superset-icon-logo-png_seeklogo-500354.png',
'data-modal-override-open-id': 'ask-ai-input',
'data-modal-override-open-class': 'search-input',
'data-modal-open-by-default': 'true',
'data-modal-disclaimer':
'This is a custom LLM for Apache Superset with access to all [documentation](superset.apache.org/docs/intro/), [GitHub Open Issues, PRs and READMEs](github.com/apache/superset).&#10;&#10;Companies deploy assistants like this ([built by kapa.ai](https://kapa.ai)) on docs via [website widget](https://docs.kapa.ai/integrations/website-widget) (Docker, Reddit), in [support forms](https://docs.kapa.ai/integrations/support-form-deflector) for ticket deflection (Monday.com, Mapbox), or as [Slack bots](https://docs.kapa.ai/integrations/slack-bot) with private sources.',
'data-modal-example-questions':
'How do I use Docker Compose?,How to run Supersets on kubernetes?',
'data-button-text-color': '#FFFFFF',
'data-modal-header-bg-color': '#1AA1C2',
'data-modal-title-color': '#FFFFFF',
'data-modal-title': 'Superset Ask AI',
'How do I install Superset?,How can I contribute to Superset?',
'data-button-text-color': 'rgb(81,166,197)',
'data-modal-header-bg-color': '#ffffff',
'data-modal-title-color': 'rgb(81,166,197)',
'data-modal-title': 'Apache Superset AI',
'data-modal-disclaimer-text-color': '#000000',
'data-consent-required': 'true',
'data-consent-screen-disclaimer':

View File

@@ -58,7 +58,6 @@ ul.dropdown__menu svg {
--ifm-code-font-size: 95%;
--ifm-menu-link-padding-vertical: 12px;
--doc-sidebar-width: 350px !important;
--ifm-navbar-height: none;
--ifm-font-family-base: Roboto;
--ifm-footer-background-color: #173036;
--ifm-footer-color: #87939a;

View File

@@ -22,7 +22,7 @@ RewriteRule ^(.*)$ https://superset.apache.org/$1 [R,L]
RewriteCond %{HTTP_HOST} ^superset.incubator.apache.org$ [NC]
RewriteRule ^(.*)$ https://superset.apache.org/$1 [R=301,L]
Header set Content-Security-Policy "default-src data: blob: 'self' *.apache.org *.githubusercontent.com *.scarf.sh *.googleapis.com *.github.com *.algolia.net *.algolianet.com 'unsafe-inline' 'unsafe-eval'; frame-src *; frame-ancestors 'self' *.google.com https://sidebar.bugherd.com; form-action 'self'; worker-src blob:; img-src 'self' blob: data: https:; font-src 'self'; object-src 'none'"
Header set Content-Security-Policy "default-src data: blob: 'self' *.apache.org widget.kapa.ai *.githubusercontent.com *.scarf.sh *.googleapis.com *.google.com *.run.app *.gstatic.com *.github.com *.algolia.net *.algolianet.com 'unsafe-inline' 'unsafe-eval'; frame-src *; frame-ancestors 'self' *.google.com https://sidebar.bugherd.com; form-action 'self'; worker-src blob:; img-src 'self' blob: data: https:; font-src 'self'; object-src 'none'"
# REDIRECTS

View File

@@ -18736,7 +18736,7 @@
{
"description": "Table name",
"in": "query",
"name": "table",
"name": "name",
"required": true,
"schema": {
"type": "string"

View File

@@ -5,5 +5,5 @@ dependencies:
- name: redis
repository: oci://registry-1.docker.io/bitnamicharts
version: 17.9.4
digest: sha256:9588e2a9f15d875a95763ed7da8e92b5b48a8d13cbacd66b775eacba3e8cebcd
generated: "2024-12-29T12:19:15.365763+09:00"
digest: sha256:c6290bb7e8ce9c694c06b3f5e9b9d01401943b0943c515d3a7a3a8dc1e6492ea
generated: "2025-03-16T00:52:41.47139769+09:00"

View File

@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.14.0
version: 0.14.1
dependencies:
- name: postgresql
version: 13.4.4

View File

@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.14.0](https://img.shields.io/badge/Version-0.14.0-informational?style=flat-square)
![Version: 0.14.1](https://img.shields.io/badge/Version-0.14.1-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application

View File

@@ -812,7 +812,7 @@ postgresql:
database: superset
image:
tag: "14.6.0-debian-11-r13"
tag: "14.17.0-debian-12-r3"
## PostgreSQL Primary parameters
primary:

Binary file not shown.
1 A
2

View File

@@ -44,7 +44,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.6.0, <5.0.0",
"flask-appbuilder>=4.6.1, <5.0.0",
"flask-caching>=2.1.0, <3",
"flask-compress>=1.13, <2.0",
"flask-talisman>=1.0.0, <2.0",

View File

@@ -23,3 +23,14 @@ numexpr>=2.9.0
# 5.0.0 has a sensitive deprecation used in other libs
# -> https://github.com/aio-libs/async-timeout/blob/master/CHANGES.rst#500-2024-10-31
async_timeout>=4.0.0,<5.0.0
# Known issue with 6.7.0 breaking a unit test, probably easy to fix, but will require
# a bit of attention to bump.
apispec>=6.0.0,<6.7.0
# 1.4.0 appears to use much more memory, where the python test suite runs out of memory
# causing CI to fail. 1.3.0 is the last version that works.
# This is probably related to the changes around PickleType
# https://marshmallow-sqlalchemy.readthedocs.io/en/latest/changelog.html#id3
# Opened this issue https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues/665
marshmallow-sqlalchemy>=1.3.0,<1.4.0

View File

@@ -4,22 +4,25 @@ alembic==1.15.1
# via flask-migrate
amqp==5.3.1
# via kombu
apispec==6.3.0
# via flask-appbuilder
apsw==3.46.0.0
apispec==6.6.1
# via
# -r requirements/base.in
# flask-appbuilder
apsw==3.49.1.0
# via shillelagh
async-timeout==4.0.3
# via
# -r requirements/base.in
# redis
attrs==24.2.0
attrs==25.3.0
# via
# cattrs
# jsonschema
# outcome
# referencing
# requests-cache
# trio
babel==2.16.0
babel==2.17.0
# via flask-babel
backoff==2.2.1
# via apache-superset (pyproject.toml)
@@ -37,13 +40,13 @@ cachelib==0.13.0
# via
# flask-caching
# flask-session
cachetools==5.5.0
cachetools==5.5.2
# via google-auth
cattrs==24.1.2
# via requests-cache
celery==5.4.0
# via apache-superset (pyproject.toml)
certifi==2024.8.30
certifi==2025.1.31
# via
# requests
# selenium
@@ -51,7 +54,7 @@ cffi==1.17.1
# via
# cryptography
# pynacl
charset-normalizer==3.4.0
charset-normalizer==3.4.1
# via requests
click==8.1.8
# via
@@ -65,7 +68,7 @@ click==8.1.8
# flask-appbuilder
click-didyoumean==0.3.1
# via celery
click-option-group==0.5.6
click-option-group==0.5.7
# via apache-superset (pyproject.toml)
click-plugins==1.1.1
# via celery
@@ -86,7 +89,7 @@ cryptography==44.0.2
# pyopenssl
defusedxml==0.7.1
# via odfpy
deprecated==1.2.15
deprecated==1.2.18
# via limits
deprecation==2.1.0
# via apache-superset (pyproject.toml)
@@ -115,7 +118,7 @@ flask==2.3.3
# flask-session
# flask-sqlalchemy
# flask-wtf
flask-appbuilder==4.6.0
flask-appbuilder==4.6.1
# via apache-superset (pyproject.toml)
flask-babel==2.0.0
# via flask-appbuilder
@@ -125,7 +128,7 @@ flask-compress==1.17
# via apache-superset (pyproject.toml)
flask-jwt-extended==4.7.1
# via flask-appbuilder
flask-limiter==3.8.0
flask-limiter==3.12
# via flask-appbuilder
flask-login==0.6.3
# via
@@ -149,7 +152,7 @@ geographiclib==2.0
# via geopy
geopy==2.4.1
# via apache-superset (pyproject.toml)
google-auth==2.36.0
google-auth==2.38.0
# via shillelagh
greenlet==3.1.1
# via
@@ -164,7 +167,7 @@ hashids==1.3.1
# via apache-superset (pyproject.toml)
holidays==0.25
# via apache-superset (pyproject.toml)
humanize==4.12.1
humanize==4.12.2
# via apache-superset (pyproject.toml)
idna==3.10
# via
@@ -173,8 +176,6 @@ idna==3.10
# trio
importlib-metadata==8.6.1
# via apache-superset (pyproject.toml)
importlib-resources==6.4.5
# via limits
isodate==0.7.2
# via apache-superset (pyproject.toml)
itsdangerous==2.2.0
@@ -187,13 +188,15 @@ jinja2==3.1.6
# flask-babel
jsonpath-ng==1.7.0
# via apache-superset (pyproject.toml)
jsonschema==4.17.3
jsonschema==4.23.0
# via flask-appbuilder
kombu==5.4.2
jsonschema-specifications==2024.10.1
# via jsonschema
kombu==5.5.0
# via celery
korean-lunar-calendar==0.3.1
# via holidays
limits==3.13.0
limits==4.4.1
# via flask-limiter
mako==1.3.9
# via
@@ -209,12 +212,14 @@ markupsafe==3.0.2
# mako
# werkzeug
# wtforms
marshmallow==3.23.1
marshmallow==3.26.1
# via
# flask-appbuilder
# marshmallow-sqlalchemy
marshmallow-sqlalchemy==0.28.2
# via flask-appbuilder
marshmallow-sqlalchemy==1.3.0
# via
# -r requirements/base.in
# flask-appbuilder
mdurl==0.1.2
# via markdown-it-py
msgpack==1.0.8
@@ -248,7 +253,6 @@ packaging==24.2
# gunicorn
# limits
# marshmallow
# marshmallow-sqlalchemy
# shillelagh
pandas==2.0.3
# via apache-superset (pyproject.toml)
@@ -260,7 +264,7 @@ parsedatetime==2.6
# via apache-superset (pyproject.toml)
pgsanity==0.2.9
# via apache-superset (pyproject.toml)
platformdirs==3.9.1
platformdirs==4.3.7
# via requests-cache
ply==3.11
# via jsonpath-ng
@@ -280,7 +284,7 @@ pyasn1-modules==0.4.1
# via google-auth
pycparser==2.22
# via cffi
pygments==2.18.0
pygments==2.19.1
# via rich
pyjwt==2.10.1
# via
@@ -291,10 +295,8 @@ pynacl==1.5.0
# via paramiko
pyopenssl==25.0.0
# via shillelagh
pyparsing==3.2.1
pyparsing==3.2.2
# via apache-superset (pyproject.toml)
pyrsistent==0.20.0
# via jsonschema
pysocks==1.7.1
# via urllib3
python-dateutil==2.9.0.post0
@@ -323,19 +325,27 @@ pyyaml==6.0.2
# apispec
redis==4.6.0
# via apache-superset (pyproject.toml)
requests==2.32.2
referencing==0.36.2
# via
# jsonschema
# jsonschema-specifications
requests==2.32.3
# via
# requests-cache
# shillelagh
requests-cache==1.2.0
requests-cache==1.2.1
# via shillelagh
rich==13.9.4
# via flask-limiter
rpds-py==0.23.1
# via
# jsonschema
# referencing
rsa==4.9
# via google-auth
selenium==4.27.1
# via apache-superset (pyproject.toml)
shillelagh==1.2.18
shillelagh==1.3.5
# via apache-superset (pyproject.toml)
simplejson==3.20.1
# via apache-superset (pyproject.toml)
@@ -345,7 +355,7 @@ six==1.17.0
# python-dateutil
# url-normalize
# wtforms-json
slack-sdk==3.34.0
slack-sdk==3.35.0
# via apache-superset (pyproject.toml)
sniffio==1.3.1
# via trio
@@ -364,7 +374,7 @@ sqlalchemy-utils==0.38.3
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
sqlglot==26.1.3
sqlglot==26.11.1
# via apache-superset (pyproject.toml)
sqlparse==0.5.3
# via apache-superset (pyproject.toml)
@@ -383,9 +393,9 @@ typing-extensions==4.12.2
# apache-superset (pyproject.toml)
# alembic
# cattrs
# flask-limiter
# limits
# pyopenssl
# referencing
# rich
# selenium
# shillelagh
@@ -418,7 +428,7 @@ werkzeug==3.1.3
# flask-appbuilder
# flask-jwt-extended
# flask-login
wrapt==1.17.0
wrapt==1.17.2
# via deprecated
wsproto==1.2.0
# via trio-websocket

View File

@@ -10,11 +10,11 @@ amqp==5.3.1
# via
# -c requirements/base.txt
# kombu
apispec==6.3.0
apispec==6.6.1
# via
# -c requirements/base.txt
# flask-appbuilder
apsw==3.46.0.0
apsw==3.49.1.0
# via
# -c requirements/base.txt
# shillelagh
@@ -22,15 +22,16 @@ async-timeout==4.0.3
# via
# -c requirements/base.txt
# redis
attrs==24.2.0
attrs==25.3.0
# via
# -c requirements/base.txt
# cattrs
# jsonschema
# outcome
# referencing
# requests-cache
# trio
babel==2.16.0
babel==2.17.0
# via
# -c requirements/base.txt
# flask-babel
@@ -63,7 +64,7 @@ cachelib==0.13.0
# -c requirements/base.txt
# flask-caching
# flask-session
cachetools==5.5.0
cachetools==5.5.2
# via
# -c requirements/base.txt
# google-auth
@@ -75,7 +76,7 @@ celery==5.4.0
# via
# -c requirements/base.txt
# apache-superset
certifi==2024.8.30
certifi==2025.1.31
# via
# -c requirements/base.txt
# requests
@@ -87,7 +88,7 @@ cffi==1.17.1
# pynacl
cfgv==3.4.0
# via pre-commit
charset-normalizer==3.4.0
charset-normalizer==3.4.1
# via
# -c requirements/base.txt
# requests
@@ -106,7 +107,7 @@ click-didyoumean==0.3.1
# via
# -c requirements/base.txt
# celery
click-option-group==0.5.6
click-option-group==0.5.7
# via
# -c requirements/base.txt
# apache-superset
@@ -151,7 +152,7 @@ defusedxml==0.7.1
# via
# -c requirements/base.txt
# odfpy
deprecated==1.2.15
deprecated==1.2.18
# via
# -c requirements/base.txt
# limits
@@ -201,7 +202,7 @@ flask==2.3.3
# flask-sqlalchemy
# flask-testing
# flask-wtf
flask-appbuilder==4.6.0
flask-appbuilder==4.6.1
# via
# -c requirements/base.txt
# apache-superset
@@ -223,7 +224,7 @@ flask-jwt-extended==4.7.1
# via
# -c requirements/base.txt
# flask-appbuilder
flask-limiter==3.8.0
flask-limiter==3.12
# via
# -c requirements/base.txt
# flask-appbuilder
@@ -279,7 +280,7 @@ google-api-core==2.23.0
# google-cloud-core
# pandas-gbq
# sqlalchemy-bigquery
google-auth==2.36.0
google-auth==2.38.0
# via
# -c requirements/base.txt
# google-api-core
@@ -318,7 +319,7 @@ greenlet==3.1.1
# gevent
# shillelagh
# sqlalchemy
grpcio==1.68.0
grpcio==1.71.0
# via
# apache-superset
# google-api-core
@@ -342,7 +343,7 @@ holidays==0.25
# -c requirements/base.txt
# apache-superset
# prophet
humanize==4.12.1
humanize==4.12.2
# via
# -c requirements/base.txt
# apache-superset
@@ -358,11 +359,8 @@ importlib-metadata==8.6.1
# via
# -c requirements/base.txt
# apache-superset
importlib-resources==6.4.5
# via
# -c requirements/base.txt
# limits
# prophet
importlib-resources==6.5.2
# via prophet
iniconfig==2.0.0
# via pytest
isodate==0.7.2
@@ -383,18 +381,22 @@ jsonpath-ng==1.7.0
# via
# -c requirements/base.txt
# apache-superset
jsonschema==4.17.3
jsonschema==4.23.0
# via
# -c requirements/base.txt
# flask-appbuilder
# jsonschema-spec
# openapi-schema-validator
# openapi-spec-validator
jsonschema-spec==0.1.6
jsonschema-path==0.3.4
# via openapi-spec-validator
jsonschema-specifications==2024.10.1
# via
# -c requirements/base.txt
# jsonschema
# openapi-schema-validator
kiwisolver==1.4.7
# via matplotlib
kombu==5.4.2
kombu==5.5.0
# via
# -c requirements/base.txt
# celery
@@ -404,7 +406,7 @@ korean-lunar-calendar==0.3.1
# holidays
lazy-object-proxy==1.10.0
# via openapi-spec-validator
limits==3.13.0
limits==4.4.1
# via
# -c requirements/base.txt
# flask-limiter
@@ -428,12 +430,12 @@ markupsafe==3.0.2
# mako
# werkzeug
# wtforms
marshmallow==3.23.1
marshmallow==3.26.1
# via
# -c requirements/base.txt
# flask-appbuilder
# marshmallow-sqlalchemy
marshmallow-sqlalchemy==0.28.2
marshmallow-sqlalchemy==1.3.0
# via
# -c requirements/base.txt
# flask-appbuilder
@@ -478,9 +480,9 @@ odfpy==1.4.1
# via
# -c requirements/base.txt
# pandas
openapi-schema-validator==0.4.4
openapi-schema-validator==0.6.3
# via openapi-spec-validator
openapi-spec-validator==0.5.6
openapi-spec-validator==0.7.1
# via apache-superset
openpyxl==3.1.5
# via
@@ -506,7 +508,6 @@ packaging==24.2
# gunicorn
# limits
# marshmallow
# marshmallow-sqlalchemy
# matplotlib
# pytest
# shillelagh
@@ -533,7 +534,7 @@ parsedatetime==2.6
# -c requirements/base.txt
# apache-superset
pathable==0.4.3
# via jsonschema-spec
# via jsonschema-path
pgsanity==0.2.9
# via
# -c requirements/base.txt
@@ -542,7 +543,7 @@ pillow==10.3.0
# via
# apache-superset
# matplotlib
platformdirs==3.9.1
platformdirs==4.3.7
# via
# -c requirements/base.txt
# requests-cache
@@ -613,7 +614,7 @@ pydruid==0.6.9
# via apache-superset
pyfakefs==5.3.5
# via apache-superset
pygments==2.18.0
pygments==2.19.1
# via
# -c requirements/base.txt
# rich
@@ -635,15 +636,11 @@ pyopenssl==25.0.0
# via
# -c requirements/base.txt
# shillelagh
pyparsing==3.2.1
pyparsing==3.2.2
# via
# -c requirements/base.txt
# apache-superset
# matplotlib
pyrsistent==0.20.0
# via
# -c requirements/base.txt
# jsonschema
pysocks==1.7.1
# via
# -c requirements/base.txt
@@ -698,26 +695,32 @@ pyyaml==6.0.2
# -c requirements/base.txt
# apache-superset
# apispec
# jsonschema-spec
# jsonschema-path
# pre-commit
redis==4.6.0
# via
# -c requirements/base.txt
# apache-superset
requests==2.32.2
referencing==0.36.2
# via
# -c requirements/base.txt
# jsonschema
# jsonschema-path
# jsonschema-specifications
requests==2.32.3
# via
# -c requirements/base.txt
# docker
# google-api-core
# google-cloud-bigquery
# jsonschema-spec
# jsonschema-path
# pydruid
# pyhive
# requests-cache
# requests-oauthlib
# shillelagh
# trino
requests-cache==1.2.0
requests-cache==1.2.1
# via
# -c requirements/base.txt
# shillelagh
@@ -729,6 +732,11 @@ rich==13.9.4
# via
# -c requirements/base.txt
# flask-limiter
rpds-py==0.23.1
# via
# -c requirements/base.txt
# jsonschema
# referencing
rsa==4.9
# via
# -c requirements/base.txt
@@ -746,7 +754,7 @@ setuptools==75.6.0
# pydata-google-auth
# zope-event
# zope-interface
shillelagh==1.2.18
shillelagh==1.3.5
# via
# -c requirements/base.txt
# apache-superset
@@ -762,7 +770,7 @@ six==1.17.0
# rfc3339-validator
# url-normalize
# wtforms-json
slack-sdk==3.34.0
slack-sdk==3.35.0
# via
# -c requirements/base.txt
# apache-superset
@@ -792,7 +800,7 @@ sqlalchemy-utils==0.38.3
# -c requirements/base.txt
# apache-superset
# flask-appbuilder
sqlglot==26.1.3
sqlglot==26.11.1
# via
# -c requirements/base.txt
# apache-superset
@@ -837,9 +845,9 @@ typing-extensions==4.12.2
# alembic
# apache-superset
# cattrs
# flask-limiter
# limits
# pyopenssl
# referencing
# rich
# selenium
# shillelagh
@@ -885,7 +893,7 @@ werkzeug==3.1.3
# flask-appbuilder
# flask-jwt-extended
# flask-login
wrapt==1.17.0
wrapt==1.17.2
# via
# -c requirements/base.txt
# deprecated

View File

@@ -1,4 +1,4 @@
# This file was autogenerated by uv via the following command:
# uv pip compile requirements/translations.in -o requirements/translations.txt
babel==2.16.0
babel==2.17.0
# via -r requirements/translations.in

View File

@@ -60,7 +60,9 @@ embedDashboard({
}
},
// optional additional iframe sandbox attributes
iframeSandboxExtras: ['allow-top-navigation', 'allow-popups-to-escape-sandbox']
iframeSandboxExtras: ['allow-top-navigation', 'allow-popups-to-escape-sandbox'],
// optional config to enforce a particular referrerPolicy
referrerPolicy: "same-origin"
});
```
@@ -146,3 +148,11 @@ To pass additional sandbox attributes you can use `iframeSandboxExtras`:
// optional additional iframe sandbox attributes
iframeSandboxExtras: ['allow-top-navigation', 'allow-popups-to-escape-sandbox']
```
### Enforcing a ReferrerPolicy on the request triggered by the iframe
By default, the Embedded SDK creates an `iframe` element without a `referrerPolicy` value enforced. This means that a policy defined for `iframe` elements at the host app level would reflect to it.
This can be an issue as during the embedded enablement for a dashboard it's possible to specify which domain(s) are allowed to embed the dashboard, and this validation happens throuth the `Referrer` header. That said, in case the hosting app has a more restrictive policy that would omit this header, this validation would fail.
Use the `referrerPolicy` parameter in the `embedDashboard` method to specify [a particular policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy) that works for your implementation.

View File

@@ -64,6 +64,8 @@ export type EmbedDashboardParams = {
iframeTitle?: string
/** additional iframe sandbox attributes ex (allow-top-navigation, allow-popups-to-escape-sandbox) **/
iframeSandboxExtras?: string[]
/** force a specific refererPolicy to be used in the iframe request **/
referrerPolicy?: ReferrerPolicy
}
export type Size = {
@@ -88,7 +90,8 @@ export async function embedDashboard({
dashboardUiConfig,
debug = false,
iframeTitle = "Embedded Dashboard",
iframeSandboxExtras = []
iframeSandboxExtras = [],
referrerPolicy,
}: EmbedDashboardParams): Promise<EmbeddedDashboard> {
function log(...info: unknown[]) {
if (debug) {
@@ -142,6 +145,10 @@ export async function embedDashboard({
iframeSandboxExtras.forEach((key: string) => {
iframe.sandbox.add(key);
});
// force a specific refererPolicy to be used in the iframe request
if(referrerPolicy) {
iframe.referrerPolicy = referrerPolicy;
}
// add the event listener before setting src, to be 100% sure that we capture the load event
iframe.addEventListener('load', () => {

View File

@@ -36,6 +36,45 @@ if (process.env.NODE_ENV === 'production') {
];
}
const restrictedImportsRules = {
'no-design-icons': {
name: '@ant-design/icons',
message:
'Avoid importing icons directly from @ant-design/icons. Use the src/components/Icons component instead.',
},
'no-moment': {
name: 'moment',
message:
'Please use the dayjs library instead of moment.js. See https://day.js.org',
},
'no-lodash-memoize': {
name: 'lodash/memoize',
message: 'Lodash Memoize is unsafe! Please use memoize-one instead',
},
'no-testing-library-react': {
name: '@testing-library/react',
message: 'Please use spec/helpers/testing-library instead',
},
'no-testing-library-react-dom-utils': {
name: '@testing-library/react-dom-utils',
message: 'Please use spec/helpers/testing-library instead',
},
'no-antd': {
name: 'antd',
message: 'Please import Ant components from the index of src/components',
},
'no-antd-v5': {
name: 'antd-v5',
message: 'Please import Ant v5 components from the index of src/components',
},
'no-superset-theme': {
name: '@superset-ui/core',
importNames: ['supersetTheme'],
message:
'Please use the theme directly from the ThemeProvider rather than importing supersetTheme.',
},
};
module.exports = {
extends: [
'airbnb',
@@ -74,6 +113,7 @@ module.exports = {
'file-progress',
'lodash',
'theme-colors',
'icons',
'i18n-strings',
'react-prefer-function-component',
'prettier',
@@ -200,6 +240,13 @@ module.exports = {
message: 'Wildcard imports are not allowed',
},
],
'no-restricted-imports': [
'error',
{
paths: Object.values(restrictedImportsRules).filter(Boolean),
patterns: ['antd/*'],
},
],
},
settings: {
'import/resolver': {
@@ -210,6 +257,51 @@ module.exports = {
},
},
},
{
files: ['packages/**'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
restrictedImportsRules['no-moment'],
restrictedImportsRules['no-lodash-memoize'],
restrictedImportsRules['no-superset-theme'],
],
patterns: [],
},
],
},
},
{
files: ['plugins/**'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
restrictedImportsRules['no-moment'],
restrictedImportsRules['no-lodash-memoize'],
],
patterns: [],
},
],
},
},
{
files: ['src/components/**', 'src/theme/**'],
rules: {
'no-restricted-imports': [
'error',
{
paths: Object.values(restrictedImportsRules).filter(
r => r.name !== 'antd-v5',
),
patterns: ['antd/*'],
},
],
},
},
{
files: [
'*.test.ts',
@@ -267,6 +359,7 @@ module.exports = {
'Default React import is not required due to automatic JSX runtime in React 16.4',
},
],
'no-restricted-imports': 0,
},
},
{
@@ -284,6 +377,7 @@ module.exports = {
],
rules: {
'theme-colors/no-literal-colors': 0,
'icons/no-fa-icons-usage': 0,
'i18n-strings/no-template-vars': 0,
'no-restricted-imports': 0,
'react/no-void-elements': 0,
@@ -292,6 +386,7 @@ module.exports = {
],
rules: {
'theme-colors/no-literal-colors': 'error',
'icons/no-fa-icons-usage': 'error',
'i18n-strings/no-template-vars': ['error', true],
camelcase: [
'error',
@@ -330,42 +425,6 @@ module.exports = {
'no-nested-ternary': 0,
'no-prototype-builtins': 0,
'no-restricted-properties': 0,
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'antd',
message:
'Please import Ant components from the index of src/components',
},
{
name: 'antd-v5',
message:
'Please import Ant v5 components from the index of src/components',
},
{
name: '@superset-ui/core',
importNames: ['supersetTheme'],
message:
'Please use the theme directly from the ThemeProvider rather than importing supersetTheme.',
},
{
name: 'lodash/memoize',
message: 'Lodash Memoize is unsafe! Please use memoize-one instead',
},
{
name: '@testing-library/react',
message: 'Please use spec/helpers/testing-library instead',
},
{
name: '@testing-library/react-dom-utils',
message: 'Please use spec/helpers/testing-library instead',
},
],
patterns: ['antd/*'],
},
],
'no-shadow': 0, // re-enable up for discussion
'padded-blocks': 0,
'prefer-arrow-callback': 0,
@@ -406,6 +465,13 @@ module.exports = {
'no-promise-executor-return': 0,
'react/no-unused-class-component-methods': 0,
'react/react-in-jsx-scope': 0,
'no-restricted-imports': [
'error',
{
paths: Object.values(restrictedImportsRules).filter(Boolean),
patterns: ['antd/*'],
},
],
},
ignorePatterns,
};

View File

@@ -28,7 +28,7 @@ describe('charts list view', () => {
});
it('should load the Charts list', () => {
cy.get('[aria-label="list-view"]').click();
cy.get('[aria-label="unordered-list"]').click();
cy.eyesOpen({
testName: 'Charts list-view',
});
@@ -36,7 +36,7 @@ describe('charts list view', () => {
});
it('should load the Charts card list', () => {
cy.get('[aria-label="card-view"]').click();
cy.get('[aria-label="appstore"]').click();
cy.eyesOpen({
testName: 'Charts card-view',
});

View File

@@ -28,7 +28,7 @@ describe('dashboard list view', () => {
});
it('should load the Dashboards list', () => {
cy.get('[aria-label="list-view"]').click();
cy.get('[aria-label="unordered-list"]').click();
cy.eyesOpen({
testName: 'Dashboards list-view',
});
@@ -36,7 +36,7 @@ describe('dashboard list view', () => {
});
it('should load the Dashboards card list', () => {
cy.get('[aria-label="card-view"]').click();
cy.get('[aria-label="appstore"]').click();
cy.eyesOpen({
testName: 'Dashboards card-view',
});

View File

@@ -35,12 +35,12 @@ function orderAlphabetical() {
}
function openProperties() {
cy.get('[aria-label="more-vert"]').eq(1).click();
cy.get('[aria-label="more"]').eq(1).click();
cy.getBySel('chart-list-edit-option').click();
}
function openMenu() {
cy.get('[aria-label="more-vert"]').eq(1).click();
cy.get('[aria-label="more"]').eq(1).click();
}
function confirmDelete() {
@@ -263,7 +263,7 @@ describe('Charts list', () => {
// deletes in list-view
setGridMode('list');
cy.getBySel('table-row').eq(1).contains('2 - Sample chart');
cy.getBySel('trash').eq(1).click();
cy.getBySel('delete').eq(1).click();
confirmDelete();
cy.wait('@delete');
cy.getBySel('table-row').eq(1).should('not.contain', '2 - Sample chart');

View File

@@ -62,7 +62,7 @@ describe.skip('Dashboard top-level controls', () => {
// should allow force refresh
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
getChartAliasesBySpec(WORLD_HEALTH_CHARTS).then(aliases => {
cy.get('[aria-label="more-horiz"]').click();
cy.get('[aria-label="ellipsis"]').click();
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
'not.have.class',
'antd5-dropdown-menu-item-disabled',
@@ -91,7 +91,7 @@ describe.skip('Dashboard top-level controls', () => {
});
});
});
cy.get('[aria-label="more-horiz"]').click();
cy.get('[aria-label="ellipsis"]').click();
cy.get('[data-test="refresh-dashboard-menu-item"]').and(
'not.have.class',
'antd5-dropdown-menu-item-disabled',

View File

@@ -24,21 +24,44 @@ describe('Dashboard actions', () => {
cy.createSampleDashboards([0]);
cy.visit(SAMPLE_DASHBOARD_1);
});
it('should allow to favorite/unfavorite dashboard', () => {
interceptFav();
interceptUnfav();
// Find and click StarOutlined (adds to favorites)
cy.getBySel('dashboard-header-container')
.find("[aria-label='favorite-unselected']")
.find("[aria-label='unstarred']")
.as('starIconOutlined')
.should('exist')
.click();
cy.wait('@select');
// After clicking, StarFilled should appear
cy.getBySel('dashboard-header-container')
.find("[aria-label='favorite-selected']")
.click();
.find("[aria-label='starred']")
.as('starIconFilled')
.should('exist');
// Verify the color of the filled star (gold)
cy.get('@starIconFilled')
.should('have.css', 'color')
.and('eq', 'rgb(252, 199, 0)');
// Click on StarFilled (removes from favorites)
cy.get('@starIconFilled').click();
cy.wait('@unselect');
// After clicking, StarOutlined should reappear
cy.getBySel('dashboard-header-container')
.find("[aria-label='favorite-selected']")
.should('not.exist');
.find("[aria-label='unstarred']")
.as('starIconOutlinedAfter')
.should('exist');
// Verify the color of the outlined star (gray)
cy.get('@starIconOutlinedAfter')
.should('have.css', 'color')
.and('eq', 'rgb(178, 178, 178)');
});
});

View File

@@ -510,29 +510,29 @@ describe('Drill by modal', () => {
it('Line chart', () => {
testEchart('echarts_timeseries_line', 'Line Chart', [
[70, 93],
[70, 93],
[85, 93],
[85, 93],
]);
});
it('Area Chart', () => {
testEchart('echarts_area', 'Area Chart', [
[70, 93],
[70, 93],
[85, 93],
[85, 93],
]);
});
it('Scatter Chart', () => {
testEchart('echarts_timeseries_scatter', 'Scatter Chart', [
[70, 93],
[70, 93],
[85, 93],
[85, 93],
]);
});
it('Bar Chart', () => {
testEchart('echarts_timeseries_bar', 'Bar Chart', [
[70, 94],
[362, 68],
[85, 94],
[490, 68],
]);
});
@@ -565,22 +565,22 @@ describe('Drill by modal', () => {
it('Generic Chart', () => {
testEchart('echarts_timeseries', 'Generic Chart', [
[70, 93],
[70, 93],
[85, 93],
[85, 93],
]);
});
it('Smooth Line Chart', () => {
testEchart('echarts_timeseries_smooth', 'Smooth Line Chart', [
[70, 93],
[70, 93],
[85, 93],
[85, 93],
]);
});
it('Step Line Chart', () => {
testEchart('echarts_timeseries_step', 'Step Line Chart', [
[70, 93],
[70, 93],
[85, 93],
[85, 93],
]);
});
@@ -616,8 +616,8 @@ describe('Drill by modal', () => {
cy.get('[data-test-viz-type="mixed_timeseries"] canvas').then($canvas => {
// click 'boy'
cy.wrap($canvas).scrollIntoView();
cy.wrap($canvas).trigger('mouseover', 70, 93);
cy.wrap($canvas).rightclick(70, 93);
cy.wrap($canvas).trigger('mouseover', 85, 93);
cy.wrap($canvas).rightclick(85, 93);
drillBy('name').then(intercepted => {
const { queries } = intercepted.request.body;
@@ -650,8 +650,8 @@ describe('Drill by modal', () => {
cy.get(`[data-test="drill-by-chart"] canvas`).then($canvas => {
// click second query
cy.wrap($canvas).scrollIntoView();
cy.wrap($canvas).trigger('mouseover', 246, 114);
cy.wrap($canvas).rightclick(246, 114);
cy.wrap($canvas).trigger('mouseover', 261, 114);
cy.wrap($canvas).rightclick(261, 114);
drillBy('ds').then(intercepted => {
const { queries } = intercepted.request.body;

View File

@@ -95,24 +95,24 @@ function testTimeChart(vizType: string) {
cy.get(`[data-test-viz-type='${vizType}'] canvas`).then($canvas => {
cy.wrap($canvas).scrollIntoView();
cy.wrap($canvas).trigger('mousemove', 70, 93);
cy.wrap($canvas).rightclick(70, 93);
cy.wrap($canvas).trigger('mousemove', 85, 93);
cy.wrap($canvas).rightclick(85, 93);
drillToDetailBy('Drill to detail by 1965');
cy.getBySel('filter-val').should('contain', '1965');
closeModal();
cy.wrap($canvas).scrollIntoView();
cy.wrap($canvas).trigger('mousemove', 70, 93);
cy.wrap($canvas).rightclick(70, 93);
cy.wrap($canvas).trigger('mousemove', 85, 93);
cy.wrap($canvas).rightclick(85, 93);
drillToDetailBy('Drill to detail by boy');
cy.getBySel('filter-val').should('contain', 'boy');
closeModal();
cy.wrap($canvas).scrollIntoView();
cy.wrap($canvas).trigger('mousemove', 70, 93);
cy.wrap($canvas).rightclick(70, 93);
cy.wrap($canvas).trigger('mousemove', 85, 93);
cy.wrap($canvas).rightclick(85, 93);
drillToDetailBy('Drill to detail by all');
cy.getBySel('filter-val').first().should('contain', '1965');
@@ -151,7 +151,7 @@ describe('Drill to detail modal', () => {
cy.on('uncaught:exception', () => false);
cy.wait('@samples');
// reload
cy.get("[aria-label='reload']").click();
cy.get("[aria-label='Reload']").click();
cy.wait('@samples');
// make sure it started back from first page
cy.get('.ant-pagination-item-active').should('contain', '1');
@@ -434,7 +434,7 @@ describe('Drill to detail modal', () => {
SUPPORTED_TIER2_CHARTS.forEach(waitForChartLoad);
});
describe('Modal actions', () => {
describe.only('Modal actions', () => {
it('clears filters', () => {
interceptSamples();
@@ -442,7 +442,7 @@ describe('Drill to detail modal', () => {
cy.get("[data-test-viz-type='box_plot'] canvas").then($canvas => {
const canvasWidth = $canvas.width() || 0;
const canvasHeight = $canvas.height() || 0;
const canvasCenterX = canvasWidth / 3;
const canvasCenterX = canvasWidth / 3 + 15;
const canvasCenterY = (canvasHeight * 5) / 6;
cy.wrap($canvas).scrollIntoView();

View File

@@ -32,12 +32,12 @@ function orderAlphabetical() {
}
function openProperties() {
cy.get('[aria-label="more-vert"]').first().click();
cy.get('[aria-label="more"]').first().click();
cy.getBySel('dashboard-card-option-edit-button').click();
}
function openMenu() {
cy.get('[aria-label="more-vert"]').first().click();
cy.get('[aria-label="more"]').first().click();
}
function confirmDelete(bulk = false) {
@@ -158,17 +158,14 @@ describe('Dashboards list', () => {
cy.getBySel('styled-card').first().contains('1 - Sample dashboard');
cy.getBySel('styled-card')
.first()
.find("[aria-label='favorite-unselected']")
.find("[aria-label='unstarred']")
.click();
cy.wait('@select');
cy.getBySel('styled-card')
.first()
.find("[aria-label='favorite-selected']")
.click();
cy.getBySel('styled-card').first().find("[aria-label='starred']").click();
cy.wait('@unselect');
cy.getBySel('styled-card')
.first()
.find("[aria-label='favorite-selected']")
.find("[aria-label='starred']")
.should('not.exist');
});

View File

@@ -171,7 +171,7 @@ export const sqlLabView = {
header: '[role=columnheader]',
table: '.QueryTable',
row: dataTestLocator('table-row'),
failureMarkIcon: '[aria-label=x-small]',
failureMarkIcon: '[aria-label=close]',
successMarkIcon: '[aria-label=check]',
},
};
@@ -252,7 +252,7 @@ export const datasetsList = {
aceTextInput: '.ace_text-input',
sourceSQLInput: '.ace_content',
sourceVirtualSQLRadio: ':nth-child(2) > .ant-radio > .ant-radio-inner',
sourcePadlock: '[aria-label=lock-locked]',
sourcePadlock: '[aria-label=lock]',
legacy: {
panel: '.panel-body',
sqlInput: '#sql',
@@ -275,8 +275,8 @@ export const chartListView = {
bulkSelect: dataTestLocator('bulk-select'),
},
header: {
cardView: '[aria-label="card-view"]',
listView: '[aria-label="list-view"]',
cardView: '[aria-label="appstore"]',
listView: '[aria-label="unordered-list"]',
sort: '[class="ant-select-selection-search-input"][aria-label="Sort"]',
sortRecentlyModifiedMenuOption: '[label="Recently modified"]',
sortAlphabeticalMenuOption: '[label="Alphabetical"]',
@@ -286,8 +286,6 @@ export const chartListView = {
card: dataTestLocator('styled-card'),
cardCover: '[class="antd5-card-cover"]',
cardImage: '[class="gradient-container"]',
selectedStarIcon: "[aria-label='favorite-selected']",
unselectedStarIcon: "[aria-label='favorite-unselected']",
starIcon: dataTestLocator('fave-unfave-icon'),
},
deleteModal: {
@@ -330,7 +328,7 @@ export const nativeFilters = {
filterItemsContainer: dataTestLocator('filter-title-container'),
tabsContainer: '[class="ant-tabs-nav-list"]',
tab: '.ant-tabs-tab',
removeTab: '[aria-label="trash"]',
removeTab: '[aria-label="delete"]',
},
addFilter: dataTestLocator('add-filter-button'),
defaultValueCheck: '.ant-checkbox-checked',
@@ -375,7 +373,7 @@ export const nativeFilters = {
listItemNotActive: '[class="ant-tabs-tab ant-tabs-tab-with-remove"]',
listItemActive:
'[class="ant-tabs-tab ant-tabs-tab-with-remove ant-tabs-tab-active"]',
removeIcon: '[aria-label="trash"]',
removeIcon: '[aria-label="delete"]',
},
filterItem: dataTestLocator('form-item-value'),
filterItemDropdown: '.ant-select-selection-search',
@@ -402,8 +400,8 @@ export const dashboardListView = {
card: dataTestLocator('styled-card'),
cardCover: '[class="antd5-card-cover"]',
cardImage: '[class="gradient-container"]',
selectedStarIcon: "[aria-label='favorite-selected']",
unselectedStarIcon: "[aria-label='favorite-unselected']",
selectedStarIcon: "[aria-label='star']",
unselectedStarIcon: "[aria-label='star']",
starIcon: dataTestLocator('fave-unfave-icon'),
},
deleteModal: {
@@ -412,8 +410,8 @@ export const dashboardListView = {
},
table: {
starIcon: dataTestLocator('fave-unfave-icon'),
selectedStarIcon: "[aria-label='favorite-selected']",
unselectedStarIcon: "[aria-label='favorite-unselected']",
selectedStarIcon: "[aria-label='star']",
unselectedStarIcon: "[aria-label='star']",
bulkSelect: {
checkboxOff: '[aria-label="checkbox-off"]',
checkboxOn: '[aria-label="checkbox-on"]',
@@ -438,8 +436,8 @@ export const dashboardListView = {
importButton: dataTestLocator('modal-confirm-button'),
},
header: {
cardView: '[aria-label="card-view"]',
listView: '[aria-label="list-view"]',
cardView: '[aria-label="appstore"]',
listView: '[aria-label="unordered-list"]',
sort: dataTestLocator('sort-header'),
sortDropdown: '.Select__menu',
statusFilterInput: `${dataTestLocator(
@@ -503,7 +501,7 @@ export const exploreView = {
optionField: dataTestLocator('option-label'),
fieldInput: '.Select__control input',
removeFieldValue: dataTestLocator('remove-control-button'),
addFieldValue: '[aria-label="plus-small"]',
addFieldValue: '[aria-label="plus"]',
vizType: dataTestLocator('visualization-type'),
runButton: dataTestLocator('run-query-button'),
saveQuery: dataTestLocator('query-save-button'),

View File

@@ -25,8 +25,14 @@ export interface ChartSpec {
viz: string;
}
const viewTypeIcons = {
card: 'appstore',
list: 'unordered-list',
};
export function setGridMode(type: 'card' | 'list') {
cy.get(`[aria-label="${type}-view"]`).click();
const icon = viewTypeIcons[type];
cy.get(`[aria-label="${icon}"]`).click();
}
export function toggleBulkSelect() {

View File

@@ -0,0 +1,68 @@
/**
* 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.
*/
/**
* @fileoverview Rule to warn about direct imports from @ant-design/icons
* @author Apache
*/
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
rules: {
'no-fa-icons-usage': {
meta: {
type: 'problem',
docs: {
description:
'Disallow the usage of FontAwesome icons in the codebase',
category: 'Best Practices',
},
schema: [],
},
create(context) {
return {
// Check for JSX elements with class names containing "fa"
JSXElement(node) {
if (
node.openingElement &&
node.openingElement.name.name === 'i' &&
node.openingElement.attributes &&
node.openingElement.attributes.some(
attr =>
attr.name &&
attr.name.name === 'className' &&
/fa fa-/.test(attr.value.value),
)
) {
context.report({
node,
message:
'FontAwesome icons should not be used. Use the src/components/Icons component instead.',
});
}
},
};
},
},
},
};

View File

@@ -0,0 +1,61 @@
/**
* 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.
*/
/**
* @fileoverview Test file for the no-fa-icons-usage rule
* @author Apache
*/
const { RuleTester } = require('eslint');
const plugin = require('.');
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
const rule = plugin.rules['no-fa-icons-usage'];
const errors = [
{
message:
'FontAwesome icons should not be used. Use the src/components/Icons component instead.',
},
];
ruleTester.run('no-fa-icons-usage', rule, {
valid: ['<Icons.Database />', '<Icons.Search />'],
invalid: [
{
code: '<i className="fa fa-database"></i>',
errors,
},
{
code: '<i className="fa fa-search"></i>',
errors,
},
{
code: '<i className="fa fa-home"></i>',
errors,
},
{
code: '<i className="fa fa-arrow-right"></i>',
errors,
},
],
});

View File

@@ -0,0 +1,16 @@
{
"name": "eslint-plugin-icons",
"version": "1.0.0",
"description": "Warns about direct usage of Ant Design icons",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"license": "Apache-2.0",
"author": "Apache",
"dependencies": {},
"peerDependencies": {
"eslint": ">=0.8.0"
}
}

View File

@@ -243,6 +243,7 @@
"eslint-import-resolver-typescript": "^3.7.0",
"eslint-plugin-cypress": "^3.6.0",
"eslint-plugin-file-progress": "^1.5.0",
"eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jest": "^27.8.0",
"eslint-plugin-jest-dom": "^5.5.0",
@@ -272,7 +273,7 @@
"mini-css-extract-plugin": "^2.9.0",
"open-cli": "^8.0.0",
"po2json": "^0.4.5",
"prettier": "3.3.3",
"prettier": "3.5.3",
"prettier-plugin-packagejson": "^2.5.3",
"process": "^0.11.10",
"react-resizable": "^3.0.5",
@@ -317,6 +318,14 @@
"eslint": ">=0.8.0"
}
},
"eslint-rules/eslint-plugin-icons": {
"version": "1.0.0",
"dev": true,
"license": "Apache-2.0",
"peerDependencies": {
"eslint": ">=0.8.0"
}
},
"eslint-rules/eslint-plugin-theme-colors": {
"version": "1.0.0",
"dev": true,
@@ -3244,9 +3253,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
"integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
@@ -17353,9 +17362,9 @@
"license": "CC-BY-4.0"
},
"node_modules/canvg": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -20646,6 +20655,16 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/dompurify": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
"integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
@@ -21906,6 +21925,10 @@
"resolved": "eslint-rules/eslint-plugin-i18n-strings",
"link": true
},
"node_modules/eslint-plugin-icons": {
"resolved": "eslint-rules/eslint-plugin-icons",
"link": true
},
"node_modules/eslint-plugin-import": {
"version": "2.31.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
@@ -30329,30 +30352,23 @@
}
},
"node_modules/jspdf": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz",
"integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz",
"integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2",
"@babel/runtime": "^7.26.7",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.6",
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^2.5.4",
"dompurify": "^3.2.4",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jspdf/node_modules/dompurify": {
"version": "2.5.8",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz",
"integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true
},
"node_modules/jsprim": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
@@ -37462,9 +37478,9 @@
}
},
"node_modules/prettier": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"devOptional": true,
"license": "MIT",
"bin": {
@@ -50711,6 +50727,7 @@
"version": "0.20.3",
"license": "Apache-2.0",
"dependencies": {
"@types/react-redux": "^7.1.10",
"d3-array": "^1.2.0",
"dayjs": "^1.11.13",
"lodash": "^4.17.21"

View File

@@ -310,6 +310,7 @@
"eslint-import-resolver-typescript": "^3.7.0",
"eslint-plugin-cypress": "^3.6.0",
"eslint-plugin-file-progress": "^1.5.0",
"eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jest": "^27.8.0",
"eslint-plugin-jest-dom": "^5.5.0",
@@ -339,7 +340,7 @@
"mini-css-extract-plugin": "^2.9.0",
"open-cli": "^8.0.0",
"po2json": "^0.4.5",
"prettier": "3.3.3",
"prettier": "3.5.3",
"prettier-plugin-packagejson": "^2.5.3",
"process": "^0.11.10",
"react-resizable": "^3.0.5",
@@ -372,7 +373,8 @@
"core-js": "^3.38.1",
"d3-color": "^3.1.0",
"puppeteer": "^22.4.1",
"underscore": "^1.13.7"
"underscore": "^1.13.7",
"jspdf": "^3.0.1"
},
"readme": "ERROR: No README data found!",
"scarfSettings": {

View File

@@ -19,12 +19,14 @@
*/
import { ReactNode } from 'react';
import { css, GenericDataType, styled, t } from '@superset-ui/core';
import { ClockCircleOutlined, QuestionOutlined } from '@ant-design/icons';
// TODO: move all icons to superset-ui/core
import FunctionSvg from './type-icons/field_derived.svg';
import BooleanSvg from './type-icons/field_boolean.svg';
import StringSvg from './type-icons/field_abc.svg';
import NumSvg from './type-icons/field_num.svg';
import {
ClockCircleOutlined,
QuestionOutlined,
FunctionOutlined,
FieldBinaryOutlined,
FieldStringOutlined,
NumberOutlined,
} from '@ant-design/icons';
export type ColumnLabelExtendedType = 'expression' | '';
@@ -56,13 +58,13 @@ export function ColumnTypeLabel({ type }: ColumnTypeLabelProps) {
);
if (type === '' || type === 'expression') {
typeIcon = <FunctionSvg aria-label={t('function type icon')} />;
typeIcon = <FunctionOutlined aria-label={t('function type icon')} />;
} else if (type === GenericDataType.String) {
typeIcon = <StringSvg aria-label={t('string type icon')} />;
typeIcon = <FieldStringOutlined aria-label={t('string type icon')} />;
} else if (type === GenericDataType.Numeric) {
typeIcon = <NumSvg aria-label={t('numeric type icon')} />;
typeIcon = <NumberOutlined aria-label={t('numeric type icon')} />;
} else if (type === GenericDataType.Boolean) {
typeIcon = <BooleanSvg aria-label={t('boolean type icon')} />;
typeIcon = <FieldBinaryOutlined aria-label={t('boolean type icon')} />;
} else if (type === GenericDataType.Temporal) {
typeIcon = <ClockCircleOutlined aria-label={t('temporal type icon')} />;
}

View File

@@ -105,6 +105,8 @@ export function ControlHeader({
{warning && (
<span>
<Tooltip id="error-tooltip" placement="top" title={warning}>
{/* TODO: Remove fa-icon */}
{/* eslint-disable-next-line icons/no-fa-icons-usage */}
<i className="fa fa-exclamation-circle text-warning" />
</Tooltip>{' '}
</span>
@@ -112,6 +114,8 @@ export function ControlHeader({
{danger && (
<span>
<Tooltip id="error-tooltip" placement="top" title={danger}>
{/* TODO: Remove fa-icon */}
{/* eslint-disable-next-line icons/no-fa-icons-usage */}
<i className="fa fa-exclamation-circle text-danger" />
</Tooltip>{' '}
</span>
@@ -123,6 +127,8 @@ export function ControlHeader({
placement="top"
title={validationErrors.join(' ')}
>
{/* TODO: Remove fa-icon */}
{/* eslint-disable-next-line icons/no-fa-icons-usage */}
<i className="fa fa-exclamation-circle text-danger" />
</Tooltip>{' '}
</span>

View File

@@ -16,11 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import { styled } from '@superset-ui/core';
import { styled, css } from '@superset-ui/core';
export const ControlSubSectionHeader = styled.div`
font-weight: ${({ theme }) => theme.typography.weights.bold};
font-size: ${({ theme }) => theme.typography.sizes.s};
margin-bottom: ${({ theme }) => theme.gridUnit}px;
${({ theme }) => css`
font-weight: ${theme.typography.weights.bold};
font-size: ${theme.typography.sizes.s};
margin-bottom: ${theme.gridUnit}px;
`}
`;
export default ControlSubSectionHeader;

View File

@@ -0,0 +1,58 @@
/**
* 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 limitationsxw
* under the License.
*/
import {
getMetricLabel,
ensureIsArray,
PostProcessingAggregation,
QueryFormData,
Aggregates,
} from '@superset-ui/core';
import { PostProcessingFactory } from './types';
export const aggregationOperator: PostProcessingFactory<
PostProcessingAggregation
> = (formData: QueryFormData, queryObject) => {
const { aggregation = 'LAST_VALUE' } = formData;
if (aggregation === 'LAST_VALUE') {
return undefined;
}
const metrics = ensureIsArray(queryObject.metrics);
if (metrics.length === 0) {
return undefined;
}
const aggregates: Aggregates = {};
metrics.forEach(metric => {
const metricLabel = getMetricLabel(metric);
aggregates[metricLabel] = {
operator: aggregation,
column: metricLabel,
};
});
return {
operation: 'aggregate',
options: {
groupby: [],
aggregates,
},
};
};

View File

@@ -21,6 +21,7 @@ export { rollingWindowOperator } from './rollingWindowOperator';
export { timeCompareOperator } from './timeCompareOperator';
export { timeComparePivotOperator } from './timeComparePivotOperator';
export { sortOperator } from './sortOperator';
export { aggregationOperator } from './aggregateOperator';
export { histogramOperator } from './histogramOperator';
export { pivotOperator } from './pivotOperator';
export { resampleOperator } from './resampleOperator';

View File

@@ -84,7 +84,7 @@ export const titleControls: ControlPanelSectionConfig = {
clearable: true,
label: t('Y Axis Title Margin'),
renderTrigger: true,
default: TITLE_MARGIN_OPTIONS[0],
default: TITLE_MARGIN_OPTIONS[1],
choices: formatSelectOptions(TITLE_MARGIN_OPTIONS),
description: t('Changing this control takes effect instantly'),
},

View File

@@ -61,6 +61,32 @@ const xAxisSortVisibility = ({ controls }: { controls: ControlStateMapping }) =>
ensureIsArray(controls?.groupby?.value).length === 0 &&
ensureIsArray(controls?.metrics?.value).length === 1;
// TODO: Expand this aggregation options list to include all backend-supported aggregations.
// TODO: Migrate existing chart types (Pivot Table, etc.) to use this shared control.
export const aggregationControl = {
name: 'aggregation',
config: {
type: 'SelectControl',
label: t('Aggregation Method'),
default: 'LAST_VALUE',
clearable: false,
renderTrigger: false,
choices: [
['LAST_VALUE', t('Last Value')],
['sum', t('Total (Sum)')],
['mean', t('Average (Mean)')],
['min', t('Minimum')],
['max', t('Maximum')],
['median', t('Median')],
],
description: t('Select an aggregation method to apply to the metric.'),
provideFormDataToProps: true,
mapStateToProps: ({ form_data }: ControlPanelState) => ({
value: form_data.aggregation || 'LAST_VALUE',
}),
},
};
const xAxisMultiSortVisibility = ({
controls,
}: {

View File

@@ -19,6 +19,7 @@
export { default as sharedControls } from './sharedControls';
// React control components
export { default as sharedControlComponents } from './components';
export { aggregationControl } from './customControls';
export * from './components';
export * from './customControls';
export * from './mixins';

View File

@@ -78,6 +78,7 @@ export const D3_TIME_FORMAT_OPTIONS: [string, string][] = [
[SMART_DATE_ID, t('Adaptive formatting')],
['%d/%m/%Y', '%d/%m/%Y | 14/01/2019'],
['%m/%d/%Y', '%m/%d/%Y | 01/14/2019'],
['%d.%m.%Y', '%d.%m.%Y | 14.01.2019'],
['%Y-%m-%d', '%Y-%m-%d | 2019-01-14'],
['%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M:%S | 2019-01-14 01:32:10'],
['%d-%m-%Y %H:%M:%S', '%d-%m-%Y %H:%M:%S | 14-01-2019 01:32:10'],

View File

@@ -0,0 +1,121 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { QueryObject, SqlaFormData, VizType } from '@superset-ui/core';
import { aggregationOperator } from '@superset-ui/chart-controls';
describe('aggregationOperator', () => {
const formData: SqlaFormData = {
metrics: [
'count(*)',
{ label: 'sum(val)', expressionType: 'SQL', sqlExpression: 'sum(val)' },
],
time_range: '2015 : 2016',
granularity: 'month',
datasource: 'foo',
viz_type: VizType.Table,
};
const queryObject: QueryObject = {
metrics: [
'count(*)',
{ label: 'sum(val)', expressionType: 'SQL', sqlExpression: 'sum(val)' },
],
time_range: '2015 : 2016',
granularity: 'month',
};
test('should return undefined for LAST_VALUE aggregation', () => {
const formDataWithLastValue = {
...formData,
aggregation: 'LAST_VALUE',
};
expect(
aggregationOperator(formDataWithLastValue, queryObject),
).toBeUndefined();
});
test('should return undefined when metrics is empty', () => {
const queryObjectWithoutMetrics = {
...queryObject,
metrics: [],
};
const formDataWithSum = {
...formData,
aggregation: 'sum',
};
expect(
aggregationOperator(formDataWithSum, queryObjectWithoutMetrics),
).toBeUndefined();
});
test('should apply sum aggregation to all metrics', () => {
const formDataWithSum = {
...formData,
aggregation: 'sum',
};
expect(aggregationOperator(formDataWithSum, queryObject)).toEqual({
operation: 'aggregate',
options: {
groupby: [],
aggregates: {
'count(*)': {
operator: 'sum',
column: 'count(*)',
},
'sum(val)': {
operator: 'sum',
column: 'sum(val)',
},
},
},
});
});
test('should apply mean aggregation to all metrics', () => {
const formDataWithMean = {
...formData,
aggregation: 'mean',
};
expect(aggregationOperator(formDataWithMean, queryObject)).toEqual({
operation: 'aggregate',
options: {
groupby: [],
aggregates: {
'count(*)': {
operator: 'mean',
column: 'count(*)',
},
'sum(val)': {
operator: 'mean',
column: 'sum(val)',
},
},
},
});
});
test('should use default aggregation when not specified', () => {
expect(aggregationOperator(formData, queryObject)).toBeUndefined();
});
});

View File

@@ -54,7 +54,7 @@ const queryObject: QueryObject = {
},
},
{
operation: 'aggregation',
operation: 'aggregate',
options: {
groupby: ['col1'],
aggregates: {},

View File

@@ -67,7 +67,7 @@ export interface Aggregates {
export type DefaultPostProcessing = undefined;
interface _PostProcessingAggregation {
operation: 'aggregation';
operation: 'aggregate';
options: {
groupby: string[];
aggregates: Aggregates;
@@ -271,7 +271,7 @@ export type PostProcessingRule =
export function isPostProcessingAggregation(
rule?: PostProcessingRule,
): rule is PostProcessingAggregation {
return rule?.operation === 'aggregation';
return rule?.operation === 'aggregate';
}
export function isPostProcessingBoxplot(

View File

@@ -61,7 +61,7 @@ const AGGREGATES_OPTION: Aggregates = {
};
const AGGREGATE_RULE: PostProcessingAggregation = {
operation: 'aggregation',
operation: 'aggregate',
options: {
groupby: ['foo'],
aggregates: AGGREGATES_OPTION,

View File

@@ -63,6 +63,11 @@ const form_data = {
header_font_size: 60,
subheader_font_size: 26,
comparison_color_enabled: true,
column_config: {
name: {
visible: true,
},
},
extra_form_data: {},
force: false,
result_format: 'json',
@@ -142,7 +147,7 @@ describe('getComparisonInfo', () => {
expect(resultFormData.adhoc_filters?.[0]).toEqual(expectedFilters[0]);
});
it('If adhoc_filter is undefrined the code wont break', () => {
it('If adhoc_filter is undefined the code wont break', () => {
const resultFormData = getComparisonInfo(
{
...form_data,
@@ -175,4 +180,21 @@ describe('getComparisonInfo', () => {
expect(resultFormData.adhoc_filters?.length).toEqual(1);
expect(resultFormData.adhoc_filters).toEqual(expectedFilters);
});
it('Updates comparison display values when toggled', () => {
const resultFormData = getComparisonInfo(
{
...form_data,
column_config: {
name: {
visible: false,
},
},
},
ComparisonTimeRangeType.Year,
{},
);
expect(resultFormData.column_config.name.visible).toEqual(false);
});
});

View File

@@ -24,9 +24,10 @@
"lib"
],
"dependencies": {
"@types/react-redux": "^7.1.10",
"d3-array": "^1.2.0",
"lodash": "^4.17.21",
"dayjs": "^1.11.13"
"dayjs": "^1.11.13",
"lodash": "^4.17.21"
},
"peerDependencies": {
"@superset-ui/chart-controls": "*",

View File

@@ -200,16 +200,19 @@ export default function PopKPI(props: PopKPIProps) {
symbol: '#',
value: prevNumber,
tooltipText: t('Data for %s', comparisonRange || 'previous range'),
columnKey: 'Previous value',
},
{
symbol: '△',
value: valueDifference,
tooltipText: t('Value difference between the time periods'),
columnKey: 'Delta',
},
{
symbol: '%',
value: percentDifferenceFormattedString,
tooltipText: t('Percentage difference between the time periods'),
columnKey: 'Percent change',
},
],
[
@@ -220,6 +223,10 @@ export default function PopKPI(props: PopKPIProps) {
],
);
const visibleSymbols = SYMBOLS_WITH_VALUES.filter(
symbol => props.columnConfig?.[symbol.columnKey]?.visible !== false,
);
const { isOverflowing, symbolContainerRef, wrapperRef } =
useOverflowDetection(flexGap);
@@ -244,51 +251,53 @@ export default function PopKPI(props: PopKPIProps) {
)}
</div>
<div
css={[
css`
display: flex;
justify-content: space-around;
gap: ${flexGap}px;
min-width: 0;
flex-shrink: 1;
`,
isOverflowing
? css`
flex-direction: column;
align-items: flex-start;
width: fit-content;
`
: css`
align-items: center;
width: 100%;
`,
]}
ref={symbolContainerRef}
>
{SYMBOLS_WITH_VALUES.map((symbol_with_value, index) => (
<ComparisonValue
key={`comparison-symbol-${symbol_with_value.symbol}`}
subheaderFontSize={subheaderFontSize}
>
<Tooltip
id="tooltip"
placement="top"
title={symbol_with_value.tooltipText}
{visibleSymbols.length > 0 && (
<div
css={[
css`
display: flex;
justify-content: space-around;
gap: ${flexGap}px;
min-width: 0;
flex-shrink: 1;
`,
isOverflowing
? css`
flex-direction: column;
align-items: flex-start;
width: fit-content;
`
: css`
align-items: center;
width: 100%;
`,
]}
ref={symbolContainerRef}
>
{visibleSymbols.map((symbol_with_value, index) => (
<ComparisonValue
key={`comparison-symbol-${symbol_with_value.symbol}`}
subheaderFontSize={subheaderFontSize}
>
<SymbolWrapper
backgroundColor={
index > 0 ? backgroundColor : defaultBackgroundColor
}
textColor={index > 0 ? textColor : defaultTextColor}
<Tooltip
id="tooltip"
placement="top"
title={symbol_with_value.tooltipText}
>
{symbol_with_value.symbol}
</SymbolWrapper>
{symbol_with_value.value}
</Tooltip>
</ComparisonValue>
))}
</div>
<SymbolWrapper
backgroundColor={
index > 0 ? backgroundColor : defaultBackgroundColor
}
textColor={index > 0 ? textColor : defaultTextColor}
>
{symbol_with_value.symbol}
</SymbolWrapper>
{symbol_with_value.value}
</Tooltip>
</ComparisonValue>
))}
</div>
)}
</NumbersContainer>
</div>
);

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { t } from '@superset-ui/core';
import { t, GenericDataType } from '@superset-ui/core';
import {
ControlPanelConfig,
getStandardizedControls,
@@ -106,6 +106,42 @@ const config: ControlPanelConfig = {
},
},
],
[
{
name: 'column_config',
config: {
type: 'ColumnConfigControl',
label: t('Customize columns'),
description: t('Further customize how to display each column'),
width: 400,
height: 320,
renderTrigger: true,
configFormLayout: {
[GenericDataType.Numeric]: [
{
tab: t('General'),
children: [['visible']],
},
],
},
shouldMapStateToProps() {
return true;
},
mapStateToProps(explore, _, chart) {
return {
columnsPropsObject: {
colnames: ['Previous value', 'Delta', 'Percent change'],
coltypes: [
GenericDataType.Numeric,
GenericDataType.Numeric,
GenericDataType.Numeric,
],
},
};
},
},
},
],
],
},
sections.timeComparisonControls({

View File

@@ -89,6 +89,7 @@ export default function transformProps(chartProps: ChartProps) {
comparisonColorScheme,
comparisonColorEnabled,
percentDifferenceFormat,
columnConfig,
} = formData;
const { data: dataA = [] } = queriesData[0];
const data = dataA;
@@ -193,5 +194,6 @@ export default function transformProps(chartProps: ChartProps) {
startDateOffset,
shift: timeComparison,
dashboardTimeRange: formData?.extraFormData?.time_range,
columnConfig,
};
}

View File

@@ -33,6 +33,10 @@ export interface PopKPIStylesProps {
comparisonColorEnabled: boolean;
}
export type TableColumnConfig = {
visible?: boolean;
};
interface PopKPICustomizeProps {
headerText: string;
}
@@ -66,6 +70,7 @@ export type PopKPIProps = PopKPIStylesProps &
startDateOffset?: string;
shift: string;
dashboardTimeRange?: string;
columnConfig?: Record<string, TableColumnConfig>;
};
export enum ColorSchemeEnum {

View File

@@ -24,6 +24,7 @@ import {
QueryFormData,
} from '@superset-ui/core';
import {
aggregationOperator,
flattenOperator,
pivotOperator,
resampleOperator,
@@ -47,5 +48,19 @@ export default function buildQuery(formData: QueryFormData) {
flattenOperator(formData, baseQueryObject),
],
},
{
...baseQueryObject,
columns: [
...(isXAxisSet(formData)
? ensureIsArray(getXAxisColumn(formData))
: []),
],
...(isXAxisSet(formData) ? {} : { is_timeseries: true }),
post_processing: [
pivotOperator(formData, baseQueryObject),
aggregationOperator(formData, baseQueryObject),
],
},
]);
}

View File

@@ -18,6 +18,7 @@
*/
import { SMART_DATE_ID, t } from '@superset-ui/core';
import {
aggregationControl,
ControlPanelConfig,
ControlSubSectionHeader,
D3_FORMAT_DOCS,
@@ -35,6 +36,7 @@ const config: ControlPanelConfig = {
controlSetRows: [
['x_axis'],
['time_grain_sqla'],
[aggregationControl],
['metric'],
['adhoc_filters'],
],

View File

@@ -66,6 +66,7 @@ export default function transformProps(
metric = 'value',
showTimestamp,
showTrendLine,
aggregation,
startYAxisAtZero,
subheader = '',
subheaderFontSize,
@@ -82,6 +83,15 @@ export default function transformProps(
from_dttm: fromDatetime,
to_dttm: toDatetime,
} = queriesData[0];
const aggregatedQueryData = queriesData.length > 1 ? queriesData[1] : null;
const hasAggregatedData =
aggregatedQueryData?.data &&
aggregatedQueryData.data.length > 0 &&
aggregation !== 'LAST_VALUE';
const aggregatedData = hasAggregatedData ? aggregatedQueryData.data[0] : null;
const refs: Refs = {};
const metricName = getMetricLabel(metric);
const compareLag = Number(compareLag_) || 0;
@@ -95,18 +105,39 @@ export default function transformProps(
let percentChange = 0;
let bigNumber = data.length === 0 ? null : data[0][metricName];
let timestamp = data.length === 0 ? null : data[0][xAxisLabel];
let bigNumberFallback;
const metricColtypeIndex = colnames.findIndex(name => name === metricName);
const metricColtype =
metricColtypeIndex > -1 ? coltypes[metricColtypeIndex] : null;
let bigNumberFallback = null;
let sortedData: [number | null, number | null][] = [];
if (data.length > 0) {
const sortedData = (data as BigNumberDatum[])
.map(d => [d[xAxisLabel], parseMetricValue(d[metricName])])
sortedData = (data as BigNumberDatum[])
.map(
d =>
[d[xAxisLabel], parseMetricValue(d[metricName])] as [
number | null,
number | null,
],
)
// sort in time descending order
.sort((a, b) => (a[0] !== null && b[0] !== null ? b[0] - a[0] : 0));
}
if (hasAggregatedData && aggregatedData) {
if (
aggregatedData[metricName] !== null &&
aggregatedData[metricName] !== undefined
) {
bigNumber = aggregatedData[metricName];
} else {
const metricKeys = Object.keys(aggregatedData).filter(
key =>
key !== xAxisLabel &&
aggregatedData[key] !== null &&
typeof aggregatedData[key] === 'number',
);
bigNumber = metricKeys.length > 0 ? aggregatedData[metricKeys[0]] : null;
}
timestamp = sortedData.length > 0 ? sortedData[0][0] : null;
} else if (sortedData.length > 0) {
bigNumber = sortedData[0][1];
timestamp = sortedData[0][0];
@@ -115,25 +146,28 @@ export default function transformProps(
bigNumber = bigNumberFallback ? bigNumberFallback[1] : null;
timestamp = bigNumberFallback ? bigNumberFallback[0] : null;
}
}
if (compareLag > 0) {
const compareIndex = compareLag;
if (compareIndex < sortedData.length) {
const compareValue = sortedData[compareIndex][1];
// compare values must both be non-nulls
if (bigNumber !== null && compareValue !== null) {
percentChange = compareValue
? (bigNumber - compareValue) / Math.abs(compareValue)
: 0;
formattedSubheader = `${formatPercentChange(
percentChange,
)} ${compareSuffix}`;
}
if (compareLag > 0 && sortedData.length > 0) {
const compareIndex = compareLag;
if (compareIndex < sortedData.length) {
const compareValue = sortedData[compareIndex][1];
// compare values must both be non-nulls
if (bigNumber !== null && compareValue !== null) {
percentChange = compareValue
? (Number(bigNumber) - compareValue) / Math.abs(compareValue)
: 0;
formattedSubheader = `${formatPercentChange(
percentChange,
)} ${compareSuffix}`;
}
}
sortedData.reverse();
}
if (data.length > 0) {
const reversedData = [...sortedData].reverse();
// @ts-ignore
trendLineData = showTrendLine ? sortedData : undefined;
trendLineData = showTrendLine ? reversedData : undefined;
}
let className = '';
@@ -143,6 +177,10 @@ export default function transformProps(
className = 'negative';
}
const metricColtypeIndex = colnames.findIndex(name => name === metricName);
const metricColtype =
metricColtypeIndex > -1 ? coltypes[metricColtypeIndex] : null;
let metricEntry: Metric | undefined;
if (chartProps.datasource?.metrics) {
metricEntry = chartProps.datasource.metrics.find(

View File

@@ -201,7 +201,7 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
name: bubbleXAxisTitle,
nameLocation: 'middle',
nameTextStyle: {
fontWight: 'bolder',
fontWeight: 'bolder',
},
nameGap: convertInteger(xAxisTitleMargin),
type: xAxisType,
@@ -219,7 +219,7 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
name: bubbleYAxisTitle,
nameLocation: 'middle',
nameTextStyle: {
fontWight: 'bolder',
fontWeight: 'bolder',
},
nameGap: convertInteger(yAxisTitleMargin),
min: yAxisMin,

View File

@@ -122,7 +122,7 @@ function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] {
clearable: true,
label: t('AXIS TITLE MARGIN'),
renderTrigger: true,
default: sections.TITLE_MARGIN_OPTIONS[0],
default: sections.TITLE_MARGIN_OPTIONS[1],
choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS),
description: t('Changing this control takes effect instantly'),
visibility: ({ controls }: ControlPanelsContainerProps) =>

View File

@@ -27,8 +27,10 @@ import {
Ref,
} from 'react';
import { useSelector } from 'react-redux';
import { styled } from '@superset-ui/core';
import { use, init, EChartsType } from 'echarts/core';
import { use, init, EChartsType, registerLocale } from 'echarts/core';
import {
SankeyChart,
PieChart,
@@ -60,6 +62,15 @@ import {
} from 'echarts/components';
import { LabelLayout } from 'echarts/features';
import { EchartsHandler, EchartsProps, EchartsStylesProps } from '../types';
import { DEFAULT_LOCALE } from '../constants';
// Define this interface here to avoid creating a dependency back to superset-frontend,
// TODO: to move the type to @superset-ui/core
interface ExplorePageState {
common: {
locale: string;
};
}
const Styles = styled.div<EchartsStylesProps>`
height: ${({ height }) => height};
@@ -123,24 +134,52 @@ function Echart(
getEchartInstance: () => chartRef.current,
}));
const locale = useSelector(
(state: ExplorePageState) => state?.common?.locale ?? DEFAULT_LOCALE,
).toUpperCase();
const handleSizeChange = useCallback(
({ width, height }: { width: number; height: number }) => {
if (chartRef.current) {
chartRef.current.resize({ width, height });
}
},
[],
);
useEffect(() => {
if (!divRef.current) return;
if (!chartRef.current) {
chartRef.current = init(divRef.current);
}
const loadLocaleAndInitChart = async () => {
if (!divRef.current) return;
Object.entries(eventHandlers || {}).forEach(([name, handler]) => {
chartRef.current?.off(name);
chartRef.current?.on(name, handler);
});
const lang = await import(`echarts/lib/i18n/lang${locale}`).catch(e => {
console.error(`Locale ${locale} not supported in ECharts`, e);
});
if (lang?.default) {
registerLocale(locale, lang.default);
}
Object.entries(zrEventHandlers || {}).forEach(([name, handler]) => {
chartRef.current?.getZr().off(name);
chartRef.current?.getZr().on(name, handler);
});
if (!chartRef.current) {
chartRef.current = init(divRef.current, null, { locale });
}
chartRef.current.setOption(echartOptions, true);
}, [echartOptions, eventHandlers, zrEventHandlers]);
Object.entries(eventHandlers || {}).forEach(([name, handler]) => {
chartRef.current?.off(name);
chartRef.current?.on(name, handler);
});
Object.entries(zrEventHandlers || {}).forEach(([name, handler]) => {
chartRef.current?.getZr().off(name);
chartRef.current?.getZr().on(name, handler);
});
chartRef.current.setOption(echartOptions, true);
// did mount
handleSizeChange({ width, height });
};
loadLocaleAndInitChart();
}, [echartOptions, eventHandlers, zrEventHandlers, locale]);
// highlighting
useEffect(() => {
@@ -158,22 +197,7 @@ function Echart(
});
}
previousSelection.current = currentSelection;
}, [currentSelection]);
const handleSizeChange = useCallback(
({ width, height }: { width: number; height: number }) => {
if (chartRef.current) {
chartRef.current.resize({ width, height });
}
},
[],
);
// did mount
useEffect(() => {
handleSizeChange({ width, height });
return () => chartRef.current?.dispose();
}, []);
}, [currentSelection, chartRef.current]);
useLayoutEffect(() => {
handleSizeChange({ width, height });

View File

@@ -121,3 +121,5 @@ export const TOOLTIP_POINTER_MARGIN = 10;
// If no satisfactory position can be found, how far away
// from the edge of the window should the tooltip be kept
export const TOOLTIP_OVERFLOW_MARGIN = 5;
export const DEFAULT_LOCALE = 'en';

View File

@@ -156,9 +156,15 @@ export function sortAndFilterSeries(
case SortSeriesType.Avg:
aggregator = name => ({ name, value: meanBy(rows, name) });
break;
default:
aggregator = name => ({ name, value: name.toLowerCase() });
break;
default: {
const collator = new Intl.Collator(undefined, {
numeric: true,
sensitivity: 'base',
});
return seriesNames.sort((a, b) =>
sortSeriesAscending ? collator.compare(a, b) : collator.compare(b, a),
);
}
}
const sortedValues = seriesNames.map(aggregator);

View File

@@ -186,3 +186,188 @@ describe('BigNumberWithTrendline', () => {
});
});
});
describe('BigNumberWithTrendline - Aggregation Tests', () => {
const baseProps = {
width: 800,
height: 600,
formData: {
colorPicker: { r: 0, g: 0, b: 0, a: 1 },
metric: 'metric',
aggregation: 'LAST_VALUE',
},
queriesData: [
{
data: [
{ __timestamp: 1607558400000, metric: 10 },
{ __timestamp: 1607558500000, metric: 30 },
{ __timestamp: 1607558600000, metric: 50 },
{ __timestamp: 1607558700000, metric: 60 },
],
colnames: ['__timestamp', 'metric'],
coltypes: ['TIMESTAMP', 'BIGINT'],
},
],
hooks: {},
filterState: {},
datasource: {
columnFormats: {},
currencyFormats: {},
},
rawDatasource: {},
rawFormData: {},
theme: {
colors: {
grayscale: {
light5: '#fafafa',
},
},
},
} as unknown as BigNumberWithTrendlineChartProps;
const propsWithEvenData = {
...baseProps,
queriesData: [
{
data: [
{ __timestamp: 1607558400000, metric: 10 },
{ __timestamp: 1607558500000, metric: 20 },
{ __timestamp: 1607558600000, metric: 30 },
{ __timestamp: 1607558700000, metric: 40 },
],
colnames: ['__timestamp', 'metric'],
coltypes: ['TIMESTAMP', 'BIGINT'],
},
],
} as unknown as BigNumberWithTrendlineChartProps;
it('should correctly calculate SUM', () => {
const props = {
...baseProps,
formData: { ...baseProps.formData, aggregation: 'sum' },
queriesData: [
baseProps.queriesData[0],
{
data: [{ metric: 150 }],
colnames: ['metric'],
coltypes: ['BIGINT'],
},
],
} as unknown as BigNumberWithTrendlineChartProps;
const transformed = transformProps(props);
expect(transformed.bigNumber).toStrictEqual(150);
});
it('should correctly calculate AVG', () => {
const props = {
...baseProps,
formData: { ...baseProps.formData, aggregation: 'mean' },
queriesData: [
baseProps.queriesData[0],
{
data: [{ metric: 37.5 }],
colnames: ['metric'],
coltypes: ['BIGINT'],
},
],
} as unknown as BigNumberWithTrendlineChartProps;
const transformed = transformProps(props);
expect(transformed.bigNumber).toStrictEqual(37.5);
});
it('should correctly calculate MIN', () => {
const props = {
...baseProps,
formData: { ...baseProps.formData, aggregation: 'min' },
queriesData: [
baseProps.queriesData[0],
{
data: [{ metric: 10 }],
colnames: ['metric'],
coltypes: ['BIGINT'],
},
],
} as unknown as BigNumberWithTrendlineChartProps;
const transformed = transformProps(props);
expect(transformed.bigNumber).toStrictEqual(10);
});
it('should correctly calculate MAX', () => {
const props = {
...baseProps,
formData: { ...baseProps.formData, aggregation: 'max' },
queriesData: [
baseProps.queriesData[0],
{
data: [{ metric: 60 }],
colnames: ['metric'],
coltypes: ['BIGINT'],
},
],
} as unknown as BigNumberWithTrendlineChartProps;
const transformed = transformProps(props);
expect(transformed.bigNumber).toStrictEqual(60);
});
it('should correctly calculate MEDIAN (odd count)', () => {
const oddCountProps = {
...baseProps,
queriesData: [
{
data: [
{ __timestamp: 1607558300000, metric: 10 },
{ __timestamp: 1607558400000, metric: 20 },
{ __timestamp: 1607558500000, metric: 30 },
{ __timestamp: 1607558600000, metric: 40 },
{ __timestamp: 1607558700000, metric: 50 },
],
colnames: ['__timestamp', 'metric'],
coltypes: ['TIMESTAMP', 'BIGINT'],
},
],
} as unknown as BigNumberWithTrendlineChartProps;
const props = {
...oddCountProps,
formData: { ...oddCountProps.formData, aggregation: 'median' },
queriesData: [
oddCountProps.queriesData[0],
{
data: [{ metric: 30 }],
colnames: ['metric'],
coltypes: ['BIGINT'],
},
],
} as unknown as BigNumberWithTrendlineChartProps;
const transformed = transformProps(props);
expect(transformed.bigNumber).toStrictEqual(30);
});
it('should correctly calculate MEDIAN (even count)', () => {
const props = {
...propsWithEvenData,
formData: { ...propsWithEvenData.formData, aggregation: 'median' },
queriesData: [
propsWithEvenData.queriesData[0],
{
data: [{ metric: 25 }],
colnames: ['metric'],
coltypes: ['BIGINT'],
},
],
} as unknown as BigNumberWithTrendlineChartProps;
const transformed = transformProps(props);
expect(transformed.bigNumber).toStrictEqual(25);
});
it('should return the LAST_VALUE correctly', () => {
const transformed = transformProps(baseProps);
expect(transformed.bigNumber).toStrictEqual(10);
});
});

View File

@@ -67,6 +67,39 @@ const sortData: DataRecord[] = [
{ my_x_axis: null, x: 4, y: 3, z: 7 },
];
const sortDataWithNumbers: DataRecord[] = [
{
my_x_axis: 'my_axis',
'9. September': 6,
6: 1,
'11. November': 8,
8: 2,
'10. October': 1,
10: 4,
'3. March': 2,
'8. August': 6,
2: 1,
12: 3,
9: 1,
'1. January': 1,
'4. April': 12,
'2. February': 9,
5: 4,
3: 1,
11: 2,
'12. December': 4,
1: 7,
'6. June': 1,
4: 5,
7: 2,
c: 0,
'7. July': 2,
d: 0,
'5. May': 4,
a: 1,
},
];
const totalStackedValues = [3, 15, 14];
test('sortRows by name ascending', () => {
@@ -288,6 +321,84 @@ test('sortAndFilterSeries by name descending', () => {
sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Name, false),
).toEqual(['z', 'y', 'x']);
});
test('sortAndFilterSeries by name with numbers asc', () => {
expect(
sortAndFilterSeries(
sortDataWithNumbers,
'my_x_axis',
[],
SortSeriesType.Name,
true,
),
).toEqual([
'1',
'1. January',
'2',
'2. February',
'3',
'3. March',
'4',
'4. April',
'5',
'5. May',
'6',
'6. June',
'7',
'7. July',
'8',
'8. August',
'9',
'9. September',
'10',
'10. October',
'11',
'11. November',
'12',
'12. December',
'a',
'c',
'd',
]);
});
test('sortAndFilterSeries by name with numbers desc', () => {
expect(
sortAndFilterSeries(
sortDataWithNumbers,
'my_x_axis',
[],
SortSeriesType.Name,
false,
),
).toEqual([
'd',
'c',
'a',
'12. December',
'12',
'11. November',
'11',
'10. October',
'10',
'9. September',
'9',
'8. August',
'8',
'7. July',
'7',
'6. June',
'6',
'5. May',
'5',
'4. April',
'4',
'3. March',
'3',
'2. February',
'2',
'1. January',
'1',
]);
});
describe('extractSeries', () => {
it('should generate a valid ECharts timeseries series object', () => {

View File

@@ -605,6 +605,8 @@ export default function TableChart<D extends DataRecord = DataRecord>(
// Calculate the number of placeholder columns needed before the current header
const startPosition = value[0];
const colSpan = value.length;
// Retrieve the originalLabel from the first column in this group
const originalLabel = columnsMeta[value[0]]?.originalLabel || key;
// Add placeholder <th> for columns before this header
for (let i = currentColumnIndex; i < startPosition; i += 1) {
@@ -620,7 +622,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
// Add the current header <th>
headers.push(
<th key={`header-${key}`} colSpan={colSpan} style={{ borderBottom: 0 }}>
{key}
{originalLabel}
<span
css={css`
float: right;
@@ -975,7 +977,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
),
Footer: totals ? (
i === 0 ? (
<th>
<th key={`footer-summary-${i}`}>
<div
css={css`
display: flex;
@@ -997,7 +999,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
</div>
</th>
) : (
<td style={sharedStyle}>
<td key={`footer-total-${i}`} style={sharedStyle}>
<strong>{formatColumnValue(column, totals[key])[1]}</strong>
</td>
)

View File

@@ -198,11 +198,6 @@ const buildQuery: BuildQuery<TableChartFormData> = (
(ownState.currentPage ?? 0) * (ownState.pageSize ?? 0);
}
if (!temporalColumn) {
// This query is not using temporal column, so it doesn't need time grain
extras.time_grain_sqla = undefined;
}
let queryObject = {
...baseQueryObject,
columns,

View File

@@ -486,8 +486,9 @@ const config: ControlPanelConfig = {
return true;
},
mapStateToProps(explore, _, chart) {
const timeComparisonStatus =
!!explore?.controls?.time_compare?.value;
const timeComparisonStatus = !isEmpty(
explore?.controls?.time_compare?.value,
);
const { colnames: _colnames, coltypes: _coltypes } =
chart?.queriesResponse?.[0] ?? {};

View File

@@ -347,6 +347,7 @@ const processComparisonColumns = (
} = props;
const savedFormat = columnFormats?.[col.key];
const savedCurrency = currencyFormats?.[col.key];
const originalLabel = col.label;
if (
(col.isMetric || col.isPercentMetric) &&
!col.key.includes(comparisonSuffix) &&
@@ -355,6 +356,7 @@ const processComparisonColumns = (
return [
{
...col,
originalLabel,
label: t('Main'),
key: `${t('Main')} ${col.key}`,
config: getComparisonColConfig(t('Main'), col.key, columnConfig),
@@ -368,6 +370,7 @@ const processComparisonColumns = (
},
{
...col,
originalLabel,
label: `#`,
key: `# ${col.key}`,
config: getComparisonColConfig(`#`, col.key, columnConfig),
@@ -381,6 +384,7 @@ const processComparisonColumns = (
},
{
...col,
originalLabel,
label: ``,
key: `${col.key}`,
config: getComparisonColConfig(``, col.key, columnConfig),
@@ -394,6 +398,7 @@ const processComparisonColumns = (
},
{
...col,
originalLabel,
label: `%`,
key: `% ${col.key}`,
config: getComparisonColConfig(`%`, col.key, columnConfig),

View File

@@ -56,6 +56,8 @@ export interface DataColumnMeta {
key: string;
// `label` is verbose column name used for rendering
label: string;
// `originalLabel` preserves the original label when time comparison transforms the labels
originalLabel?: string;
dataType: GenericDataType;
formatter?:
| TimeFormatter

View File

@@ -175,6 +175,75 @@ describe('plugin-chart-table', () => {
?.formatter?.(0.123456);
expect(formattedPercentMetric).toBe('0.123');
});
it('should set originalLabel for comparison columns when time_compare and comparison_type are set', () => {
const transformedProps = transformProps(testData.comparison);
// Check if comparison columns are processed
const comparisonColumns = transformedProps.columns.filter(
col =>
col.label === 'Main' ||
col.label === '#' ||
col.label === '△' ||
col.label === '%',
);
expect(comparisonColumns.length).toBeGreaterThan(0);
expect(comparisonColumns.some(col => col.label === 'Main')).toBe(true);
expect(comparisonColumns.some(col => col.label === '#')).toBe(true);
expect(comparisonColumns.some(col => col.label === '△')).toBe(true);
expect(comparisonColumns.some(col => col.label === '%')).toBe(true);
// Verify originalLabel for metric_1 comparison columns
const mainMetric1 = transformedProps.columns.find(
col => col.key === 'Main metric_1',
);
expect(mainMetric1).toBeDefined();
expect(mainMetric1?.originalLabel).toBe('metric_1');
const hashMetric1 = transformedProps.columns.find(
col => col.key === '# metric_1',
);
expect(hashMetric1).toBeDefined();
expect(hashMetric1?.originalLabel).toBe('metric_1');
const deltaMetric1 = transformedProps.columns.find(
col => col.key === '△ metric_1',
);
expect(deltaMetric1).toBeDefined();
expect(deltaMetric1?.originalLabel).toBe('metric_1');
const percentMetric1 = transformedProps.columns.find(
col => col.key === '% metric_1',
);
expect(percentMetric1).toBeDefined();
expect(percentMetric1?.originalLabel).toBe('metric_1');
// Verify originalLabel for metric_2 comparison columns
const mainMetric2 = transformedProps.columns.find(
col => col.key === 'Main metric_2',
);
expect(mainMetric2).toBeDefined();
expect(mainMetric2?.originalLabel).toBe('metric_2');
const hashMetric2 = transformedProps.columns.find(
col => col.key === '# metric_2',
);
expect(hashMetric2).toBeDefined();
expect(hashMetric2?.originalLabel).toBe('metric_2');
const deltaMetric2 = transformedProps.columns.find(
col => col.key === '△ metric_2',
);
expect(deltaMetric2).toBeDefined();
expect(deltaMetric2?.originalLabel).toBe('metric_2');
const percentMetric2 = transformedProps.columns.find(
col => col.key === '% metric_2',
);
expect(percentMetric2).toBeDefined();
expect(percentMetric2?.originalLabel).toBe('metric_2');
});
});
describe('TableChart', () => {
@@ -400,6 +469,17 @@ describe('plugin-chart-table', () => {
);
expect(getComputedStyle(screen.getByText('N/A')).background).toBe('');
});
it('should display originalLabel in grouped headers', () => {
render(
<ThemeProvider theme={supersetTheme}>
<TableChart {...transformProps(testData.comparison)} sticky={false} />
</ThemeProvider>,
);
const groupHeaders = screen.getAllByRole('columnheader');
expect(groupHeaders[0]).toHaveTextContent('metric_1');
expect(groupHeaders[1]).toHaveTextContent('metric_2');
});
});
it('render cell bars properly, and only when it is toggled on in both regular and percent metrics', () => {

View File

@@ -148,14 +148,5 @@ describe('plugin-chart-table', () => {
expect(queries[1].extras?.time_grain_sqla).toEqual(TimeGranularity.MONTH);
expect(queries[1].extras?.where).toEqual("(status IN ('In Process'))");
});
it('should not include time_grain_sqla in extras if temporal colum is not used and keep the rest', () => {
const { queries } = buildQuery(extraQueryFormData);
// Extras in regular query
expect(queries[0].extras?.time_grain_sqla).toBeUndefined();
expect(queries[0].extras?.where).toEqual("(status IN ('In Process'))");
// Extras in summary query
expect(queries[1].extras?.time_grain_sqla).toBeUndefined();
expect(queries[1].extras?.where).toEqual("(status IN ('In Process'))");
});
});
});

View File

@@ -19,6 +19,7 @@
import './shim';
// eslint-disable-next-line no-restricted-syntax -- whole React import is required for mocking React module in tests.
import React from 'react';
// eslint-disable-next-line no-restricted-imports
import { configure as configureTestingLibrary } from '@testing-library/react';
import { matchers } from '@emotion/jest';

View File

@@ -92,7 +92,7 @@ jest.mock('rehype-raw', () => () => jest.fn());
// Mocks the Icon component due to its async nature
// Tests should override this when needed
jest.mock('src/components/Icons/Icon', () => ({
jest.mock('src/components/Icons/AsyncIcon', () => ({
__esModule: true,
default: ({
fileName,
@@ -111,13 +111,22 @@ jest.mock('src/components/Icons/Icon', () => ({
/>
),
StyledIcon: ({
component: Component,
role,
'aria-label': ariaLabel,
...rest
}: {
component: React.ComponentType<any>;
role: string;
'aria-label': AriaAttributes['aria-label'];
}) => <span role={role ?? 'img'} aria-label={ariaLabel} {...rest} />,
}) => (
<Component
role={role ?? 'img'}
alt={ariaLabel}
aria-label={ariaLabel}
{...rest}
/>
),
}));
process.env.WEBPACK_MODE = 'test';

View File

@@ -18,6 +18,7 @@
*/
import '@testing-library/jest-dom';
import { ReactNode, ReactElement } from 'react';
// eslint-disable-next-line no-restricted-imports
import {
render,
RenderOptions,
@@ -25,6 +26,7 @@ import {
waitFor,
within,
} from '@testing-library/react';
// eslint-disable-next-line no-restricted-imports
import { ThemeProvider, supersetTheme } from '@superset-ui/core';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
@@ -107,6 +109,7 @@ export function sleep(time: number) {
});
}
// eslint-disable-next-line no-restricted-imports
export * from '@testing-library/react';
export { customRender as render };
export { default as userEvent } from '@testing-library/user-event';

View File

@@ -17,10 +17,10 @@
* under the License.
*/
import { useDispatch } from 'react-redux';
import { useTheme, t } from '@superset-ui/core';
import { t } from '@superset-ui/core';
import { Dropdown } from 'src/components/Dropdown';
import { Menu } from 'src/components/Menu';
import Icons from 'src/components/Icons';
import { Icons } from 'src/components/Icons';
import { queryEditorSetQueryLimit } from 'src/SqlLab/actions/sqlLab';
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
import Button from 'src/components/Button';
@@ -64,7 +64,6 @@ const QueryLimitSelect = ({
maxRow,
defaultQueryLimit,
}: QueryLimitSelectProps) => {
const theme = useTheme();
const dispatch = useDispatch();
const queryEditor = useQueryEditor(queryEditorId, ['id', 'queryLimit']);
@@ -82,7 +81,7 @@ const QueryLimitSelect = ({
<span className="limitDropdown">
{convertToNumWithSpaces(queryLimit)}
</span>
<Icons.TriangleDown iconColor={theme.colors.grayscale.base} />
<Icons.CaretDownOutlined iconSize="m" />
</Button>
</Dropdown>
);

View File

@@ -32,7 +32,7 @@ import {
import TableView from 'src/components/TableView';
import Button from 'src/components/Button';
import { fDuration, extendedDayjs } from 'src/utils/dates';
import Icons from 'src/components/Icons';
import { Icons } from 'src/components/Icons';
import Label from 'src/components/Label';
import { Tooltip } from 'src/components/Tooltip';
import { SqlLabRootState } from 'src/SqlLab/types';
@@ -40,7 +40,7 @@ import ModalTrigger from 'src/components/ModalTrigger';
import { UserWithPermissionsAndRoles as User } from 'src/types/bootstrapTypes';
import ResultSet from '../ResultSet';
import HighlightedSql from '../HighlightedSql';
import { StaticPosition, verticalAlign, StyledTooltip } from './styles';
import { StaticPosition, StyledTooltip } from './styles';
interface QueryTableQuery
extends Omit<
@@ -188,7 +188,10 @@ const QueryTable = ({
timed_out: {
config: {
icon: (
<Icons.Clock iconColor={theme.colors.error.base} iconSize="m" />
<Icons.ClockCircleOutlined
iconColor={theme.colors.error.base}
iconSize="m"
/>
),
label: t('Offline'),
},
@@ -277,7 +280,7 @@ const QueryTable = ({
buttonStyle="link"
onClick={() => openQuery(q.queryId)}
>
<i className="fa fa-external-link m-r-3" />
<Icons.Full iconSize="m" iconColor={theme.colors.primary.dark1} />
{t('Edit')}
</Button>
);
@@ -342,7 +345,7 @@ const QueryTable = ({
placement="top"
className="pointer"
>
<Icons.Edit iconSize="xl" />
<Icons.EditOutlined iconSize="l" />
</StyledTooltip>
<StyledTooltip
onClick={() => openQueryInNewTab(query)}
@@ -350,7 +353,7 @@ const QueryTable = ({
placement="top"
className="pointer"
>
<Icons.PlusCircleOutlined iconSize="xl" css={verticalAlign} />
<Icons.PlusCircleOutlined iconSize="l" />
</StyledTooltip>
{q.id !== latestQueryId && (
<StyledTooltip
@@ -358,7 +361,7 @@ const QueryTable = ({
onClick={() => dispatch(removeQuery(query))}
className="pointer"
>
<Icons.Trash iconSize="xl" />
<Icons.DeleteOutlined iconSize="l" />
</StyledTooltip>
)}
</div>

View File

@@ -79,7 +79,7 @@ import {
LOG_ACTIONS_SQLLAB_CREATE_CHART,
LOG_ACTIONS_SQLLAB_DOWNLOAD_CSV,
} from 'src/logger/LogUtils';
import Icons from 'src/components/Icons';
import { Icons } from 'src/components/Icons';
import { findPermission } from 'src/utils/findPermission';
import ExploreCtasResultsButton from '../ExploreCtasResultsButton';
import ExploreResultsButton from '../ExploreResultsButton';
@@ -150,6 +150,15 @@ const ResultSetButtons = styled.div`
padding-right: ${({ theme }) => 2 * theme.gridUnit}px;
`;
const copyButtonStyles = css`
&:hover {
text-decoration: unset;
}
span > :first-of-type {
margin: 0px;
}
`;
const ROWS_CHIP_WIDTH = 100;
const GAP = 8;
@@ -342,6 +351,7 @@ const ResultSet = ({
)}
{csv && canExportData && (
<Button
css={copyButtonStyles}
buttonSize="small"
href={getExportCsvUrl(query.id)}
data-test="export-csv-button"
@@ -361,7 +371,11 @@ const ResultSet = ({
}
}}
>
<i className="fa fa-file-text-o" /> {t('Download to CSV')}
<Icons.DownloadOutlined
iconSize="m"
iconColor={theme.colors.primary.dark2}
/>{' '}
{t('Download to CSV')}
</Button>
)}
@@ -371,10 +385,15 @@ const ResultSet = ({
wrapped={false}
copyNode={
<Button
css={copyButtonStyles}
buttonSize="small"
data-test="copy-to-clipboard-button"
>
<i className="fa fa-clipboard" /> {t('Copy to Clipboard')}
<Icons.CopyOutlined
iconSize="s"
iconColor={theme.colors.primary.dark2}
/>{' '}
{t('Copy to Clipboard')}
</Button>
}
hideTooltip

View File

@@ -18,10 +18,10 @@
*/
import { useMemo, FC, ReactElement } from 'react';
import { t, styled, useTheme } from '@superset-ui/core';
import { t, styled, useTheme, SupersetTheme } from '@superset-ui/core';
import Button from 'src/components/Button';
import Icons from 'src/components/Icons';
import { Icons } from 'src/components/Icons';
import { DropdownButton } from 'src/components/DropdownButton';
import { detectOS } from 'src/utils/common';
import { QueryButtonProps } from 'src/SqlLab/types';
@@ -44,11 +44,13 @@ export interface RunQueryActionButtonProps {
const buildText = (
shouldShowStopButton: boolean,
selectedText: string | undefined,
theme: SupersetTheme,
): string | JSX.Element => {
if (shouldShowStopButton) {
return (
<>
<i className="fa fa-stop" /> {t('Stop')}
<Icons.Square iconSize="xs" iconColor={theme.colors.primary.light5} />
{t('Stop')}
</>
);
}
@@ -148,13 +150,12 @@ const RunQueryActionButton = ({
? {
overlay: overlayCreateAsMenu,
icon: (
<Icons.CaretDown
<Icons.DownOutlined
iconColor={
isDisabled
? theme.colors.grayscale.base
: theme.colors.grayscale.light5
}
name="caret-down"
/>
),
trigger: 'click',
@@ -163,7 +164,7 @@ const RunQueryActionButton = ({
buttonStyle: shouldShowStopBtn ? 'warning' : 'primary',
})}
>
{buildText(shouldShowStopBtn, selectedText)}
{buildText(shouldShowStopBtn, selectedText, theme)}
</ButtonComponent>
</StyledButton>
);

View File

@@ -36,7 +36,7 @@ describe('SaveDatasetActionButton', () => {
);
const saveBtn = screen.getByRole('button', { name: /save/i });
const caretBtn = screen.getByRole('button', { name: /caret-down/i });
const caretBtn = screen.getByRole('button', { name: /down/i });
expect(
await screen.findByRole('button', { name: /save/i }),
@@ -53,9 +53,9 @@ describe('SaveDatasetActionButton', () => {
/>,
);
const caretBtn = screen.getByRole('button', { name: /caret-down/i });
const caretBtn = screen.getByRole('button', { name: /down/i });
expect(
await screen.findByRole('button', { name: /caret-down/i }),
await screen.findByRole('button', { name: /down/i }),
).toBeInTheDocument();
userEvent.click(caretBtn);

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import { t, useTheme } from '@superset-ui/core';
import Icons from 'src/components/Icons';
import { Icons } from 'src/components/Icons';
import { DropdownButton } from 'src/components/DropdownButton';
import Button from 'src/components/Button';
@@ -41,9 +41,9 @@ const SaveDatasetActionButton = ({
onClick={() => setShowSave(true)}
dropdownRender={() => overlayMenu}
icon={
<Icons.CaretDown
iconColor={theme.colors.grayscale.light5}
name="caret-down"
<Icons.DownOutlined
iconSize="xs"
iconColor={theme.colors.primary.dark2}
/>
}
trigger={['click']}

View File

@@ -49,7 +49,8 @@ import {
import { mountExploreUrl } from 'src/explore/exploreUtils';
import { postFormData } from 'src/explore/exploreUtils/formData';
import { URL_PARAMS } from 'src/constants';
import { SelectValue } from 'antd/lib/select';
// eslint-disable-next-line no-restricted-imports
import { SelectValue } from 'antd/lib/select'; // TODO: Remove antd
import { isEmpty } from 'lodash';
interface QueryDatabase {

View File

@@ -179,7 +179,7 @@ describe('SavedQuery', () => {
await waitFor(() => {
const saveBtn = screen.getByRole('button', { name: /save/i });
const caretBtn = screen.getByRole('button', { name: /caret-down/i });
const caretBtn = screen.getByRole('button', { name: /down/i });
expect(saveBtn).toBeVisible();
expect(caretBtn).toBeVisible();
@@ -192,7 +192,9 @@ describe('SavedQuery', () => {
store: mockStore(mockState),
});
const caretBtn = await screen.findByRole('button', { name: /caret-down/i });
const caretBtn = await screen.findByRole('button', {
name: /down/i,
});
userEvent.click(caretBtn);
const saveDatasetMenuItem = await screen.findByText(/save dataset/i);
@@ -209,7 +211,9 @@ describe('SavedQuery', () => {
store: mockStore(mockState),
});
const caretBtn = await screen.findByRole('button', { name: /caret-down/i });
const caretBtn = await screen.findByRole('button', {
name: /down/i,
});
userEvent.click(caretBtn);
const saveDatasetMenuItem = await screen.findByText(/save dataset/i);

View File

@@ -57,7 +57,7 @@ export type QueryPayload = {
} & Pick<QueryEditor, 'dbId' | 'catalog' | 'schema' | 'sql'>;
const Styles = styled.span`
span[role='img'] {
span[role='img']:not([aria-label='down']) {
display: flex;
margin: 0;
color: ${({ theme }) => theme.colors.grayscale.base};

View File

@@ -17,14 +17,14 @@
* under the License.
*/
import {
styled,
t,
useTheme,
getClientErrorObject,
SupersetClient,
css,
} from '@superset-ui/core';
import Button from 'src/components/Button';
import Icons from 'src/components/Icons';
import { Icons } from 'src/components/Icons';
import withToasts from 'src/components/MessageToasts/withToasts';
import CopyToClipboard from 'src/components/CopyToClipboard';
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
@@ -36,16 +36,6 @@ interface ShareSqlLabQueryProps {
addDangerToast: (msg: string) => void;
}
const StyledIcon = styled(Icons.Link)`
&:first-of-type {
margin: 0;
display: flex;
svg {
margin: 0;
}
}
`;
const ShareSqlLabQuery = ({
queryEditorId,
addDangerToast,
@@ -85,8 +75,19 @@ const ShareSqlLabQuery = ({
const buildButton = () => {
const tooltip = t('Copy query link to your clipboard');
return (
<Button buttonSize="small" tooltip={tooltip}>
<StyledIcon iconColor={theme.colors.primary.base} iconSize="xl" />
<Button
buttonSize="small"
tooltip={tooltip}
css={css`
span > :first-of-type {
margin-right: 0;
}
`}
>
<Icons.LinkOutlined
iconColor={theme.colors.primary.base}
iconSize="m"
/>
{t('Copy link')}
</Button>
);

View File

@@ -21,6 +21,7 @@ import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql';
import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github';
import { IconTooltip } from 'src/components/IconTooltip';
import ModalTrigger from 'src/components/ModalTrigger';
import { Icons } from 'src/components/Icons';
SyntaxHighlighter.registerLanguage('sql', sql);
@@ -42,10 +43,9 @@ export default function ShowSQL({
modalTitle={title}
triggerNode={
triggerNode || (
<IconTooltip
className="fa fa-eye pull-left m-l-2"
tooltip={tooltipText}
/>
<IconTooltip className="pull-left m-l-2" tooltip={tooltipText}>
<Icons.EyeOutlined iconSize="s" />
</IconTooltip>
)
}
modalBody={

View File

@@ -20,12 +20,12 @@ import { createRef, useCallback, useMemo } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { nanoid } from 'nanoid';
import Tabs from 'src/components/Tabs';
import { css, styled, t } from '@superset-ui/core';
import { css, styled, t, useTheme } from '@superset-ui/core';
import { removeTables, setActiveSouthPaneTab } from 'src/SqlLab/actions/sqlLab';
import Label from 'src/components/Label';
import Icons from 'src/components/Icons';
import { Icons } from 'src/components/Icons';
import { SqlLabRootState } from 'src/SqlLab/types';
import QueryHistory from '../QueryHistory';
import {
@@ -91,6 +91,7 @@ const SouthPane = ({
displayLimit,
defaultQueryLimit,
}: SouthPaneProps) => {
const theme = useTheme();
const dispatch = useDispatch();
const { offline, tables } = useSelector(
({ sqlLab: { offline, tables } }: SqlLabRootState) => ({
@@ -180,11 +181,11 @@ const SouthPane = ({
<Tabs.TabPane
tab={
<>
<Icons.Table
iconSize="s"
<Icons.InsertRowAboveOutlined
iconSize="l"
css={css`
margin-bottom: 2px;
margin-right: 4px;
margin-bottom: ${theme.gridUnit * 0.5}px;
margin-right: ${theme.gridUnit}px;
`}
/>
{`${schema}.${decodeURIComponent(name)}`}

View File

@@ -61,7 +61,7 @@ import { Skeleton } from 'src/components';
import { Switch } from 'src/components/Switch';
import { Input } from 'src/components/Input';
import { Menu } from 'src/components/Menu';
import Icons from 'src/components/Icons';
import { Icons } from 'src/components/Icons';
import { detectOS } from 'src/utils/common';
import {
addNewQueryEditor,
@@ -874,7 +874,7 @@ const SqlEditor: FC<Props> = ({
trigger={['click']}
>
<Button buttonSize="xsmall" type="link" showMarginRight={false}>
<Icons.MoreHoriz iconColor={theme.colors.grayscale.base} />
<Icons.EllipsisOutlined />
</Button>
</Dropdown>
</div>

View File

@@ -36,7 +36,7 @@ import {
import Button from 'src/components/Button';
import { t, styled, css, SupersetTheme } from '@superset-ui/core';
import Collapse from 'src/components/Collapse';
import Icons from 'src/components/Icons';
import { Icons } from 'src/components/Icons';
import { TableSelectorMultiple } from 'src/components/TableSelector';
import { IconTooltip } from 'src/components/IconTooltip';
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
@@ -298,6 +298,8 @@ const SqlEditorLeftBar = ({
buttonStyle="danger"
onClick={handleResetState}
>
{/* TODO: Remove fa-icon */}
{/* eslint-disable-next-line icons/no-fa-icons-usage */}
<i className="fa fa-bomb" /> {t('Reset state')}
</Button>
)}

View File

@@ -22,7 +22,14 @@ import { bindActionCreators } from 'redux';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { MenuDotsDropdown } from 'src/components/Dropdown';
import { Menu } from 'src/components/Menu';
import { styled, t, QueryState } from '@superset-ui/core';
import {
styled,
css,
t,
QueryState,
SupersetTheme,
useTheme,
} from '@superset-ui/core';
import {
removeQueryEditor,
removeAllOtherQueryEditors,
@@ -31,11 +38,16 @@ import {
toggleLeftBar,
} from 'src/SqlLab/actions/sqlLab';
import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
import TabStatusIcon from '../TabStatusIcon';
import { Icons, IconType } from 'src/components/Icons';
const TabTitleWrapper = styled.div`
display: flex;
align-items: center;
[aria-label='check-circle'],
.status-icon {
margin: 0px;
}
`;
const TabTitle = styled.span`
margin-right: ${({ theme }) => theme.gridUnit * 2}px;
@@ -43,16 +55,29 @@ const TabTitle = styled.span`
`;
const IconContainer = styled.div`
display: inline-block;
width: ${({ theme }) => theme.gridUnit * 8}px;
text-align: center;
${({ theme }) => css`
display: inline-block;
margin: 0 ${theme.gridUnit * 2}px 0 0px;
`}
`;
interface Props {
queryEditor: QueryEditor;
}
const STATE_ICONS: Record<string, FC<IconType>> = {
started: Icons.CircleSolid,
stopped: Icons.StopOutlined,
pending: Icons.CircleSolid,
scheduled: Icons.CalendarOutlined,
fetching: Icons.CircleSolid,
timedOut: Icons.FieldTimeOutlined,
running: Icons.CircleSolid,
success: Icons.CheckCircleOutlined,
failed: Icons.CloseCircleOutlined,
};
const SqlEditorTabHeader: FC<Props> = ({ queryEditor }) => {
const theme = useTheme();
const qe = useSelector<SqlLabRootState, QueryEditor>(
({ sqlLab: { unsavedQueryEditor } }) => ({
...queryEditor,
@@ -63,6 +88,8 @@ const SqlEditorTabHeader: FC<Props> = ({ queryEditor }) => {
const queryState = useSelector<SqlLabRootState, QueryState>(
({ sqlLab }) => sqlLab.queries[qe.latestQueryId || '']?.state || '',
);
const StatusIcon = queryState ? STATE_ICONS[queryState] : STATE_ICONS.running;
const dispatch = useDispatch();
const actions = useMemo(
() =>
@@ -85,7 +112,21 @@ const SqlEditorTabHeader: FC<Props> = ({ queryEditor }) => {
actions.queryEditorSetTitle(qe, newTitle, qe.id);
}
}
const getStatusColor = (state: QueryState, theme: SupersetTheme): string => {
const statusColors: Record<QueryState, string> = {
[QueryState.Running]: theme.colors.info.base,
[QueryState.Success]: theme.colors.success.base,
[QueryState.Failed]: theme.colors.error.base,
[QueryState.Started]: theme.colors.primary.base,
[QueryState.Stopped]: theme.colors.warning.base,
[QueryState.Pending]: theme.colors.grayscale.light1,
[QueryState.Scheduled]: theme.colors.grayscale.light2,
[QueryState.Fetching]: theme.colors.secondary.base,
[QueryState.TimedOut]: theme.colors.error.dark1,
};
return statusColors[state] || theme.colors.grayscale.light2;
};
return (
<TabTitleWrapper>
<MenuDotsDropdown
@@ -99,7 +140,12 @@ const SqlEditorTabHeader: FC<Props> = ({ queryEditor }) => {
data-test="close-tab-menu-option"
>
<IconContainer>
<i className="fa fa-close" />
<Icons.CloseOutlined
iconSize="l"
css={css`
verticalalign: middle;
`}
/>
</IconContainer>
{t('Close tab')}
</Menu.Item>
@@ -109,7 +155,12 @@ const SqlEditorTabHeader: FC<Props> = ({ queryEditor }) => {
data-test="rename-tab-menu-option"
>
<IconContainer>
<i className="fa fa-i-cursor" />
<Icons.EditOutlined
css={css`
verticalalign: middle;
`}
iconSize="l"
/>
</IconContainer>
{t('Rename tab')}
</Menu.Item>
@@ -119,7 +170,12 @@ const SqlEditorTabHeader: FC<Props> = ({ queryEditor }) => {
data-test="toggle-menu-option"
>
<IconContainer>
<i className="fa fa-cogs" />
<Icons.VerticalAlignBottomOutlined
iconSize="l"
css={css`
rotate: ${qe.hideLeftBar ? '-90deg;' : '90deg;'};
`}
/>
</IconContainer>
{qe.hideLeftBar ? t('Expand tool bar') : t('Hide tool bar')}
</Menu.Item>
@@ -129,7 +185,12 @@ const SqlEditorTabHeader: FC<Props> = ({ queryEditor }) => {
data-test="close-all-other-menu-option"
>
<IconContainer>
<i className="fa fa-times-circle-o" />
<Icons.CloseOutlined
iconSize="l"
css={css`
vertical-align: middle;
`}
/>
</IconContainer>
{t('Close all other tabs')}
</Menu.Item>
@@ -139,14 +200,24 @@ const SqlEditorTabHeader: FC<Props> = ({ queryEditor }) => {
data-test="clone-tab-menu-option"
>
<IconContainer>
<i className="fa fa-files-o" />
<Icons.CopyOutlined
iconSize="l"
css={css`
vertical-align: middle;
`}
/>
</IconContainer>
{t('Duplicate tab')}
</Menu.Item>
</Menu>
}
/>
<TabTitle>{qe.name}</TabTitle> <TabStatusIcon tabState={queryState} />{' '}
<TabTitle>{qe.name}</TabTitle>
<StatusIcon
className="status-icon"
iconSize="xs"
iconColor={getStatusColor(queryState, theme)}
/>
</TabTitleWrapper>
);
};

View File

@@ -1,36 +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 { QueryState } from '@superset-ui/core';
import { render } from 'spec/helpers/testing-library';
import TabStatusIcon from 'src/SqlLab/components/TabStatusIcon';
function setup() {
return render(<TabStatusIcon tabState={'running' as QueryState} />);
}
describe('TabStatusIcon', () => {
it('renders a circle without an x when hovered', () => {
const { container } = setup();
expect(container.getElementsByClassName('circle')[0]).toBeInTheDocument();
expect(
container.getElementsByClassName('circle')[0]?.textContent ?? 'undefined',
).toBe('');
});
});

View File

@@ -1,78 +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 { FC } from 'react';
import { css, QueryState, styled } from '@superset-ui/core';
import Icons, { IconType } from 'src/components/Icons';
const IconContainer = styled.span`
position: absolute;
top: -6px;
left: 1px;
`;
const Circle = styled.div`
${({ theme }) => css`
border-radius: 50%;
width: ${theme.gridUnit * 3}px;
height: ${theme.gridUnit * 3}px;
display: inline-block;
background-color: ${theme.colors.grayscale.light2};
text-align: center;
vertical-align: middle;
font-size: ${theme.typography.sizes.m}px;
font-weight: ${theme.typography.weights.bold};
color: ${theme.colors.grayscale.light5};
position: relative;
&.running {
background-color: ${theme.colors.info.base};
}
&.success {
background-color: ${theme.colors.success.base};
}
&.failed {
background-color: ${theme.colors.error.base};
}
`}
`;
interface TabStatusIconProps {
tabState: QueryState;
}
const STATE_ICONS: Record<string, FC<IconType>> = {
success: Icons.Check,
failed: Icons.CancelX,
};
export default function TabStatusIcon({ tabState }: TabStatusIconProps) {
const StatusIcon = STATE_ICONS[tabState];
return (
<Circle className={`circle ${tabState}`}>
{StatusIcon && (
<IconContainer>
<StatusIcon iconSize="xs" />
</IconContainer>
)}
</Circle>
);
}

View File

@@ -22,7 +22,13 @@ import { EditableTabs } from 'src/components/Tabs';
import { connect } from 'react-redux';
import URI from 'urijs';
import type { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
import { FeatureFlag, styled, t, isFeatureEnabled } from '@superset-ui/core';
import {
FeatureFlag,
styled,
t,
isFeatureEnabled,
css,
} from '@superset-ui/core';
import { Logger } from 'src/logger/LogUtils';
import { Tooltip } from 'src/components/Tooltip';
import { detectOS } from 'src/utils/common';
@@ -30,6 +36,7 @@ import * as Actions from 'src/SqlLab/actions/sqlLab';
import { EmptyState } from 'src/components/EmptyState';
import getBootstrapData from 'src/utils/getBootstrapData';
import { locationContext } from 'src/pages/SqlLab/LocationContext';
import { Icons } from 'src/components/Icons';
import SqlEditor from '../SqlEditor';
import SqlEditorTabHeader from '../SqlEditorTabHeader';
@@ -247,7 +254,13 @@ class TabbedSqlEditors extends PureComponent<TabbedSqlEditorsProps> {
: t('New tab (Ctrl + t)')
}
>
<i data-test="add-tab-icon" className="fa fa-plus-circle" />
<Icons.PlusCircleOutlined
iconSize="s"
css={css`
vertical-align: middle;
`}
data-test="add-tab-icon"
/>
</Tooltip>
</StyledTab>
);
@@ -289,7 +302,13 @@ class TabbedSqlEditors extends PureComponent<TabbedSqlEditorsProps> {
: t('New tab (Ctrl + t)')
}
>
<i data-test="add-tab-icon" className="fa fa-plus-circle" />
<Icons.PlusCircleOutlined
iconSize="l"
css={css`
vertical-align: middle;
`}
data-test="add-tab-icon"
/>
</Tooltip>
}
>

View File

@@ -16,6 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
// TODO: Remove fa-icon
/* eslint-disable icons/no-fa-icons-usage */
import { useState, useRef, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import type { Table } from 'src/SqlLab/types';
@@ -43,6 +45,7 @@ import ModalTrigger from 'src/components/ModalTrigger';
import Loading from 'src/components/Loading';
import useEffectEvent from 'src/hooks/useEffectEvent';
import { ActionType } from 'src/types/Action';
import { Icons } from 'src/components/Icons';
import ColumnElement, { ColumnKeyTypeType } from '../ColumnElement';
import ShowSQL from '../ShowSQL';
@@ -200,7 +203,7 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
text={partitionQuery}
shouldShowText={false}
tooltipText={tt}
copyNode={<i className="fa fa-clipboard" />}
copyNode={<Icons.CopyOutlined iconSize="s" />}
/>
);
}
@@ -257,9 +260,14 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
))}
triggerNode={
<IconTooltip
className="fa fa-key pull-left m-l-2"
className="pull-left m-l-2"
tooltip={t('View keys & indexes (%s)', tableData.indexes.length)}
/>
>
<Icons.TableOutlined
iconSize="m"
iconColor={theme.colors.primary.dark2}
/>
</IconTooltip>
}
/>
);
@@ -277,10 +285,15 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
`}
>
<IconTooltip
className="fa fa-refresh pull-left m-l-2 pointer"
className="pull-left m-l-2 pointer"
onClick={refreshTableMetadata}
tooltip={t('Refresh table schema')}
/>
>
<Icons.SyncOutlined
iconSize="m"
iconColor={theme.colors.primary.dark2}
/>
</IconTooltip>
{keyLink}
<IconTooltip
className={
@@ -301,7 +314,11 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
aria-label="Copy"
tooltip={t('Copy SELECT statement to the clipboard')}
>
<i aria-hidden className="fa fa-clipboard pull-left m-l-2" />
<Icons.CopyOutlined
iconSize="m"
iconColor={theme.colors.primary.dark2}
aria-hidden
/>
</IconTooltip>
}
text={tableData.selectStar}
@@ -316,10 +333,16 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
/>
)}
<IconTooltip
className="fa fa-times table-remove pull-left m-l-2 pointer"
className=" table-remove pull-left m-l-2 pointer"
onClick={removeTable}
tooltip={t('Remove table preview')}
/>
>
<Icons.CloseOutlined
iconSize="m"
iconColor={theme.colors.primary.dark2}
aria-hidden
/>
</IconTooltip>
</ButtonGroup>
);
};

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