Compare commits

..
110 Commits
Author SHA1 Message Date
Michael S. Molinaandhenryyeh 4fa307566d fix: Fixes email body when sharing a chart by email (#14664)
(cherry picked from commit 32f5f365d8)
2021-05-18 11:55:04 -07:00
Yaozong Liuandhenryyeh 097d1fb85c fix(sqllab): fix error message (#14651)
(cherry picked from commit 2320bd44d4)
2021-05-18 11:55:04 -07:00
Geidoandhenryyeh 5646bcb9c8 Fix tooltip position (#14656)
(cherry picked from commit b5e9854ddc)
2021-05-18 11:55:04 -07:00
Geidoandhenryyeh 1393c5660c Add max width (#14663)
(cherry picked from commit 9a9f093e63)
2021-05-18 11:55:04 -07:00
Geidoandhenryyeh 3cea7b4199 fix(explore): Fix column number calculation (#14665)
* Fixes columns calc

* Fix type

(cherry picked from commit 7a050c59e4)
2021-05-18 11:55:04 -07:00
Phillip Kelley-Dotsonandhenryyeh 15126ef17d fix: nav submenu dropdown styles (#14580)
* fix nav submenu dropdown styles

* lint

* fix mobile view styles

* run lint

* address comments

* undo comit lock files

* Update superset-frontend/src/common/components/index.tsx

Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>

Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
(cherry picked from commit 3ad8b546e6)
2021-05-18 11:55:04 -07:00
Geidoandhenryyeh d99d173817 Clear search (#14655)
(cherry picked from commit ea96d95622)
2021-05-18 11:55:04 -07:00
Phillip Kelley-Dotsonandhenryyeh 5f2bb51393 fix: fix submenu header double line (#14631)
* fix submenu header

* remove unused css

* lint

* address comment

* address more comments

(cherry picked from commit 60f903ff58)
2021-05-18 11:55:04 -07:00
Michael S. Molinaandhenryyeh 2da0c347db fix: Fixes top level tabs and automatic scroll (#14624)
(cherry picked from commit 9cb4a4602f)
2021-05-18 11:55:04 -07:00
Geidoandhenryyeh c5637dba34 Fix class name (#14609)
(cherry picked from commit 3466cb253e)
2021-05-18 11:55:04 -07:00
Evan Rusackasandhenryyeh 574d3a1a49 fix: Removing specific column widths, letting things flex naturally. (#14637)
(cherry picked from commit bf90885828)
2021-05-18 11:55:04 -07:00
Michael S. Molinaandhenryyeh b6ef99a51c fix: Fixes #12672 (#14525)
(cherry picked from commit d31958cbd2)
2021-05-18 11:55:04 -07:00
Evan Rusackasandhenryyeh 8843b895ce fix: don't show busted label for unknown data types (#14585)
(cherry picked from commit ad699e8b48)
2021-05-18 11:55:04 -07:00
Phillip Kelley-Dotsonandhenryyeh b39dae95c5 fix: error icon spacing in explore (#14597)
* fix error message icon

* lint

(cherry picked from commit 5f7722cb36)
2021-05-18 11:55:03 -07:00
Phillip Kelley-Dotsonandhenryyeh e5c6472d4f fix: dashboard side actions (#14587)
* fix dashboard side actions

* lint being lint

(cherry picked from commit bfbf767663)
2021-05-18 11:55:03 -07:00
Evan Rusackasandhenryyeh e48d3f5315 fix: Adds space under dataset change warning (#14582)
* fix: Adding a little margin under the warning about changing datasets

* feat: moves Alert spacing from a css override to an Alert prop

* fix: prop needs to be optional... proptional

* fix: moving the typing to a better spot, adding the new prop to storybook.

* style: linting

(cherry picked from commit 6d786d4d47)
2021-05-18 11:55:03 -07:00
Geidoandhenryyeh 925a05e67e fix: Menu does not appear on scroll in Dashboard (#14566)
* Fix menu

* Fix test

(cherry picked from commit b960843015)
2021-05-18 11:55:03 -07:00
Geidoandhenryyeh 4daa87fcd0 fix: Column name and icons alignment in the Datasource Panel (Explore) (#14551)
* Fix column name alignment

* Fix space wrap

(cherry picked from commit 3a4536acac)
2021-05-18 11:55:03 -07:00
Beto DealmeidaandGitHub 09050aee62 fix: select country in examples chart (#14561) 2021-05-11 09:05:39 -07:00
Ben ReinhartandGitHub 0fe2bf1150 chore: Update FAB to 3.3.0 (#14556) 2021-05-11 10:00:07 +01:00
Lily KuangandGitHub adbe56c8a8 fix image width to 1000px for email (#14527) 2021-05-10 13:38:07 -07:00
GeidoandGitHub e21beb76f1 fix: White space between Chart and Data panel in Explore (#14495)
* Adjust white space

* Adjust chart padding
2021-05-10 13:03:43 -07:00
77bc51f6aa Adding logging for datasource/save requests (#14476)
Co-authored-by: michelle_thomas <michelle.thomas@airbnb.com>
2021-05-10 10:26:10 -07:00
Ben ReinhartandGitHub f33c433f33 feat: Containerize WebSocket server (#14514)
* feat: Containerize WebSocket server

* Add license

* Ensure Redis SSL is always turned off in dev
2021-05-10 09:28:50 -07:00
Ville BrofeldtandGitHub 37276e1af9 fix(viz): apply uniform sorting to all nvd3 timeseries charts (#14539) 2021-05-10 15:43:21 +03:00
Daniel Vaz GasparandGitHub 7a95f8f839 fix: flaky test on reports (#14544) 2021-05-10 13:27:12 +01:00
Bawantha Mihiran RajapakshaandGitHub 809983d32d Uptate CONTRIBUTING.md (#13095)
add nvm latest tag --lts
2021-05-10 13:28:32 +03:00
Phillip Kelley-DotsonandGitHub cee22e28c0 refactor: remove panel from userinfo component (#14364)
* first pass

* fix merge

* fix lint

* covert pane to styled comp

* add theme

* fix lint

* fix test

* remove unused import
2021-05-08 15:04:16 -07:00
Phillip Kelley-DotsonandGitHub e16c4d856e refactor(navbar): migrate Bootstrap navbar to AntD menus (#14184)
* initial commit

* more polish

* fix types and remove tests

* fix tests, update menu css, add oetc

* fix lint and precommit

* fix test

* update css, address comments

* fix lint

* update submenu for extra buttons

* remove block and lint

* fix lint

* remove block

* adjust margin

* test round 2

* test round 3

* about section

* src/components/Menu/Menu.test.tsx

* remove redundant test

* fmore pointed test

* fix lint

* remove unused css

* fix dashboard nav view

* update comments

* use suggestion

* lint-fix

* move css, fix dropdown and text

* lint

* rearchitect main nav component

* run lint fix

* nit
2021-05-08 14:57:59 -07:00
Michael S. MolinaandGitHub e7a4734742 chore: Removes tabs animation by default (#14496) 2021-05-08 14:22:16 -07:00
Michael S. MolinaandGitHub 79ff96269b refactor: Bootstrap to AntD - Form - iteration 3 (#14502) 2021-05-08 12:34:52 -07:00
Evan RusackasandGitHub 4f000cc8d1 fix: bringing metric type icon styles into SelectControl (#14531) 2021-05-08 15:47:25 +08:00
John BodleyandGitHub 6871ad15f1 docs(UPDATING): Adding downtime for #14234 (#14513) 2021-05-08 07:59:44 +12:00
Ville BrofeldtandGitHub 66a4c94a1e fix(chart-data): handle url_params in csv export and native filters (#14526) 2021-05-07 21:07:44 +03:00
Aleksandr GordienkoandGitHub c14665cad6 chore: Improved translation into Russian language (#14412) 2021-05-07 08:20:47 -07:00
Ville BrofeldtandGitHub d1d98d81b0 feat(dremio): implement convert_dttm method (#14519) 2021-05-07 15:31:58 +03:00
simcha90andGitHub e8e838e279 feat(native-filters): Auto apply changes in FiltersConfigModal (#14461)
* fix:fix get permission function

* feat: auto apply changes after save filters config modal

* fix: repopulate default values

* test: fix tests

* test: fix tests

* fix: fix CR notes

* fix: fix CR notes
2021-05-07 13:06:59 +03:00
Ville BrofeldtandGitHub 680c96ec54 feat(explore): collapse time section if no ts columns (#14493)
* feat(explore): collapse time section if no ts columns

* fix viz change bug

* fix test
2021-05-07 11:55:54 +03:00
MayurandGitHub 05c24056b5 add new plugin (#14516) 2021-05-07 10:21:00 +03:00
Ville BrofeldtandGitHub 19b408b08d chore: bump superset-ui to 0.17.44 (#14515) 2021-05-07 09:31:34 +03:00
10a53b7f5d fix: Add extra check to loggerMiddleware (#14492)
* Add extra check

* Update superset-frontend/src/middleware/loggerMiddleware.js

Co-authored-by: Evan Rusackas <evan@preset.io>

* Fix linting issue

Co-authored-by: Evan Rusackas <evan@preset.io>
2021-05-06 22:39:58 -07:00
David Aaron SuddjianandGitHub 52dbe311c7 fix: parameterize titles correctly (#14509) 2021-05-06 15:30:49 -07:00
Hugh A. Miles IIandGitHub 1df93848d2 fix: disable pylint error breaking CI (#14506) 2021-05-06 15:11:07 -04:00
Hugh A. Miles IIandGitHub 96289e93d7 feat: add make format command (#14491)
* Update Makefile

* fix makefile
2021-05-06 11:16:49 -04:00
cccs-rcandGitHub f55882e756 fix: SQL Statement on QUERY_LOGGER prints none to log (#14358) 2021-05-06 17:50:19 +03:00
Ville BrofeldtandGitHub 4ac90ddaee refactor(utils): replace strtobool with parse_boolean_string (#14494) 2021-05-06 15:57:02 +03:00
Michael S. MolinaandGitHub 68058b0ad9 chore: Moves spec files to the src folder - iteration 4 (#14380) 2021-05-05 22:01:05 -07:00
Michael S. MolinaandGitHub 331310db9b refactor: Bootstrap to AntD - Form - iteration 2 (#14379) 2021-05-05 22:00:37 -07:00
Michael S. MolinaandGitHub fa510df624 chore: Adds QueryParamProvider to testing helper (#14195) 2021-05-05 21:58:40 -07:00
Rob DiCiuccioandGitHub fa0915d9e6 Attempt to reduce asyncEvent test flakiness (#14497) 2021-05-05 19:34:13 -07:00
Elizabeth ThompsonandGitHub 9b42eec064 feat: db modal split (#14470)
* refactor: split db modal file (#14436)

* split db modal file

* fix tests

* fix extra options

* update database id methodology
2021-05-05 19:03:19 -04:00
AAfghahiandGitHub 12baba03cc fix: Query History (#14498)
* fix for QUery History

* fixed
2021-05-05 17:00:32 -04:00
Michael S. MolinaandGitHub f9567efe81 fix: Fix #13831 (#14490) 2021-05-05 13:48:48 -07:00
c832542570 fix(dashboard): Prevent char overflow when displaying chart description (#14467)
* Force refresh the chart on toggle display description

* Use forceUpdate instead of forceRefresh

* Instead of forceUpdate, add a state for desciption height

Co-authored-by: Ajay Mancheery <ajaymancheery@Ajays-MacBook-Pro.local>
2021-05-05 13:30:10 -07:00
Jesse YangandGitHub ae256a9119 chore: bump sankey and pivot table chart to 0.17.43 (#14477) 2021-05-05 11:17:18 -07:00
Evan RusackasandGitHub 8ccd19629a fix: explore flex layout is sometimes too short (#14481) 2021-05-05 10:21:15 -07:00
Michael S. MolinaandGitHub 7182a1b390 chore: Removes common storybook (#14418) 2021-05-05 09:53:14 -07:00
Kamil GabryjelskiandGitHub b030c9801c chore: Move styles from .less stylesheet to emotion in Explore (#14485)
* Move some styles from .less stylesheet to emotion in Explore

* Replace Global styles with local styled components

* Address comments
2021-05-05 15:36:52 +02:00
Ville BrofeldtandGitHub 93c7f5bb44 fix(annotations): pass force param to annotation request (#14483)
* fix(annotations): pass force param to annotation request

* use strtobool

* add util for parsing bools
2021-05-05 13:52:17 +03:00
Ville BrofeldtandGitHub 5aef6bccd1 feat(viz): add funnel chart (#14480) 2021-05-05 14:26:12 +08:00
Ben ReinhartandGitHub 90a72bf8d2 chore: Update WebSocket server code from feedback (#14417) 2021-05-04 22:07:53 -07:00
Erik RitterandGitHub a816655715 fix: dashboard datasources filter None (#14471) 2021-05-04 15:34:38 -07:00
Jesse YangandGitHub 23014e9233 chore: upgrade @emotion (#14454) 2021-05-04 15:26:16 -07:00
Michael S. MolinaandGitHub d138b4407d fix: Ignore database extra fields when saving (#14465) 2021-05-04 20:03:36 +01:00
Elizabeth ThompsonandGitHub 2242f74818 Revert "refactor: split db modal file (#14436)" (#14466)
This reverts commit 2d991ec359.
2021-05-04 14:40:58 -04:00
David Aaron SuddjianandGitHub 21cf12a480 chore(dashboard): Integrate dashboard app into the SPA bundle (#14356)
* chore(dashboard): Integrate dashboard app into the SPA bundle

* fix url params

* change variable name

* change title correctly

* custom css

* lint

* remove unused file

* remove content assertions from dashboard tests

* fix case with missing bootstrap data

* fix: respect crud views flag

* crud views -> spa

* remove unused dashboard templates

* fix: remove unused variable

* fix: missed a spot with the crudViews -> spa

* router link to dashboard from dashboard list page

* link using the router when in card mode

* lint

* fix tests, add memory router

* remove  dashboard app files

* split up the bundle a little more

* use webpack preload
2021-05-04 08:51:17 -07:00
Amit MiranandGitHub 158ac302d8 chore: bump table plugin to 0.17.42 (#14460) 2021-05-04 17:41:02 +03:00
simcha90andGitHub 582900c170 perf(native-filters): Load native filters after charts (#14443)
* fix:fix get permission function

* refactor: filter default value

* refactor: update default value loading

* refactor: apply defaultValues

* lint: fix lint

* lint: fix lint

* test: fix test

* refactor: use extraFormData for reload charts

* feat: load native filters after after charts

* feat: load filters after charts

* fix: revert changes

* test: fix timers

* test: fix tests
2021-05-04 16:07:40 +03:00
Duy Nguyen HoangandGitHub 9a22fb00d9 fix: CSV Export permission is not consistent (#13713) 2021-05-04 10:19:58 +03:00
Jesse YangandGitHub a75e4af99b fix(viz): bump superset-ui to fix bugs in table and graph chart (#14438) 2021-05-03 16:23:42 -07:00
Elizabeth ThompsonandGitHub 2d991ec359 refactor: split db modal file (#14436)
* split db modal file

* fix tests
2021-05-03 17:00:41 -04:00
Erik RitterandGitHub e05a70fc82 fix: dashboard changed on calculation (#14450) 2021-05-03 13:55:34 -07:00
ʈᵃᵢandGitHub 0b9fd24cf3 fix(logging): log unexpected exceptions as exceptions (#14399) 2021-05-03 12:33:02 -06:00
Ville BrofeldtandGitHub 2f9efb2e23 add migration (#14446) 2021-05-03 15:32:48 +03:00
simcha90andGitHub bbb1f2d757 perf(native-filters): avoid unnecessary reloading of charts (#14408)
* fix:fix get permission function

* refactor: filter default value

* refactor: update default value loading

* refactor: apply defaultValues

* lint: fix lint

* lint: fix lint

* test: fix test

* refactor: use extraFormData for reload charts

* test: fix tests

* test: fix tests

* test: fix tests
2021-05-03 14:56:41 +03:00
simcha90andGitHub abbf4bf05a refactor(native-filters): allow cascading only for filter_select (#14441)
* fix:fix get permission function

* refactor: allow cascading only for filter_select
2021-05-02 13:16:10 +03:00
1f8de1d06c test(maximize-chart): Add tests to maximize chart action (#14371)
* fixed FullSize charts broken #13600

* Update ChartHolder.jsx

* fix:fix get permission function

* test: adding tests to full screen

* lint: fix lint

* fix: fix CR notes

* fix: fix CR notes

* fix: fix CR notes

Co-authored-by: toop <mytoop@163.com>
2021-05-02 09:27:38 +03:00
7466595563 fix: fixing mysql error message (#14416)
* fixing mysql error message

* Update tests/db_engine_specs/mysql_tests.py

Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>

* Update tests/db_engine_specs/mysql_tests.py

Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>

* Update tests/db_engine_specs/mysql_tests.py

Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>

* Update superset/db_engine_specs/mysql.py

Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>

* fixed broken test

* changed error type

Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>
2021-04-30 15:15:37 -07:00
e507508b48 feat: Logic added to limiting factor column in Query model (#13521)
* Sqllab limit

* Add migration script

* Set default values

* initial push

* revisions

* Update superset/views/core.py

Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>

* moving migration to separate PR

* with migration

* revisions

* Fix apply_limit_to_sql

* all but tests

* added unit tests

* revisions

* Update superset/sql_lab.py

Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>

* Update superset/sql_parse.py

Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>

* fixed black issue

* Update superset/views/core.py

Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>

* updated logic

Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>
2021-04-30 15:15:18 -07:00
Hugh A. Miles IIandGitHub efcdc8cb9b change relationship (#14435) 2021-04-30 17:27:05 -04:00
Daniel Vaz GasparandGitHub b4e527a8c4 fix: bootstrap data permissions (#14348) 2021-04-30 12:02:01 -06:00
Sam Faber-ManningandGitHub b147fa84e2 fix: parse simple string error message values (#14360)
* fix parsing error messages that are formatted as single strings

* fix other instances of error typing
2021-04-30 09:30:53 -07:00
Sam Faber-ManningandGitHub 11260b3117 chore: add stack trace to all calls of logger.error (#14382) 2021-04-30 09:22:05 -07:00
Srini KadamatiandGitHub 13cfc421d5 update README with new docs and recordings (#14432) 2021-04-30 11:59:08 -04:00
JagadishandGitHub 9097c29d39 Renamed impyla from implya in impala.mdx and Renamed PIP package impyla from impala in index.mdx (#14425) 2021-04-30 19:51:32 +08:00
Ville BrofeldtandGitHub 319125ec3b fix(native-filters): fix filter scope error (#14426) 2021-04-30 12:20:02 +03:00
AAfghahiandGitHub 982a91ee62 feat: Adding limiting_factor column to Query model (#14234)
* add limiting factor to query model

* varchar instead of enum
2021-04-29 14:37:22 -07:00
Erik RitterandGitHub bc47bc8f66 feat: Add etag caching to dashboard APIs (#14357) 2021-04-29 14:14:26 -07:00
Michael S. MolinaandGitHub a52ff13b5d chore: Moves Card to the components folder (#14139) 2021-04-29 11:51:41 -07:00
GeidoandGitHub 545e257537 feat: Dynamic imports for the Icons component (#14318)
* Add aria-label and twotone

* Enhance LazyIcon

* Fix tests and solve ject warnings

* Add new line

* Revert package-lock to master

* Fix failing test

* Implement icon overrides

* Fix failing storybook

* Clean up

* Improve var name
2021-04-29 11:49:39 -07:00
Ben ReinhartandGitHub a2831382a9 feat: Support env vars configuration for WebSocket server (#14398) 2021-04-29 09:37:57 -07:00
Daniel Vaz GasparandGitHub 6541a03d0b fix: SQLLab role permissions (#14372)
* fix: SQLLab role permissions

* add missing perm

* fix tests

* fix security test

* fix security test

* fix tests
2021-04-29 15:58:08 +01:00
Ville BrofeldtandGitHub 1c16261651 fix(native-filters): always show filters without dataset (#14409) 2021-04-29 14:25:39 +03:00
2dc3fa3f21 fix error getting partitionQuery from table.partition (#14369)
Co-authored-by: eriendeng <eriendeng@tencent.com>
2021-04-29 10:42:11 +03:00
Michael S. MolinaandGitHub 1d6a746a09 refactor: Boostrap to AntD - Tabs (#14048) 2021-04-28 23:49:50 -07:00
Ville BrofeldtandGitHub 4410fd047e fix(native-filters): default value not populated on second opening (#14401) 2021-04-29 09:09:47 +03:00
Michael S. MolinaandGitHub b38aadf9e4 chore: Moves the branding folder to the assets folder (#14166) 2021-04-28 22:17:21 -07:00
Ben ReinhartandGitHub e7f5100833 fix: Fix unintended cache misses with async queries (#14291)
* bug: Fix unintended cache misses with async queries

* Ensure sort order

* Ensure columns are sorted

* Update failing tests
2021-04-28 12:14:55 -07:00
Srini KadamatiandGitHub b78b7b3fad docs: improved clickhouse connection details (#14394)
* docs: multiple small fixes around databases

* add link to postgres SSL options

* docs: added more Clickhouse connection details

* minor tweak
2021-04-28 13:06:49 -04:00
Ville BrofeldtandGitHub 183b5ae362 fix(sqllab): fix error due to anonymous user (#14390) 2021-04-28 19:57:45 +03:00
Srini KadamatiandGitHub bbdb4ee24f docs: multiple small fixes around databases (#14321)
* docs: multiple small fixes around databases

* add link to postgres SSL options
2021-04-28 10:30:28 -04:00
Ville BrofeldtandGitHub 43ee52be57 fix(native-filters): force update control value on change (#14385)
* fix(native-filters): force update control value on change

* fix test
2021-04-28 11:41:22 +03:00
simcha90andGitHub fc0c1155c2 fix(native-filters): fix lint (#14387)
* fix:fix get permission function

* lint: fix types
2021-04-28 10:16:21 +03:00
2486fd4f6d feat(native-filters): Show/Hide filter bar by metdata ff (#14261)
* fix:fix get permission function

* feat: hide native filters by metadata

* fix: fix show_native_filters name

* fix: metadata

* chore: add migration to hide native_filters in all existing dashboards

* chore: fix pre-commit

* fix: make migration migration dashboard with filter_box only

* fix: pre-commit

* feat: hide filter bar if no permission edit and no filters

* fix: remove migration

Co-authored-by: amitmiran137 <amit.miran@nielsen.com>
2021-04-28 07:49:03 +03:00
d8bb2d3e62 refactor(db_engine_specs): Removing top-level import of app (#14366)
Co-authored-by: John Bodley <john.bodley@airbnb.com>
2021-04-28 15:47:32 +12:00
Aakash NandandGitHub 77d17152bc Update trino connection docs (#14219) 2021-04-27 19:51:00 -04:00
ʈᵃᵢandGitHub 86d2a616c1 fix(emotion): add separate namespace for menu app (#14362) 2021-04-27 15:53:55 -06:00
Evan RusackasandGitHub 8e6803f65f chore: Bumping lodash version in Superset (#14203)
* Bumping lodash in main package

* new package lock with updated lodash
2021-04-27 14:54:56 -06:00
44c09b5383 feat: Setup supersetText configuration to overwrite errors (#14256)
* Setup error message overwrite config`

* fix

* bad reabse

* bad rebase 2

* remove unneeded comment

* working dynamic importing

* saveeee

* working textUtil file

* add ignore tslint

* fix linting

* fix linting

* import order

* setup guard statements

* linting again

* linting again

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>
2021-04-27 15:03:35 -04:00
Ville BrofeldtandGitHub 40fb94dcca feat(native-filters): add optional sort metric to select filter (#14346)
* feat(native-filters): add optional sort metric to select filter

* use verbose name when defined

* fixes

* lint

* disable flaky test

* disable flaky test

* disable flaky test
2021-04-27 20:28:38 +03:00
344 changed files with 10833 additions and 14387 deletions
+1
View File
@@ -43,4 +43,5 @@ install/
superset-frontend/cypress-base/
superset-frontend/coverage/
superset/static/assets/
superset-websocket/dist/
venv
+1
View File
@@ -54,6 +54,7 @@ envpy3
env36
local_config.py
/superset_config.py
/superset_text.yml
superset.egg-info/
superset/bin/supersetc
tmp
+2 -2
View File
@@ -512,8 +512,8 @@ First, be sure you are using recent versions of Node.js and npm. We recommend us
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.0/install.sh | bash
cd superset-frontend
nvm install
nvm use
nvm install --lts
nvm use --lts
```
Or if you use the default macOS starting with Catalina shell `zsh`, try:
+8
View File
@@ -47,3 +47,11 @@ pre-commit:
# setup pre commit dependencies
pip3 install -r requirements/integration.txt
pre-commit install
format: py-format js-format
py-format:
python -m black superset
js-format:
cd superset-frontend; npm run prettier
+45 -49
View File
@@ -17,8 +17,7 @@ specific language governing permissions and limitations
under the License.
-->
Superset
=========
# Superset
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/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)
@@ -31,7 +30,7 @@ Superset
[![Dependencies Status](https://david-dm.org/apache/superset/status.svg?path=superset-frontend)](https://david-dm.org/apache/superset?path=superset-frontend)
<img
src="https://github.com/apache/superset/raw/master/superset-frontend/branding/superset-logo-horiz-apache.png"
src="https://github.com/apache/superset/raw/master/superset-frontend/src/assets/branding/superset-logo-horiz-apache.png"
alt="Superset"
width="500"
/>
@@ -47,7 +46,6 @@ A modern, enterprise-ready business intelligence web application.
[**Resources**](#resources) |
[**Organizations Using Superset**](RESOURCES/INTHEWILD.md)
## Screenshots & Gifs
**Gallery**
@@ -74,26 +72,25 @@ A modern, enterprise-ready business intelligence web application.
<kbd><img title="Visualizations" src="superset-frontend/images/screenshots/explore_visualizations.jpg"/></kbd><br/>
## Why Superset?
Superset provides:
* An intuitive interface for visualizing datasets and
crafting interactive dashboards
* A wide array of beautiful visualizations to showcase your data
* Code-free visualization builder to extract and present datasets
* A world-class SQL IDE for preparing data for visualization, including a rich metadata browser
* A lightweight semantic layer which empowers data analysts to quickly define custom dimensions and metrics
* Out-of-the-box support for most SQL-speaking databases
* Seamless, in-memory asynchronous caching and queries
* An extensible security model that allows configuration of very intricate rules
on who can access which product features and datasets.
* Integration with major
authentication backends (database, OpenID, LDAP, OAuth, REMOTE_USER, etc)
* The ability to add custom visualization plugins
* An API for programmatic customization
* A cloud-native architecture designed from the ground up for scale
- An intuitive interface for visualizing datasets and
crafting interactive dashboards
- A wide array of beautiful visualizations to showcase your data
- Code-free visualization builder to extract and present datasets
- A world-class SQL IDE for preparing data for visualization, including a rich metadata browser
- A lightweight semantic layer which empowers data analysts to quickly define custom dimensions and metrics
- Out-of-the-box support for most SQL-speaking databases
- Seamless, in-memory asynchronous caching and queries
- An extensible security model that allows configuration of very intricate rules
on who can access which product features and datasets.
- Integration with major
authentication backends (database, OpenID, LDAP, OAuth, REMOTE_USER, etc)
- The ability to add custom visualization plugins
- An API for programmatic customization
- A cloud-native architecture designed from the ground up for scale
## Supported Databases
@@ -131,18 +128,16 @@ Here are some of the major database solutions that are supported:
Want to add support for your datastore or data engine? Read more [here](https://superset.apache.org/docs/frequently-asked-questions#does-superset-work-with-insert-database-engine-here) about the technical requirements.
## Installation and Configuration
[Extended documentation for Superset](https://superset.apache.org/docs/installation/installing-superset-using-docker-compose)
## Get Involved
* Ask and answer questions on [StackOverflow](https://stackoverflow.com/questions/tagged/apache-superset) using the **apache-superset** tag
* [Join our community's Slack](https://join.slack.com/t/apache-superset/shared_invite/zt-l5f5e0av-fyYu8tlfdqbMdz_sPLwUqQ)
- Ask and answer questions on [StackOverflow](https://stackoverflow.com/questions/tagged/apache-superset) using the **apache-superset** tag
- [Join our community's Slack](https://join.slack.com/t/apache-superset/shared_invite/zt-l5f5e0av-fyYu8tlfdqbMdz_sPLwUqQ)
and please read our [Slack Community Guidelines](https://github.com/apache/superset/blob/master/CODE_OF_CONDUCT.md#slack-community-guidelines)
* [Join our dev@superset.apache.org Mailing list](https://lists.apache.org/list.html?dev@superset.apache.org)
- [Join our dev@superset.apache.org Mailing list](https://lists.apache.org/list.html?dev@superset.apache.org)
## Contributor Guide
@@ -151,29 +146,30 @@ Interested in contributing? Check out our
to find resources around contributing along with a detailed guide on
how to set up a development environment.
## Resources
* Superset 1.0
* [Superset 1.0 Milestone](https://superset.apache.org/docs/version-one)
* [Superset 1.0 Release Notes](https://github.com/apache/superset/tree/master/RELEASING/release-notes-1-0)
* [Presentation on Superset 1.0 Public Roadmap](https://docs.google.com/presentation/d/1FGgyI8tLWLUPSQ5eEno78bylLfobj9O2W4yoUoFYHH8/edit#slide=id.g9c182b81b9_1_0)
* [Public Superset Roadmap](https://github.com/apache-superset/superset-roadmap/projects/1)
* Superset 101 -- Getting Started Guide (From [Preset Blog](https://preset.io/blog/))
* [Installing Apache Superset Locally](https://preset.io/blog/2020-05-11-getting-started-installing-superset/)
* [Installing Database Drivers](https://preset.io/blog/2020-05-18-install-db-drivers/)
* [Connect Superset To Google Sheets](https://preset.io/blog/2020-06-01-connect-superset-google-sheets/)
* [Create Your First Chart](https://preset.io/blog/2020-06-08-first-chart/)
* [Create Time Series Charts](https://preset.io/blog/2020-06-26-timeseries-chart/)
* [Documentation for End-Users (by Preset)](https://docs.preset.io/)
* [Docker image](https://hub.docker.com/r/apache/superset)
* [Recordings of Community Events](https://www.youtube.com/channel/UCMuwrvBsg_jjI2gLcm04R0g)
* [May 2020: Virtual Meetup. Topics: 0.36 Overview, Committers Self-Intro, Roadmap](https://www.youtube.com/watch?v=tXGDmqjmcTs&t=20s)
* [July 2020: Virtual Meetup. Topics: Visualization Plugins, 0.37 Preview, Demo](https://www.youtube.com/watch?v=f6up5x_iRbI)
* [November 2020: Virtual Meetup. Topics: Superset 1.0 & the Roadmap](https://www.youtube.com/watch?v=GwtWRUSEjk4)
* [November 2020: Live Demo. Topic: Superset Semantic Layer](https://www.youtube.com/watch?v=8VL4ZPLFUYI)
* [December 2020: Live Demo. Topic: Annotations](https://www.youtube.com/watch?v=Yk6bKgphj1Q)
* Custom Visualizations
* [Building Custom Viz Plugins](https://superset.apache.org/docs/installation/building-custom-viz-plugins)
* [Managing and Deploying Custom Viz Plugins](https://medium.com/nmc-techblog/apache-superset-manage-custom-viz-plugins-in-production-9fde1a708e55)
* [Superset API](https://superset.apache.org/docs/rest-api)
- Superset 1.0
- [Superset 1.0 Milestone](https://superset.apache.org/docs/version-one)
- [Superset 1.0 Release Notes](https://github.com/apache/superset/tree/master/RELEASING/release-notes-1-0)
- Getting Started with Superset
- [Superset in 2 Minutes using Docker Compose](https://superset.apache.org/docs/installation/installing-superset-using-docker-compose#installing-superset-locally-using-docker-compose)
- [Installing Database Drivers](https://superset.apache.org/docs/databases/dockeradddrivers)
- [Building New Database Connectors](https://preset.io/blog/building-database-connector/)
- [Create Your First Dashboard](https://superset.apache.org/docs/creating-charts-dashboards/first-dashboard)
- [Documentation for Superset End-Users (by Preset)](https://docs.preset.io/docs/terminology)
- Deploying Superset
- [Official Docker image](https://hub.docker.com/r/apache/superset)
- [Helm Chart](https://github.com/apache/superset/tree/master/helm/superset)
- [Recordings of Past Community Events](https://www.youtube.com/channel/UCMuwrvBsg_jjI2gLcm04R0g)
- [Meetup: Superset 1.0](https://www.youtube.com/watch?v=gEZkFF2kokk)
- [Live Demo: Interactive Time-series Analysis with Druid and Superset](https://www.youtube.com/watch?v=4eh7OTfMln8)
- [Live Demo: Visualizing MongoDB and Pinot Data using Trino](https://www.youtube.com/watch?v=Dw_al_26F6o)
- Upcoming Superset Events
- [Superset + Star Wars: May the 4th Be With You](https://preset.io/events/2021-05-04-superset-star-wars-may-the-4th-be-with-you)
- [Meetup - Developing and Deploying Custom Visualization Plugins in Superset](https://www.meetup.com/Global-Apache-Superset-Community-Meetup/events/277835486/)
- [Visualize Your Data Lake Using Athena and Superset](https://preset.io/events/2021-05-18-visualize-your-data-lake-using-athena-and-superset)
- Visualizations
- [Building Custom Viz Plugins](https://superset.apache.org/docs/installation/building-custom-viz-plugins)
- [Managing and Deploying Custom Viz Plugins](https://medium.com/nmc-techblog/apache-superset-manage-custom-viz-plugins-in-production-9fde1a708e55)
- [Why Apache Superset is Betting on Apache ECharts](https://preset.io/blog/2021-4-1-why-echarts/)
- [Superset API](https://superset.apache.org/docs/rest-api)
+2
View File
@@ -31,6 +31,8 @@ assists people when migrating to a new version.
### Breaking Changes
### Potential Downtime
- [14234](https://github.com/apache/superset/pull/14234): Adds the `limiting_factor` column to the `query` table. Give the migration includes a DDL operation on a heavily trafficed table, potential service downtime may be required.
### Deprecations
- [13440](https://github.com/apache/superset/pull/13440): Dashboard/Charts reports and old Alerts is deprecated. The following config keys are deprecated:
- ENABLE_ALERTS
+28
View File
@@ -61,6 +61,34 @@ services:
environment:
CYPRESS_CONFIG: "${CYPRESS_CONFIG}"
superset-websocket:
container_name: superset_websocket
build: ./superset-websocket
image: superset-websocket
ports:
- 8080:8080
depends_on:
- redis
# Mount everything in superset-websocket into container and
# then exclude node_modules and dist with bogus volume mount.
# This is necessary because host and container need to have
# their own, separate versions of these files. .dockerignore
# does not seem to work when starting the service through
# docker-compose.
#
# For example, node_modules may contain libs with native bindings.
# Those bindings need to be compiled for each OS and the container
# OS is not necessarily the same as host OS.
volumes:
- ./superset-websocket:/home/superset-websocket
- /home/superset-websocket/node_modules
- /home/superset-websocket/dist
environment:
- PORT=8080
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_SSL=false
superset-init:
image: *superset-image
container_name: superset_init
@@ -8,11 +8,38 @@ version: 1
## Clickhouse
To use Clickhouse with Superset, you will need to add the following Python libraries:
```
clickhouse-driver==0.2.0
clickhouse-sqlalchemy==0.1.6
```
If running Superset using Docker Compose, add the following to your `./docker/requirements-local.txt` file:
```
clickhouse-driver>=0.2.0
clickhouse-sqlalchemy>=0.1.6
```
The recommended connector library for Clickhouse is
[sqlalchemy-clickhouse](https://github.com/cloudflare/sqlalchemy-clickhouse).
The expected connection string is formatted as follows:
```
clickhouse://{username}:{password}@{hostname}:{port}/{database}
clickhouse+native://<user>:<password>@<host>:<port>/<database>[?options…]clickhouse://{username}:{password}@{hostname}:{port}/{database}
```
Here's a concrete example of a real connection string:
```
clickhouse+native://demo:demo@github.demo.trial.altinity.cloud/default?secure=true
```
If you're using Clickhouse locally on your computer, you can get away with using a native protocol URL that
uses the default user without a password (and doesn't encrypt the connection):
```
clickhouse+native://localhost/default
```
@@ -8,7 +8,7 @@ version: 1
## Apache Impala
The recommended connector library to Apache Hive is [implya](https://github.com/cloudera/impyla).
The recommended connector library to Apache Hive is [impyla](https://github.com/cloudera/impyla).
The expected connection string is formatted as follows:
@@ -8,13 +8,16 @@ version: 1
## Install Database Drivers
Superset requires a Python DB-API database driver and a SQLAlchemy dialect to be installed for each datastore you want to connect to.
Superset requires a Python DB-API database driver and a SQLAlchemy
dialect to be installed for each datastore you want to connect to.
You can read more [here](/docs/databases/dockeradddrivers) about how to install new database drivers into your Superset configuration.
You can read more [here](/docs/databases/dockeradddrivers) about how to
install new database drivers into your Superset configuration.
### Supported Databases and Dependencies
Superset does not ship bundled with connectivity to databases, except for SQLite, which is part of the Python standard library. Youll need to install the required packages for the database you want to use as your metadata database as well as the packages needed to connect to the databases you want to access through Superset.
Superset does not ship bundled with connectivity to databases, except for SQLite,
which is part of the Python standard library. Youll need to install the required packages for the database you want to use as your metadata database as well as the packages needed to connect to the databases you want to access through Superset.
A list of some of the recommended packages.
@@ -25,14 +28,14 @@ A list of some of the recommended packages.
|[Apache Drill](/docs/databases/drill)|```pip install sqlalchemy-drill```| ```drill+sadrill:// For JDBC drill+jdbc://``` |
|[Apache Druid](/docs/databases/druid)|```pip install pydruid```| ```druid://<User>:<password>@<Host>:<Port-default-9088>/druid/v2/sql``` |
|[Apache Hive](/docs/databases/hive)|```pip install pyhive```|```hive://hive@{hostname}:{port}/{database}```|
|[Apache Impala](/docs/databases/impala)|```pip install impala```|```impala://{hostname}:{port}/{database}```|
|[Apache Impala](/docs/databases/impala)|```pip install impyla```|```impala://{hostname}:{port}/{database}```|
|[Apache Kylin](/docs/databases/kylin)|```pip install kylinpy```|```kylin://<username>:<password>@<hostname>:<port>/<project>?<param1>=<value1>&<param2>=<value2>```|
|[Apache Pinot](/docs/databases/pinot)|```pip install pinotdb```|```pinot://BROKER:5436/query?server=http://CONTROLLER:5983/```|
|[Apache Solr](/docs/databases/solr)|```pip install sqlalchemy-solr```|```solr://{username}:{password}@{hostname}:{port}/{server_path}/{collection}```
|[Apache Spark SQL](/docs/databases/spark-sql)|```pip install pyhive```|```hive://hive@{hostname}:{port}/{database}```
|[Azure MS SQL](/docs/databases/sql-server)|```pip install pymssql``` |```mssql+pymssql://UserName@presetSQL:TestPassword@presetSQL.database.windows.net:1433/TestSchema```
|[Big Query](/docs/databases/bigquery)|```pip install pybigquery```|```bigquery://{project_id}```|
|[ClickHouse](/docs/databases/clickhouse)|```pip install sqlalchemy-clickhouse```|```clickhouse://{username}:{password}@{hostname}:{port}/{database}```|
|[ClickHouse](/docs/databases/clickhouse)|```pip install clickhouse-driver==0.2.0 && pip install clickhouse-sqlalchemy==0.1.6```|```clickhouse+native://{username}:{password}@{hostname}:{port}/{database}```|
|[CockroachDB](/docs/databases/cockroachdb)|```pip install cockroachdb```|```cockroachdb://root@{hostname}:{port}/{database}?sslmode=disable```|
|[Dremio](/docs/databases/dremio)|```pip install sqlalchemy_dremio```|```dremio://user:pwd@host:31010/```|
|[Elasticsearch](/docs/databases/elasticsearch)|```pip install elasticsearch-dbapi```|```elasticsearch+http://{user}:{password}@{host}:9200/```|
@@ -8,21 +8,16 @@ version: 1
## MySQL
The recommended connector library for MySQL is [mysqlclient](https://pypi.org/project/mysqlclient/).
The recommended connector library for MySQL is [mysql-connector-python](https://pypi.org/project/mysql-connector-python/).
Set up the Connection String using the following settings:
Here's the connection string:
User Name: UserName
Password: DBPassword
Database Host:
- For Localhost or docker running Linux: localhost or 127.0.0.1
```
mysql+mysqlconnector://{username}:{password}@{host}/{database}
```
Host:
- For Localhost or Docker running Linux: `localhost` or `127.0.0.1`
- For On Prem: IP address or Host name
- For AWS Endpoint
- For Docker running in OSX: docker.for.mac.host.internal
Database Name: Database Name
Port: default 3306
Connection String
```
mysql://<UserName>:<DBPassword>@<Database Host>/<Database Name>
```
- For Docker running in OSX: `docker.for.mac.host.internal`
Port: `3306` by default
@@ -25,15 +25,18 @@ Postgres sample connection parameters:
The connection string looks like:
```
postgresql+psycopg2://{username}:{password}@{host}:{port}/{database}
postgresql://{username}:{password}@{host}:{port}/{database}
```
Extra parameters:
You can require SSL by adding `?sslmode=require` at the end:
```
?sslmode=require
postgresql://{username}:{password}@{host}:{port}/{database}?sslmode=require
```
You can read about the other SSL modes that Postgres supports in
[Table 31-1 from this documentation](https://www.postgresql.org/docs/9.1/libpq-ssl.html).
More information about PostgreSQL connection options can be found in the
[SQLAlchemy docs](https://docs.sqlalchemy.org/en/13/dialects/postgresql.html#module-sqlalchemy.dialects.postgresql.psycopg2)
and the
@@ -14,13 +14,24 @@ The expected connection string is formatted as follows:
```
presto://{hostname}:{port}/{database}
e.g.
presto://presto.example.com:8080/hive
```
You can pass in a username and password as well:
```
presto://{username}:{password}@{hostname}:{port}/{database}
```
Here is an example connection string with values:
```
presto://datascientist:securepassword@presto.example.com:8080/hive
```
By default Superset assumes the most recent version of Presto is being used when querying the
datasource. If youre using an older version of Presto, you can configure it in the extra parameter:
```
{
"version": "0.123"
@@ -8,6 +8,8 @@ version: 1
## Trino
Supported trino version 352 and higher
The [sqlalchemy-trino](https://pypi.org/project/sqlalchemy-trino/) library is the recommended way to connect to Trino through SQLAlchemy.
The expected connection string is formatted as follows:
@@ -15,3 +17,11 @@ The expected connection string is formatted as follows:
```
trino://{username}:{password}@{hostname}:{port}/{catalog}
```
If you are running trino with docker on local machine please use the following connection URL
```
trino://trino@host.docker.internal:8080
```
Reference:
[Trino-Superset-Podcast](https://trino.io/episodes/12.html)
+1 -1
View File
@@ -66,7 +66,7 @@ dnspython==2.0.0
# via email-validator
email-validator==1.1.1
# via flask-appbuilder
flask-appbuilder==3.2.3
flask-appbuilder==3.3.0
# via apache-superset
flask-babel==1.0.0
# via flask-appbuilder
+1 -1
View File
@@ -75,7 +75,7 @@ setup(
"cron-descriptor",
"cryptography>=3.2.1",
"flask>=1.1.0, <2.0.0",
"flask-appbuilder>=3.2.3, <4.0.0",
"flask-appbuilder>=3.3.0, <4.0.0",
"flask-caching",
"flask-compress",
"flask-talisman",
+5
View File
@@ -58,4 +58,9 @@ addParameters({
{ name: 'Medium', value: '32px', default: true },
{ name: 'Large', value: '64px' },
],
options: {
storySort: {
method: 'alphabetical',
},
},
});
@@ -91,7 +91,7 @@ describe('Dashboard edit action', () => {
cy.get('.ant-modal-body')
.should('be.visible')
.contains('Title')
.siblings('input')
.get('[data-test="dashboard-title-input"]')
.type(`{selectall}{backspace}${dashboardTitle}`);
// save edit changes
@@ -19,9 +19,10 @@
import { CHART_LIST } from '../chart_list/chart_list.helper';
import { DASHBOARD_LIST } from '../dashboard_list/dashboard_list.helper';
// TODO: fix flaky init logic and re-enable
const milliseconds = new Date().getTime();
const dashboard = `Test Dashboard${milliseconds}`;
describe('Nativefilters', () => {
xdescribe('Nativefilters', () => {
before(() => {
cy.login();
cy.visit(DASHBOARD_LIST);
@@ -29,6 +29,16 @@ describe('Add database', () => {
// open modal
cy.get('[data-test="btn-create-database"]').click();
// values should be blank
cy.get('[data-test="database-modal"] input[name="database_name"]').should(
'have.value',
'',
);
cy.get('[data-test="database-modal"] input[name="sqlalchemy_uri"]').should(
'have.value',
'',
);
// type values
cy.get('[data-test="database-modal"] input[name="database_name"]')
.focus()
@@ -54,6 +64,14 @@ describe('Add database', () => {
// open modal
cy.get('[data-test="database-edit"]:last').click();
// it should show saved values
cy.get('[data-test="database-modal"]:last input[name="sqlalchemy_uri"]')
.invoke('val')
.should('not.be.empty');
cy.get('[data-test="database-modal"] input[name="database_name"]')
.invoke('val')
.should('not.be.empty');
cy.get('[data-test="database-modal"]:last input[name="sqlalchemy_uri"]')
.focus()
.dblclick()
@@ -111,7 +111,7 @@ describe('VizType control', () => {
// should load mathjs for line chart
cy.get('script[src*="mathjs"]').should('have.length', 1);
cy.get('script').then(nodes => {
expect(nodes.length).to.eq(numScripts);
expect(nodes.length).to.greaterThan(numScripts);
});
cy.get('button[data-test="run-query-button"]').click();
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -11,7 +11,7 @@
"license": "Apache-2.0",
"dependencies": {
"@cypress/code-coverage": "^3.9.2",
"@superset-ui/core": "^0.17.18",
"@superset-ui/core": "^0.17.42",
"react-dom": "^16.13.0",
"rison": "^0.1.1",
"shortid": "^2.2.15"
@@ -0,0 +1,19 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"></svg>

After

Width:  |  Height:  |  Size: 866 B

+2 -1
View File
@@ -21,7 +21,7 @@ module.exports = {
moduleNameMapper: {
'\\.(css|less)$': '<rootDir>/spec/__mocks__/styleMock.js',
'\\.(gif|ttf|eot|png)$': '<rootDir>/spec/__mocks__/fileMock.js',
'\\.svg$': '<rootDir>/spec/__mocks__/svgrMock.js',
'\\.svg$': '<rootDir>/spec/__mocks__/svgrMock.tsx',
'^src/(.*)$': '<rootDir>/src/$1',
'^spec/(.*)$': '<rootDir>/spec/$1',
},
@@ -35,6 +35,7 @@ module.exports = {
'^.+\\.tsx?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
snapshotSerializers: ['@emotion/jest/enzyme-serializer'],
globals: {
'ts-jest': {
babelConfig: true,
+1172 -8283
View File
File diff suppressed because it is too large Load Diff
+35 -34
View File
@@ -64,36 +64,38 @@
"@ant-design/icons": "^4.2.2",
"@babel/runtime-corejs3": "^7.12.5",
"@data-ui/sparkline": "^0.0.84",
"@emotion/core": "^10.0.35",
"@superset-ui/chart-controls": "^0.17.36",
"@superset-ui/core": "^0.17.32",
"@superset-ui/legacy-plugin-chart-calendar": "^0.17.36",
"@superset-ui/legacy-plugin-chart-chord": "^0.17.36",
"@superset-ui/legacy-plugin-chart-country-map": "^0.17.36",
"@superset-ui/legacy-plugin-chart-event-flow": "^0.17.36",
"@superset-ui/legacy-plugin-chart-force-directed": "^0.17.36",
"@superset-ui/legacy-plugin-chart-heatmap": "^0.17.36",
"@superset-ui/legacy-plugin-chart-histogram": "^0.17.36",
"@superset-ui/legacy-plugin-chart-horizon": "^0.17.36",
"@superset-ui/legacy-plugin-chart-map-box": "^0.17.36",
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.36",
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.36",
"@superset-ui/legacy-plugin-chart-partition": "^0.17.36",
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.36",
"@superset-ui/legacy-plugin-chart-rose": "^0.17.36",
"@superset-ui/legacy-plugin-chart-sankey": "^0.17.36",
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.36",
"@superset-ui/legacy-plugin-chart-sunburst": "^0.17.36",
"@superset-ui/legacy-plugin-chart-treemap": "^0.17.36",
"@superset-ui/legacy-plugin-chart-world-map": "^0.17.36",
"@superset-ui/legacy-preset-chart-big-number": "^0.17.36",
"@emotion/babel-preset-css-prop": "^11.2.0",
"@emotion/cache": "^11.1.3",
"@emotion/react": "^11.1.5",
"@superset-ui/chart-controls": "^0.17.42",
"@superset-ui/core": "^0.17.42",
"@superset-ui/legacy-plugin-chart-calendar": "^0.17.42",
"@superset-ui/legacy-plugin-chart-chord": "^0.17.42",
"@superset-ui/legacy-plugin-chart-country-map": "^0.17.42",
"@superset-ui/legacy-plugin-chart-event-flow": "^0.17.42",
"@superset-ui/legacy-plugin-chart-force-directed": "^0.17.42",
"@superset-ui/legacy-plugin-chart-heatmap": "^0.17.42",
"@superset-ui/legacy-plugin-chart-histogram": "^0.17.42",
"@superset-ui/legacy-plugin-chart-horizon": "^0.17.42",
"@superset-ui/legacy-plugin-chart-map-box": "^0.17.42",
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.42",
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.42",
"@superset-ui/legacy-plugin-chart-partition": "^0.17.42",
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.42",
"@superset-ui/legacy-plugin-chart-rose": "^0.17.42",
"@superset-ui/legacy-plugin-chart-sankey": "^0.17.43",
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.42",
"@superset-ui/legacy-plugin-chart-sunburst": "^0.17.42",
"@superset-ui/legacy-plugin-chart-treemap": "^0.17.42",
"@superset-ui/legacy-plugin-chart-world-map": "^0.17.42",
"@superset-ui/legacy-preset-chart-big-number": "^0.17.42",
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.6",
"@superset-ui/legacy-preset-chart-nvd3": "^0.17.36",
"@superset-ui/plugin-chart-echarts": "^0.17.38",
"@superset-ui/plugin-chart-pivot-table": "^0.17.36",
"@superset-ui/plugin-chart-table": "^0.17.37",
"@superset-ui/plugin-chart-word-cloud": "^0.17.36",
"@superset-ui/preset-chart-xy": "^0.17.36",
"@superset-ui/legacy-preset-chart-nvd3": "^0.17.42",
"@superset-ui/plugin-chart-echarts": "^0.17.44",
"@superset-ui/plugin-chart-pivot-table": "^0.17.44",
"@superset-ui/plugin-chart-table": "^0.17.44",
"@superset-ui/plugin-chart-word-cloud": "^0.17.42",
"@superset-ui/preset-chart-xy": "^0.17.42",
"@vx/responsive": "^0.0.195",
"abortcontroller-polyfill": "^1.1.9",
"antd": "^4.9.4",
@@ -118,10 +120,11 @@
"interweave": "^11.2.0",
"jquery": "^3.5.1",
"js-levenshtein": "^1.1.6",
"js-yaml-loader": "^1.2.2",
"json-bigint": "^1.0.0",
"json-stringify-pretty-compact": "^2.0.0",
"lodash": "^4.17.20",
"lodash-es": "^4.17.14",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"match-sorter": "^6.1.0",
"mathjs": "^8.0.1",
"memoize-one": "^5.1.1",
@@ -191,7 +194,7 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@babel/register": "^7.12.10",
"@emotion/babel-preset-css-prop": "^10.2.1",
"@emotion/jest": "^11.3.0",
"@hot-loader/react-dom": "^16.13.0",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@storybook/addon-actions": "^6.1.17",
@@ -243,7 +246,6 @@
"babel-jest": "^26.6.3",
"babel-loader": "^8.2.2",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-emotion": "^10.0.33",
"babel-plugin-jsx-remove-data-test-id": "^2.1.3",
"babel-plugin-lodash": "^3.3.4",
"cache-loader": "^1.2.2",
@@ -251,7 +253,6 @@
"copy-webpack-plugin": "^6.0.3",
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"emotion-ts-plugin": "^0.5.3",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.14.0",
"eslint": "^7.17.0",
@@ -0,0 +1,29 @@
/**
* 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 React, { SVGProps } from 'react';
const SvgrMock = React.forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
(props, ref) => <svg ref={ref} {...props} />,
);
SvgrMock.displayName = 'SvgrMock';
export const ReactComponent = SvgrMock;
export default SvgrMock;
+1 -1
View File
@@ -29,7 +29,7 @@ export default {
chartUpdateStartTime: 1525852454838,
latestQueryFormData: {},
queryRequest: {},
queryResponse: {},
queriesResponse: [{}],
triggerQuery: false,
lastRendered: 0,
form_data: {
+15 -5
View File
@@ -21,7 +21,6 @@ import { NativeFiltersState } from 'src/dashboard/reducers/types';
import { DataMaskStateWithId } from '../../src/dataMask/types';
export const nativeFilters: NativeFiltersState = {
isInitialized: true,
filterSets: {},
filters: {
'NATIVE_FILTER-e7Q8zKixx': {
@@ -36,7 +35,11 @@ export const nativeFilters: NativeFiltersState = {
},
},
],
defaultValue: null,
defaultDataMask: {
filterState: {
value: null,
},
},
cascadeParentIds: [],
scope: {
rootPath: ['ROOT_ID'],
@@ -61,7 +64,11 @@ export const nativeFilters: NativeFiltersState = {
},
},
],
defaultValue: null,
defaultDataMask: {
filterState: {
value: null,
},
},
cascadeParentIds: [],
scope: {
rootPath: ['ROOT_ID'],
@@ -115,14 +122,17 @@ export const extraFormData: ExtraFormData = {
export const NATIVE_FILTER_ID = 'NATIVE_FILTER-p4LImrSgA';
export const singleNativeFiltersState = {
isInitialized: true,
filters: {
[NATIVE_FILTER_ID]: {
id: [NATIVE_FILTER_ID],
name: 'eth',
type: 'text',
targets: [{ datasetId: 13, column: { name: 'ethnic_minority' } }],
defaultValue: null,
defaultDataMask: {
filterState: {
value: null,
},
},
cascadeParentIds: [],
scope: { rootPath: ['ROOT_ID'], excluded: [227, 229] },
inverseSelection: false,
+1 -1
View File
@@ -19,7 +19,7 @@
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from 'src/dashboard/reducers/index';
import { rootReducer } from 'src/views/store';
import mockState from './mockState';
import {
@@ -53,5 +53,5 @@ export default {
explore,
sqlLab,
localStorageUsageInKilobytes,
common,
common: () => common,
};
@@ -26,16 +26,19 @@ import thunk from 'redux-thunk';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import reducerIndex from 'spec/helpers/reducerIndex';
import { QueryParamProvider } from 'use-query-params';
type Options = Omit<RenderOptions, 'queries'> & {
useRedux?: boolean;
useDnd?: boolean;
useQueryParams?: boolean;
initialState?: {};
reducers?: {};
};
function createWrapper(options?: Options) {
const { useDnd, useRedux, initialState, reducers } = options || {};
const { useDnd, useRedux, useQueryParams, initialState, reducers } =
options || {};
return ({ children }: { children?: ReactNode }) => {
let result = (
@@ -56,6 +59,10 @@ function createWrapper(options?: Options) {
result = <Provider store={store}>{result}</Provider>;
}
if (useQueryParams) {
result = <QueryParamProvider>{result}</QueryParamProvider>;
}
return result;
};
}
@@ -24,7 +24,7 @@ import fetchMock from 'fetch-mock';
import { ParentSize } from '@vx/responsive';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
import { Sticky, StickyContainer } from 'react-sticky';
import { TabContainer, TabContent, TabPane } from 'react-bootstrap';
import Tabs from 'src/components/Tabs';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import BuilderComponentPane from 'src/dashboard/components/BuilderComponentPane';
@@ -39,7 +39,10 @@ import {
} from 'spec/fixtures/mockDashboardLayout';
import { mockStoreWithTabs, storeWithState } from 'spec/fixtures/mockStore';
import mockState from 'spec/fixtures/mockState';
import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
import {
DASHBOARD_ROOT_ID,
DASHBOARD_GRID_ID,
} from 'src/dashboard/util/constants';
fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
@@ -118,19 +121,17 @@ describe('DashboardBuilder', () => {
});
});
it('should render a TabContainer and TabContent', () => {
it('should render one Tabs and two TabPane', () => {
const wrapper = setup({ dashboardLayout: undoableDashboardLayoutWithTabs });
const parentSize = wrapper.find(ParentSize);
expect(parentSize.find(TabContainer)).toHaveLength(1);
expect(parentSize.find(TabContent)).toHaveLength(1);
expect(parentSize.find(Tabs)).toHaveLength(1);
expect(parentSize.find(Tabs.TabPane)).toHaveLength(2);
});
it('should set animation=true, mountOnEnter=true, and unmounOnExit=false on TabContainer for perf', () => {
it('should set animated=true on Tabs for perf', () => {
const wrapper = setup({ dashboardLayout: undoableDashboardLayoutWithTabs });
const tabProps = wrapper.find(ParentSize).find(TabContainer).props();
expect(tabProps.animation).toBe(true);
expect(tabProps.mountOnEnter).toBe(true);
expect(tabProps.unmountOnExit).toBe(false);
const tabProps = wrapper.find(ParentSize).find(Tabs).props();
expect(tabProps.animated).toEqual({ inkBar: true, tabPane: false });
});
it('should render a TabPane and DashboardGrid for first Tab', () => {
@@ -138,10 +139,10 @@ describe('DashboardBuilder', () => {
const parentSize = wrapper.find(ParentSize);
const expectedCount =
undoableDashboardLayoutWithTabs.present.TABS_ID.children.length;
expect(parentSize.find(TabPane)).toHaveLength(expectedCount);
expect(parentSize.find(TabPane).first().find(DashboardGrid)).toHaveLength(
1,
);
expect(parentSize.find(Tabs.TabPane)).toHaveLength(expectedCount);
expect(
parentSize.find(Tabs.TabPane).first().find(DashboardGrid),
).toHaveLength(1);
});
it('should render a TabPane and DashboardGrid for second Tab', () => {
@@ -155,8 +156,10 @@ describe('DashboardBuilder', () => {
const parentSize = wrapper.find(ParentSize);
const expectedCount =
undoableDashboardLayoutWithTabs.present.TABS_ID.children.length;
expect(parentSize.find(TabPane)).toHaveLength(expectedCount);
expect(parentSize.find(TabPane).at(1).find(DashboardGrid)).toHaveLength(1);
expect(parentSize.find(Tabs.TabPane)).toHaveLength(expectedCount);
expect(
parentSize.find(Tabs.TabPane).at(1).find(DashboardGrid),
).toHaveLength(1);
});
it('should render a BuilderComponentPane if editMode=false and user selects "Insert Components" pane', () => {
@@ -179,7 +182,7 @@ describe('DashboardBuilder', () => {
dashboardLayout: undoableDashboardLayoutWithTabs,
});
expect(wrapper.find(TabContainer).prop('activeKey')).toBe(0);
expect(wrapper.find(Tabs).prop('activeKey')).toBe(DASHBOARD_GRID_ID);
wrapper
.find('.dashboard-component-tabs .ant-tabs .ant-tabs-tab')
@@ -167,7 +167,7 @@ describe('Dashboard', () => {
...OVERRIDE_FILTERS,
[NATIVE_FILTER_ID]: {
scope: [230],
values: [extraFormData],
values: extraFormData,
},
});
});
@@ -31,6 +31,7 @@ import {
import { sliceId } from 'spec/fixtures/mockChartQueries';
import { dashboardFilters } from 'spec/fixtures/mockDashboardFilters';
import { dashboardWithFilter } from 'spec/fixtures/mockDashboardLayout';
import Icons from 'src/components/Icons';
import { FeatureFlag } from 'src/featureFlags';
describe('FiltersBadge', () => {
@@ -129,7 +130,7 @@ describe('FiltersBadge', () => {
).toHaveText('1');
// to look at the shape of the wrapper use:
// console.log(wrapper.dive().debug())
expect(wrapper.dive().find('Icon[name="alert-solid"]')).toExist();
expect(wrapper.dive().find(Icons.AlertSolid)).toExist();
});
});
@@ -216,7 +217,7 @@ describe('FiltersBadge', () => {
expect(
wrapper.dive().find('[data-test="incompatible-filter-count"]'),
).toHaveText('1');
expect(wrapper.dive().find('Icon[name="alert-solid"]')).toExist();
expect(wrapper.dive().find(Icons.AlertSolid)).toExist();
});
});
});
@@ -30,7 +30,6 @@ export const mockDataMaskInfo: DataMaskStateWithId = {
};
export const nativeFiltersInfo: NativeFiltersState = {
isInitialized: true,
filterSets: {
'set-id': {
id: 'DefaultsID',
@@ -54,7 +53,11 @@ export const nativeFiltersInfo: NativeFiltersState = {
},
},
],
defaultValue: null,
defaultDataMask: {
filterState: {
value: null,
},
},
scope: {
rootPath: [],
excluded: [],
@@ -29,6 +29,16 @@ describe('getFormDataWithExtraFilters', () => {
const filterId = 'native-filter-1';
const mockChart = {
id: chartId,
chartAlert: null,
chartStatus: null,
chartUpdateEndTime: null,
chartUpdateStartTime: 1,
lastRendered: 1,
latestQueryFormData: {},
sliceFormData: null,
queryController: null,
queriesResponse: null,
triggerQuery: false,
formData: {
viz_type: 'filter_select',
filters: [
@@ -43,7 +53,7 @@ describe('getFormDataWithExtraFilters', () => {
const mockArgs: GetFormDataWithExtraFiltersArguments = {
chartConfiguration: {},
charts: {
[chartId]: mockChart,
[chartId as number]: mockChart,
},
chart: mockChart,
filters: {
@@ -52,7 +62,6 @@ describe('getFormDataWithExtraFilters', () => {
},
sliceId: chartId,
nativeFilters: {
isInitialized: true,
filterSets: {},
filters: {
[filterId]: ({
@@ -20,7 +20,7 @@
import React from 'react';
import sinon from 'sinon';
import { shallow } from 'enzyme';
import { FormGroup } from 'react-bootstrap';
import { FormItem } from 'src/components/Form';
import Button from 'src/components/Button';
import { AGGREGATES } from 'src/explore/constants';
@@ -67,7 +67,7 @@ function setup(overrides) {
describe('AdhocMetricEditPopover', () => {
it('renders a popover with edit metric form contents', () => {
const { wrapper } = setup();
expect(wrapper.find(FormGroup)).toHaveLength(4);
expect(wrapper.find(FormItem)).toHaveLength(3);
expect(wrapper.find(Button)).toHaveLength(2);
});
@@ -16,50 +16,38 @@
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable no-unused-expressions */
import React from 'react';
import { FormControl } from 'react-bootstrap';
import sinon from 'sinon';
import { mount } from 'enzyme';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import BoundsControl from 'src/explore/components/controls/BoundsControl';
const defaultProps = {
name: 'y_axis_bounds',
label: 'Bounds of the y axis',
onChange: sinon.spy(),
onChange: jest.fn(),
};
describe('BoundsControl', () => {
let wrapper;
test('renders two inputs', () => {
render(<BoundsControl {...defaultProps} />);
expect(screen.getAllByRole('spinbutton')).toHaveLength(2);
});
beforeEach(() => {
wrapper = mount(<BoundsControl {...defaultProps} />);
});
test('receives null on non-numeric', async () => {
render(<BoundsControl {...defaultProps} />);
const minInput = screen.getAllByRole('spinbutton')[0];
userEvent.type(minInput, 'text');
await waitFor(() =>
expect(defaultProps.onChange).toHaveBeenCalledWith([null, null]),
);
});
it('renders two FormControls', () => {
expect(wrapper.find(FormControl)).toHaveLength(2);
});
it('errors on non-numeric', () => {
wrapper
.find(FormControl)
.first()
.simulate('change', { target: { value: 's' } });
expect(defaultProps.onChange.calledWith([null, null])).toBe(true);
expect(defaultProps.onChange.getCall(0).args[1][0]).toContain(
'value should be numeric',
);
});
it('casts to numeric', () => {
wrapper
.find(FormControl)
.first()
.simulate('change', { target: { value: '1' } });
wrapper
.find(FormControl)
.last()
.simulate('change', { target: { value: '5' } });
expect(defaultProps.onChange.calledWith([1, 5])).toBe(true);
});
test('calls onChange with correct values', async () => {
render(<BoundsControl {...defaultProps} />);
const minInput = screen.getAllByRole('spinbutton')[0];
const maxInput = screen.getAllByRole('spinbutton')[1];
userEvent.type(minInput, '1');
userEvent.type(maxInput, '2');
await waitFor(() =>
expect(defaultProps.onChange).toHaveBeenLastCalledWith([1, 2]),
);
});
@@ -24,7 +24,7 @@ import { Menu } from 'src/common/components';
import DatasourceModal from 'src/datasource/DatasourceModal';
import ChangeDatasourceModal from 'src/datasource/ChangeDatasourceModal';
import DatasourceControl from 'src/explore/components/controls/DatasourceControl';
import Icon from 'src/components/Icon';
import Icons from 'src/components/Icons';
import { Tooltip } from 'src/components/Tooltip';
const defaultProps = {
@@ -119,8 +119,7 @@ describe('DatasourceControl', () => {
it('should render health check message', () => {
const wrapper = setup();
const alert = wrapper.find(Icon);
expect(alert.at(1).prop('name')).toBe('alert-solid');
expect(wrapper.find(Icons.AlertSolid)).toExist();
const tooltip = wrapper.find(Tooltip).at(0);
expect(tooltip.prop('title')).toBe(
defaultProps.datasource.health_check_message,
@@ -17,14 +17,19 @@
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { shallow, mount } from 'enzyme';
import { mockStore } from 'spec/fixtures/mockStore';
import ExploreActionButtons from 'src/explore/components/ExploreActionButtons';
import * as exploreUtils from 'src/explore/exploreUtils';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
import { Provider } from 'react-redux';
import sinon from 'sinon';
describe('ExploreActionButtons', () => {
const defaultProps = {
actions: {},
canDownload: 'True',
canDownloadCSV: 'True',
latestQueryFormData: {},
queryEndpoint: 'localhost',
chartHeight: '30px',
@@ -42,4 +47,39 @@ describe('ExploreActionButtons', () => {
);
expect(wrapper.dive().children()).toHaveLength(6);
});
describe('ExploreActionButtons and no permission to download CSV', () => {
let wrapper;
const defaultProps = {
actions: {},
canDownloadCSV: false,
latestQueryFormData: {},
queryEndpoint: 'localhost',
chartHeight: '30px',
};
beforeEach(() => {
wrapper = mount(
<ThemeProvider theme={supersetTheme}>
<ExploreActionButtons {...defaultProps} />
</ThemeProvider>,
{
wrappingComponent: Provider,
wrappingComponentProps: {
store: mockStore,
},
},
);
});
it('should render csv buttons but is disabled and not clickable', () => {
const spyExportChart = sinon.spy(exploreUtils, 'exportChart');
const csvButton = wrapper.find('div.disabled');
expect(wrapper).toHaveLength(1);
csvButton.simulate('click');
expect(spyExportChart.callCount).toBe(0);
spyExportChart.restore();
});
});
});
@@ -19,14 +19,13 @@
/* eslint-disable no-unused-expressions */
import React from 'react';
import { shallow } from 'enzyme';
import { ColumnOption } from '@superset-ui/chart-controls';
import { styledMount as mount } from 'spec/helpers/theming';
import FilterDefinitionOption from 'src/explore/components/controls/MetricControl/FilterDefinitionOption';
import AdhocMetricStaticOption from 'src/explore/components/controls/MetricControl/AdhocMetricStaticOption';
import { AGGREGATES } from 'src/explore/constants';
import AdhocMetric, {
EXPRESSION_TYPES,
} from 'src/explore/components/controls/MetricControl/AdhocMetric';
import { StyledColumnOption } from 'src/explore/components/optionRenderers';
const sumValueAdhocMetric = new AdhocMetric({
expressionType: EXPRESSION_TYPES.SIMPLE,
@@ -35,26 +34,26 @@ const sumValueAdhocMetric = new AdhocMetric({
});
describe('FilterDefinitionOption', () => {
it('renders a ColumnOption given a column', () => {
it('renders a StyledColumnOption given a column', () => {
const wrapper = shallow(
<FilterDefinitionOption option={{ column_name: 'a_column' }} />,
);
expect(wrapper.find(ColumnOption)).toExist();
expect(wrapper.find(StyledColumnOption)).toExist();
});
it('renders a AdhocMetricStaticOption given an adhoc metric', () => {
it('renders a StyledColumnOption given an adhoc metric', () => {
const wrapper = shallow(
<FilterDefinitionOption option={sumValueAdhocMetric} />,
);
expect(wrapper.find(AdhocMetricStaticOption)).toExist();
expect(wrapper.find(StyledColumnOption)).toExist();
});
it('renders the metric name given a saved metric', () => {
const wrapper = shallow(
const wrapper = mount(
<FilterDefinitionOption
option={{ saved_metric_name: 'my_custom_metric' }}
/>,
);
expect(wrapper.text()).toBe('<ColumnTypeLabel />my_custom_metric');
expect(wrapper.find('.option-label').text()).toBe('my_custom_metric');
});
});
@@ -19,10 +19,13 @@
import React from 'react';
import configureStore from 'redux-mock-store';
import { shallow } from 'enzyme';
import { ColumnOption, MetricOption } from '@superset-ui/chart-controls';
import MetricDefinitionOption from 'src/explore/components/controls/MetricControl/MetricDefinitionOption';
import AggregateOption from 'src/explore/components/controls/MetricControl/AggregateOption';
import {
StyledMetricOption,
StyledColumnOption,
} from 'src/explore/components/optionRenderers';
describe('MetricDefinitionOption', () => {
const mockStore = configureStore([]);
@@ -32,16 +35,16 @@ describe('MetricDefinitionOption', () => {
return shallow(<MetricDefinitionOption store={store} {...props} />).dive();
}
it('renders a MetricOption given a saved metric', () => {
it('renders a StyledMetricOption given a saved metric', () => {
const wrapper = setup({
option: { metric_name: 'a_saved_metric', expression: 'COUNT(*)' },
});
expect(wrapper.find(MetricOption)).toExist();
expect(wrapper.find(StyledMetricOption)).toExist();
});
it('renders a ColumnOption given a column', () => {
it('renders a StyledColumnOption given a column', () => {
const wrapper = setup({ option: { column_name: 'a_column' } });
expect(wrapper.find(ColumnOption)).toExist();
expect(wrapper.find(StyledColumnOption)).toExist();
});
it('renders an AggregateOption given an aggregate metric', () => {
@@ -55,7 +55,7 @@ function setup(overrides) {
...overrides,
};
const wrapper = shallow(<MetricsControl {...props} />);
const component = wrapper.dive().shallow();
const component = wrapper.shallow();
return { wrapper, component, onChange };
}
@@ -24,7 +24,6 @@ import { Provider } from 'react-redux';
import { shallow } from 'enzyme';
import { styledMount as mount } from 'spec/helpers/theming';
import { FormControl } from 'react-bootstrap';
import { Radio } from 'src/components/Radio';
import Button from 'src/components/Button';
import sinon from 'sinon';
@@ -91,7 +90,6 @@ describe('SaveModal', () => {
it('renders a Modal with the right set of components', () => {
const wrapper = getWrapper();
expect(wrapper.find(StyledModal)).toExist();
expect(wrapper.find(FormControl)).toExist();
expect(wrapper.find(Radio)).toHaveLength(2);
const footerWrapper = shallow(wrapper.find(StyledModal).props().footer);
@@ -65,7 +65,7 @@ function verify(sourceProp: string) {
async function setup({
extraProps,
baseControl = MetricsControl,
baseControl = MetricsControl as WithAsyncVerificationOptions['baseControl'],
onChange,
}: Partial<WithAsyncVerificationOptions> & {
extraProps?: ExtraControlProps;
@@ -121,7 +121,7 @@ describe('VerifiedMetricsControl', () => {
});
const child = wrapper.find(MetricsControl);
child.props().onChange(['abc']);
child.props().onChange?.(['abc']);
expect(child.length).toBe(1);
expect(mockOnChange).toBeCalledTimes(1);
@@ -129,21 +129,15 @@ describe('Filter utils', () => {
);
});
it('getSelectExtraFormData - col: "testCol", value: [], emptyFilter: false, inverseSelection: false', () => {
expect(getSelectExtraFormData('testCol', [], false, false)).toEqual({
filters: [],
});
expect(getSelectExtraFormData('testCol', [], false, false)).toEqual({});
});
it('getSelectExtraFormData - col: "testCol", value: undefined, emptyFilter: false, inverseSelection: false', () => {
expect(
getSelectExtraFormData('testCol', undefined, false, false),
).toEqual({
filters: [],
});
).toEqual({});
});
it('getSelectExtraFormData - col: "testCol", value: null, emptyFilter: false, inverseSelection: false', () => {
expect(getSelectExtraFormData('testCol', null, false, false)).toEqual({
filters: [],
});
expect(getSelectExtraFormData('testCol', null, false, false)).toEqual({});
});
});
@@ -20,7 +20,6 @@ import React from 'react';
import { Row, Col } from 'src/common/components';
import { shallow } from 'enzyme';
import App from 'src/profile/components/App';
import Tabs from 'src/components/Tabs';
import { user } from './fixtures';
@@ -40,6 +39,6 @@ describe('App', () => {
it('renders 4 Tabs', () => {
const wrapper = shallow(<App {...mockedProps} />);
expect(wrapper.find(Tabs.TabPane)).toHaveLength(4);
expect(wrapper.find('[tab]')).toHaveLength(4);
});
});
@@ -17,12 +17,12 @@
* under the License.
*/
import React from 'react';
import { FormControl } from 'react-bootstrap';
import { shallow } from 'enzyme';
import * as sinon from 'sinon';
import SaveQuery from 'src/SqlLab/components/SaveQuery';
import Modal from 'src/components/Modal';
import Button from 'src/components/Button';
import { FormItem } from 'src/components/Form';
describe('SavedQuery', () => {
const mockedProps = {
@@ -52,11 +52,11 @@ describe('SavedQuery', () => {
expect(modal.find('[data-test="cancel-query"]')).toHaveLength(1);
});
it('has 2 FormControls', () => {
it('has 2 FormItem', () => {
const wrapper = shallow(<SaveQuery {...mockedProps} />);
const modal = wrapper.find(Modal);
expect(modal.find(FormControl)).toHaveLength(2);
expect(modal.find(FormItem)).toHaveLength(2);
});
// eslint-disable-next-line jest/no-disabled-tests
it.skip('has a save button if this is a new query', () => {
+1 -1
View File
@@ -21,7 +21,7 @@ import PropTypes from 'prop-types';
import { FormGroup, HelpBlock, FormControl } from 'react-bootstrap';
import { Tooltip } from 'src/components/Tooltip';
import FormLabel from 'src/components/FormLabel';
import { FormLabel } from 'src/components/Form';
import './crud.less';
const propTypes = {
@@ -18,7 +18,7 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import { ClassNames } from '@emotion/core';
import { ClassNames } from '@emotion/react';
import { styled, useTheme } from '@superset-ui/core';
import { Tooltip } from 'src/components/Tooltip';
@@ -19,7 +19,7 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import Card from 'src/common/components/Card';
import Card from 'src/components/Card';
import ProgressBar from 'src/components/ProgressBar';
import Label from 'src/components/Label';
import { t } from '@superset-ui/core';
@@ -17,12 +17,10 @@
* under the License.
*/
import React, { useState } from 'react';
import { Row, Col } from 'src/common/components';
import { FormControl, FormGroup } from 'react-bootstrap';
import { Row, Col, Input, TextArea } from 'src/common/components';
import { t, supersetTheme, styled } from '@superset-ui/core';
import Button from 'src/components/Button';
import FormLabel from 'src/components/FormLabel';
import { Form, FormItem } from 'src/components/Form';
import Modal from 'src/components/Modal';
import Icon from 'src/components/Icon';
@@ -100,12 +98,12 @@ export default function SaveQuery({
close();
};
const onLabelChange = (e: React.FormEvent<FormControl>) => {
setLabel((e.target as HTMLInputElement).value);
const onLabelChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setLabel(e.target.value);
};
const onDescriptionChange = (e: React.FormEvent<FormControl>) => {
setDescription((e.target as HTMLInputElement).value);
const onDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setDescription(e.target.value);
};
const toggleSave = () => {
@@ -113,27 +111,24 @@ export default function SaveQuery({
};
const renderModalBody = () => (
<FormGroup bsSize="small">
<Form layout="vertical">
<Row>
<Col xs={24}>
<small>
<FormLabel htmlFor="embed-height">{t('Name')}</FormLabel>
</small>
<FormControl type="text" value={label} onChange={onLabelChange} />
<FormItem label={t('Name')}>
<Input type="text" value={label} onChange={onLabelChange} />
</FormItem>
</Col>
</Row>
<br />
<Row>
<Col xs={24}>
<small>
<FormLabel htmlFor="embed-height">{t('Description')}</FormLabel>
</small>
<FormControl
rows={5}
componentClass="textarea"
value={description}
onChange={onDescriptionChange}
/>
<FormItem label={t('Description')}>
<TextArea
rows={4}
value={description}
onChange={onDescriptionChange}
/>
</FormItem>
</Col>
</Row>
{saveQueryWarning && (
@@ -149,7 +144,7 @@ export default function SaveQuery({
</div>
</>
)}
</FormGroup>
</Form>
);
return (
@@ -17,13 +17,12 @@
* under the License.
*/
import React, { FunctionComponent, useState } from 'react';
import Form, { FormProps, FormValidation } from 'react-jsonschema-form';
import { Row, Col } from 'src/common/components';
import { FormControl, FormGroup } from 'react-bootstrap';
import SchemaForm, { FormProps, FormValidation } from 'react-jsonschema-form';
import { Row, Col, Input, TextArea } from 'src/common/components';
import { t, styled } from '@superset-ui/core';
import * as chrono from 'chrono-node';
import ModalTrigger from 'src/components/ModalTrigger';
import FormLabel from 'src/components/FormLabel';
import { Form, FormItem } from 'src/components/Form';
import './ScheduleQueryButton.less';
import Button from 'src/components/Button';
@@ -139,42 +138,52 @@ const ScheduleQueryButton: FunctionComponent<ScheduleQueryButtonProps> = ({
};
const renderModalBody = () => (
<FormGroup>
<Form layout="vertical">
<StyledRow>
<Col xs={24}>
<FormLabel className="control-label" htmlFor="embed-height">
{t('Label')}
</FormLabel>
<FormControl
type="text"
placeholder={t('Label for your query')}
value={label}
onChange={(event: any) => setLabel(event.target?.value)}
/>
<FormItem label={t('Label')}>
<Input
type="text"
placeholder={t('Label for your query')}
value={label}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setLabel(event.target.value)
}
/>
</FormItem>
</Col>
</StyledRow>
<StyledRow>
<Col xs={24}>
<FormLabel className="control-label" htmlFor="embed-height">
{t('Description')}
</FormLabel>
<FormControl
componentClass="textarea"
placeholder={t('Write a description for your query')}
value={description}
onChange={(event: any) => setDescription(event.target?.value)}
/>
<FormItem label={t('Description')}>
<TextArea
rows={4}
placeholder={t('Write a description for your query')}
value={description}
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) =>
setDescription(event.target.value)
}
/>
</FormItem>
</Col>
</StyledRow>
<Row>
<Col xs={24}>
<div className="json-schema">
<Form
<SchemaForm
schema={getJSONSchema()}
uiSchema={getUISchema}
onSubmit={onScheduleSubmit}
validate={getValidator()}
/>
>
<Button
buttonStyle="primary"
htmlType="submit"
css={{ float: 'right' }}
>
Submit
</Button>
</SchemaForm>
</div>
</Col>
</Row>
@@ -185,7 +194,7 @@ const ScheduleQueryButton: FunctionComponent<ScheduleQueryButtonProps> = ({
</Col>
</Row>
)}
</FormGroup>
</Form>
);
return (
@@ -168,6 +168,7 @@ export default function SouthPane({
onChange={switchTab}
id={shortid.generate()}
fullWidth={false}
animated={false}
>
<Tabs.TabPane tab={t('Results')} key="Results">
{renderResults()}
@@ -18,8 +18,8 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import Card from 'src/common/components/Card';
import Collapse from 'src/components/Collapse';
import Card from 'src/components/Card';
import ButtonGroup from 'src/components/ButtonGroup';
import shortid from 'shortid';
import { t, styled } from '@superset-ui/core';
@@ -99,7 +99,7 @@ class TableElement extends React.PureComponent {
let partitionQuery;
let partitionClipBoard;
if (table.partitions.partitionQuery) {
({ partitionQuery } = table.partitions.partitionQuery);
({ partitionQuery } = table.partitions);
const tt = t('Copy partition query to clipboard');
partitionClipBoard = (
<CopyToClipboard
@@ -204,7 +204,7 @@ class TableElement extends React.PureComponent {
>
<Tooltip
id="copy-to-clipboard-tooltip"
placement="top"
placement="topLeft"
style={{ cursor: 'pointer' }}
title={table.name}
trigger={['hover']}
@@ -334,6 +334,7 @@ export default function sqlLabReducer(state = {}, action) {
results: action.results,
rows: action?.results?.data?.length,
state: 'success',
limitingFactor: action?.results?.query?.limitingFactor,
tempSchema: action?.results?.query?.tempSchema,
tempTable: action?.results?.query?.tempTable,
errorMessage: null,
+1
View File
@@ -65,4 +65,5 @@ export type Query = {
templateParams: any;
rows: number;
queryLimit: number;
limitingFactor: string;
};

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 186 KiB

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

+1 -7
View File
@@ -121,13 +121,6 @@ class Chart extends React.PureComponent {
}
runQuery() {
if (
this.props.dashboardId && // we on dashboard screen
isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) &&
!this.props.isFiltersInitialized
) {
return;
}
if (this.props.chartId > 0 && isFeatureEnabled(FeatureFlag.CLIENT_CACHE)) {
// Load saved chart with a GET request
this.props.actions.getSavedChart(
@@ -221,6 +214,7 @@ class Chart extends React.PureComponent {
showMessage={false}
>
<Styles
data-ui-anchor="chart"
className="chart-container"
data-test="chart-container"
height={height}
@@ -37,9 +37,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(
({ nativeFilters }) => ({
isFiltersInitialized: nativeFilters?.isInitialized,
}),
mapDispatchToProps,
)(Chart);
export default connect(null, mapDispatchToProps)(Chart);
+15 -2
View File
@@ -248,6 +248,7 @@ export function runAnnotationQuery(
formData = null,
key,
isDashboardRequest = false,
force = false,
) {
return function (dispatch, getState) {
const sliceKey = key || Object.keys(getState().charts)[0];
@@ -286,7 +287,12 @@ export function runAnnotationQuery(
}
const isNative = annotation.sourceType === ANNOTATION_SOURCE_TYPES.NATIVE;
const url = getAnnotationJsonUrl(annotation.value, sliceFormData, isNative);
const url = getAnnotationJsonUrl(
annotation.value,
sliceFormData,
isNative,
force,
);
const controller = new AbortController();
const { signal } = controller;
@@ -462,7 +468,14 @@ export function exploreJSON(
dispatch(updateQueryFormData(formData, key)),
...annotationLayers.map(x =>
dispatch(
runAnnotationQuery(x, timeout, formData, key, isDashboardRequest),
runAnnotationQuery(
x,
timeout,
formData,
key,
isDashboardRequest,
force,
),
),
),
]);
@@ -1,210 +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 React, { useState, useRef, useCallback } from 'react';
import { action } from '@storybook/addon-actions';
import { withKnobs, boolean } from '@storybook/addon-knobs';
import { CronPicker, CronError } from 'src/components/CronPicker';
import Modal from 'src/components/Modal';
import InfoTooltip from 'src/components/InfoTooltip';
import { Dropdown } from 'src/components/Dropdown';
import Tabs, { EditableTabs } from 'src/components/Tabs';
import { Menu, Input, Divider } from '.';
export default {
title: 'Common components',
decorators: [withKnobs],
};
export const StyledModal = () => (
<Modal
disablePrimaryButton={false}
onHandledPrimaryAction={action('Primary action')}
primaryButtonName="Danger"
primaryButtonType="danger"
show
onHide={action('hidden')}
title="I'm a modal!"
>
<div>hi!</div>
</Modal>
);
export const StyledTabs = () => (
<Tabs
defaultActiveKey="1"
centered={boolean('Center tabs', false)}
fullWidth={boolean('Full width', true)}
>
<Tabs.TabPane
tab="Tab 1"
key="1"
disabled={boolean('Tab 1 disabled', false)}
>
Tab 1 Content!
</Tabs.TabPane>
<Tabs.TabPane
tab="Tab 2"
key="2"
disabled={boolean('Tab 2 disabled', false)}
>
Tab 2 Content!
</Tabs.TabPane>
</Tabs>
);
export const StyledEditableTabs = () => (
<EditableTabs
defaultActiveKey="1"
centered={boolean('Center tabs', false)}
fullWidth={boolean('Full width', true)}
>
<Tabs.TabPane
tab="Tab 1"
key="1"
disabled={boolean('Tab 1 disabled', false)}
>
Tab 1 Content!
</Tabs.TabPane>
<Tabs.TabPane
tab="Tab 2"
key="2"
disabled={boolean('Tab 2 disabled', false)}
>
Tab 2 Content!
</Tabs.TabPane>
</EditableTabs>
);
export const TabsWithDropdownMenu = () => (
<EditableTabs
defaultActiveKey="1"
centered={boolean('Center tabs', false)}
fullWidth={boolean('Full width', true)}
>
<Tabs.TabPane
tab={
<>
<Dropdown
overlay={
<Menu>
<Menu.Item key="1">Item 1</Menu.Item>
<Menu.Item key="2">Item 2</Menu.Item>
</Menu>
}
/>
Tab with dropdown menu
</>
}
key="1"
disabled={boolean('Tab 1 disabled', false)}
>
Tab 1 Content!
</Tabs.TabPane>
</EditableTabs>
);
export const StyledInfoTooltip = (args: any) => {
const styles = {
padding: '100px 0 0 200px',
};
return (
<div style={styles}>
<InfoTooltip tooltip="This is the text that will display!" {...args} />
</div>
);
};
StyledInfoTooltip.args = {
placement: 'right',
trigger: 'hover',
};
StyledInfoTooltip.argTypes = {
placement: {
name: 'Placement',
control: {
type: 'select',
options: [
'bottom',
'left',
'right',
'top',
'topLeft',
'topRight',
'bottomLeft',
'bottomRight',
'leftTop',
'leftBottom',
'rightTop',
'rightBottom',
],
},
},
trigger: {
name: 'Trigger',
control: {
type: 'select',
options: ['hover', 'click'],
},
},
};
export function StyledCronPicker() {
// @ts-ignore
const inputRef = useRef<Input>(null);
const defaultValue = '30 5 * * 1,6';
const [value, setValue] = useState(defaultValue);
const customSetValue = useCallback(
(newValue: string) => {
setValue(newValue);
inputRef.current?.setValue(newValue);
},
[inputRef],
);
const [error, onError] = useState<CronError>();
return (
<div>
<Input
ref={inputRef}
onBlur={event => {
setValue(event.target.value);
}}
onPressEnter={() => {
setValue(inputRef.current?.input.value || '');
}}
/>
<Divider />
<CronPicker
clearButton={false}
value={value}
setValue={customSetValue}
onError={onError}
/>
<p style={{ marginTop: 20 }}>
Error: {error ? error.description : 'undefined'}
</p>
</div>
);
}
@@ -18,7 +18,13 @@
*/
import React from 'react';
import { styled } from '@superset-ui/core';
import { Dropdown, Menu as AntdMenu, Input as AntdInput, Skeleton } from 'antd';
import {
Dropdown,
Menu as AntdMenu,
Input as AntdInput,
InputNumber as AntdInputNumber,
Skeleton,
} from 'antd';
import { DropDownProps } from 'antd/lib/dropdown';
/*
Antd is re-exported from here so we can override components with Emotion as needed.
@@ -36,7 +42,6 @@ export {
Dropdown,
Form,
Empty,
InputNumber,
Modal,
Typography,
Tree,
@@ -61,7 +66,7 @@ export { default as List, ListItemProps } from 'antd/lib/list';
export { default as Collapse } from 'src/components/Collapse';
export { default as Badge } from 'src/components/Badge';
export { default as Card } from './Card';
export { default as Card } from 'src/components/Card';
export { default as Progress } from 'src/components/ProgressBar';
export const MenuItem = styled(AntdMenu.Item)`
@@ -72,6 +77,29 @@ export const MenuItem = styled(AntdMenu.Item)`
&.ant-menu-item {
height: ${({ theme }) => theme.gridUnit * 7}px;
line-height: ${({ theme }) => theme.gridUnit * 7}px;
a {
border-bottom: none;
transition: background-color ${({ theme }) => theme.transitionTiming}s;
&:after {
content: '';
position: absolute;
bottom: -3px;
left: 50%;
width: 0;
height: 3px;
opacity: 0;
transform: translateX(-50%);
transition: all ${({ theme }) => theme.transitionTiming}s;
background-color: ${({ theme }) => theme.colors.primary.base};
}
&:focus {
border-bottom: none;
background-color: transparent;
@media (max-width: 767px) {
background-color: ${({ theme }) => theme.colors.primary.light5};
}
}
}
}
&.ant-menu-item,
@@ -84,16 +112,122 @@ export const MenuItem = styled(AntdMenu.Item)`
}
`;
export const StyledNav = styled(AntdMenu)`
line-height: 51px;
border: none;
& > .ant-menu-item,
& > .ant-menu-submenu {
vertical-align: inherit;
&:hover {
color: ${({ theme }) => theme.colors.grayscale.dark1};
}
}
&:not(.ant-menu-dark) > .ant-menu-submenu,
&:not(.ant-menu-dark) > .ant-menu-item {
&:hover {
border-bottom: none;
}
}
@media (min-width: 767px) {
&:not(.ant-menu-dark) > .ant-menu-submenu,
&:not(.ant-menu-dark) > .ant-menu-item {
margin: 0px;
}
}
& > .ant-menu-item > a {
padding: ${({ theme }) => theme.gridUnit * 4}px;
}
`;
export const StyledSubMenu = styled(AntdMenu.SubMenu)`
color: ${({ theme }) => theme.colors.grayscale.dark1};
border-bottom: none;
.ant-menu-submenu-open,
.ant-menu-submenu-active {
background-color: ${({ theme }) => theme.colors.primary.light5};
.ant-menu-submenu-title {
color: ${({ theme }) => theme.colors.grayscale.dark1};
background-color: ${({ theme }) => theme.colors.primary.light5};
border-bottom: none;
margin: 0;
&:after {
opacity: 1;
width: calc(100% - 1);
}
}
}
.ant-menu-submenu-title {
position: relative;
top: ${({ theme }) => -theme.gridUnit - 3}px;
&:after {
content: '';
position: absolute;
bottom: -3px;
left: 50%;
width: 0;
height: 3px;
opacity: 0;
transform: translateX(-50%);
transition: all ${({ theme }) => theme.transitionTiming}s;
background-color: ${({ theme }) => theme.colors.primary.base};
}
}
.ant-menu-submenu-arrow {
top: 67%;
}
& > .ant-menu-submenu-title {
padding: 0 ${({ theme }) => theme.gridUnit * 6}px 0
${({ theme }) => theme.gridUnit * 3}px !important;
svg {
position: absolute;
top: ${({ theme }) => theme.gridUnit * 4 + 7}px;
right: ${({ theme }) => theme.gridUnit}px;
width: ${({ theme }) => theme.gridUnit * 6}px;
}
& > span {
position: relative;
top: 7px;
}
&:hover {
color: ${({ theme }) => theme.colors.primary.base};
}
}
`;
export declare type MenuMode =
| 'vertical'
| 'vertical-left'
| 'vertical-right'
| 'horizontal'
| 'inline';
export const Menu = Object.assign(AntdMenu, {
Item: MenuItem,
});
export const MainNav = Object.assign(StyledNav, {
Item: MenuItem,
SubMenu: StyledSubMenu,
Divider: AntdMenu.Divider,
ItemGroup: AntdMenu.ItemGroup,
});
export const Input = styled(AntdInput)`
&[type='text'],
&[type='textarea'] {
border: 1px solid ${({ theme }) => theme.colors.secondary.light3};
border-radius: ${({ theme }) => theme.borderRadius}px;
}
border: 1px solid ${({ theme }) => theme.colors.secondary.light3};
border-radius: ${({ theme }) => theme.borderRadius}px;
`;
export const InputNumber = styled(AntdInputNumber)`
border: 1px solid ${({ theme }) => theme.colors.secondary.light3};
border-radius: ${({ theme }) => theme.borderRadius}px;
`;
export const TextArea = styled(AntdInput.TextArea)`
border: 1px solid ${({ theme }) => theme.colors.secondary.light3};
border-radius: ${({ theme }) => theme.borderRadius}px;
`;
export const NoAnimationDropdown = (props: DropDownProps) => (
@@ -74,10 +74,16 @@ AlertGallery.story = {
},
};
export const InteractiveAlert = (args: AlertProps) => <Alert {...args} />;
export const InteractiveAlert = (args: AlertProps) => (
<>
<Alert {...args} />
Some content to test the `roomBelow` prop
</>
);
InteractiveAlert.args = {
closable: true,
roomBelow: false,
type: 'info',
message: smallText,
description: bigText,
@@ -17,31 +17,37 @@
* under the License.
*/
import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import Alert, { AlertProps } from 'src/components/Alert';
type AlertType = Pick<AlertProps, 'type'>;
type AlertTypeValue = AlertType[keyof AlertType];
test('renders with default props', () => {
test('renders with default props', async () => {
render(<Alert message="Message" />);
expect(screen.getByRole('alert')).toHaveTextContent('Message');
expect(screen.queryByLabelText(`info icon`)).toBeInTheDocument();
expect(screen.queryByLabelText('close icon')).toBeInTheDocument();
expect(await screen.findByLabelText(`info icon`)).toBeInTheDocument();
expect(await screen.findByLabelText('close icon')).toBeInTheDocument();
});
test('renders each type', () => {
test('renders each type', async () => {
const types: AlertTypeValue[] = ['info', 'error', 'warning', 'success'];
types.forEach(type => {
for (let i = 0; i < types.length; i += 1) {
const type = types[i];
render(<Alert type={type} message="Message" />);
expect(screen.queryByLabelText(`${type} icon`)).toBeInTheDocument();
});
// eslint-disable-next-line no-await-in-loop
expect(await screen.findByLabelText(`${type} icon`)).toBeInTheDocument();
}
});
test('renders without close button', () => {
test('renders without close button', async () => {
render(<Alert message="Message" closable={false} />);
expect(screen.queryByLabelText('close icon')).not.toBeInTheDocument();
await waitFor(() => {
expect(screen.queryByLabelText('close icon')).not.toBeInTheDocument();
});
});
test('disappear when closed', () => {
@@ -50,10 +56,12 @@ test('disappear when closed', () => {
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
test('renders without icon', () => {
test('renders without icon', async () => {
const type = 'info';
render(<Alert type={type} message="Message" showIcon={false} />);
expect(screen.queryByLabelText(`${type} icon`)).not.toBeInTheDocument();
await waitFor(() => {
expect(screen.queryByLabelText(`${type} icon`)).not.toBeInTheDocument();
});
});
test('renders message', () => {
@@ -22,9 +22,12 @@ import {
AlertProps as AntdAlertProps,
} from 'src/common/components';
import { useTheme } from '@superset-ui/core';
import Icon, { IconName } from 'src/components/Icon';
import Icon from 'src/components/Icon';
import Icons from 'src/components/Icons';
export type AlertProps = PropsWithChildren<AntdAlertProps>;
export type AlertProps = PropsWithChildren<
AntdAlertProps & { roomBelow?: boolean }
>;
export default function Alert(props: AlertProps) {
const {
@@ -32,39 +35,41 @@ export default function Alert(props: AlertProps) {
description,
showIcon = true,
closable = true,
roomBelow = false,
children,
} = props;
const theme = useTheme();
const { colors, typography } = theme;
const { colors, typography, gridUnit } = theme;
const { alert, error, info, success } = colors;
let baseColor = info;
let iconName: IconName = 'info-solid';
let AlertIcon = Icons.InfoSolid;
if (type === 'error') {
baseColor = error;
iconName = 'error-solid';
AlertIcon = Icons.ErrorSolid;
} else if (type === 'warning') {
baseColor = alert;
iconName = 'alert-solid';
AlertIcon = Icons.AlertSolid;
} else if (type === 'success') {
baseColor = success;
iconName = 'circle-check-solid';
AlertIcon = Icons.CircleCheckSolid;
}
return (
<AntdAlert
role="alert"
showIcon={showIcon}
icon={<Icon name={iconName} aria-label={`${type} icon`} />}
icon={<AlertIcon aria-label={`${type} icon`} />}
closeText={closable && <Icon name="x-small" aria-label="close icon" />}
css={{
padding: '6px 10px',
marginBottom: roomBelow ? gridUnit * 4 : 0,
padding: `${gridUnit * 2}px ${gridUnit * 3}px`,
alignItems: 'flex-start',
border: 0,
backgroundColor: baseColor.light2,
'& .ant-alert-icon': {
marginRight: 10,
marginRight: gridUnit * 2,
},
'& .ant-alert-message': {
color: baseColor.dark2,
@@ -16,32 +16,43 @@
* specific language governing permissions and limitations
* under the License.
*/
// import { styled } from '@superset-ui/core';
import React, { ReactNode } from 'react';
import { ControlLabel } from 'react-bootstrap';
import React from 'react';
import Card, { CardProps } from '.';
export type FormLabelProps = {
children: ReactNode;
htmlFor?: string;
required?: boolean;
className?: string;
export default {
title: 'Card',
component: Card,
};
export default function FormLabel({
children,
htmlFor,
required = false,
}: FormLabelProps) {
return (
<>
<ControlLabel htmlFor={htmlFor}>
{children}
{required && (
<span className="text-danger m-l-4">
<strong>*</strong>
</span>
)}
</ControlLabel>
</>
);
}
export const InteractiveCard = (args: CardProps) => <Card {...args} />;
InteractiveCard.args = {
padded: true,
title: 'Card title',
children: 'Card content',
bordered: true,
loading: false,
hoverable: false,
};
InteractiveCard.argTypes = {
onClick: {
table: {
disable: true,
},
action: 'onClick',
},
theme: {
table: {
disable: true,
},
},
};
InteractiveCard.story = {
parameters: {
knobs: {
disable: true,
},
},
};
@@ -17,23 +17,24 @@
* under the License.
*/
import React from 'react';
import { styled } from '@superset-ui/core';
import { SupersetTheme } from '@superset-ui/core';
import AntdCard, { CardProps as AntdCardProps } from 'antd/lib/card';
interface CardProps extends AntdCardProps {
export interface CardProps extends AntdCardProps {
padded?: boolean;
}
const Card = styled(({ padded, ...props }: CardProps) => (
<AntdCard {...props} />
))`
background-color: ${({ theme }) => theme.colors.grayscale.light4};
border-radius: ${({ theme }) => theme.borderRadius}px;
.ant-card-body {
padding: ${({ padded, theme }) =>
padded ? theme.gridUnit * 4 : theme.gridUnit}px;
}
`;
const Card = ({ padded, ...props }: CardProps) => (
<AntdCard
{...props}
css={(theme: SupersetTheme) => ({
backgroundColor: theme.colors.grayscale.light4,
borderRadius: theme.borderRadius,
'.ant-card-body': {
padding: padded ? theme.gridUnit * 4 : theme.gridUnit,
},
})}
/>
);
export default Card;
@@ -45,9 +45,7 @@ describe('Checkbox', () => {
const shallowWrapper = shallow(
<Checkbox style={{}} checked={false} onChange={() => true} />,
);
expect(
shallowWrapper.dive().dive().dive().find(CheckboxUnchecked),
).toExist();
expect(shallowWrapper.dive().find(CheckboxUnchecked)).toExist();
});
});
@@ -56,9 +54,7 @@ describe('Checkbox', () => {
const shallowWrapper = shallow(
<Checkbox style={{}} checked onChange={() => true} />,
);
expect(
shallowWrapper.dive().dive().dive().find(CheckboxChecked),
).toExist();
expect(shallowWrapper.dive().find(CheckboxChecked)).toExist();
});
});
@@ -0,0 +1,88 @@
/**
* 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 React, { useState, useRef, useCallback } from 'react';
import { Input, Divider } from 'src/common/components';
import { CronPicker, CronError, CronProps } from '.';
export default {
title: 'CronPicker',
component: CronPicker,
};
export const InteractiveCronPicker = (props: CronProps) => {
// @ts-ignore
const inputRef = useRef<Input>(null);
const [value, setValue] = useState(props.value);
const customSetValue = useCallback(
(newValue: string) => {
setValue(newValue);
inputRef.current?.setValue(newValue);
},
[inputRef],
);
const [error, onError] = useState<CronError>();
return (
<div>
<Input
ref={inputRef}
onBlur={event => {
setValue(event.target.value);
}}
onChange={e => setValue(e.target.value || '')}
/>
<Divider />
<CronPicker
{...props}
value={value}
setValue={customSetValue}
onError={onError}
/>
{error && <p style={{ marginTop: 20 }}>Error: {error.description}</p>}
</div>
);
};
InteractiveCronPicker.args = {
clearButton: false,
disabled: false,
readOnly: false,
};
InteractiveCronPicker.argTypes = {
value: {
defaultValue: '30 5 * * *',
table: {
disable: true,
},
},
theme: {
table: {
disable: true,
},
},
};
InteractiveCronPicker.story = {
parameters: {
knobs: {
disable: true,
},
},
};
@@ -20,7 +20,7 @@ import { t, styled } from '@superset-ui/core';
import React, { useState } from 'react';
import { FormGroup, FormControl, FormControlProps } from 'react-bootstrap';
import Modal from 'src/components/Modal';
import FormLabel from 'src/components/FormLabel';
import { FormLabel } from 'src/components/Form';
const StyleFormGroup = styled(FormGroup)`
padding-top: 8px;
@@ -17,18 +17,18 @@
* under the License.
*/
import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import NewMenu, { dropdownItems } from './NewMenu';
import AntDForm, { FormProps } from 'antd/lib/form';
import { styled } from '@superset-ui/core';
test('should render', () => {
const { container } = render(<NewMenu />);
expect(container).toBeInTheDocument();
});
const StyledForm = styled(AntDForm)`
&.ant-form label {
font-size: ${({ theme }) => theme.typography.sizes.s}px;
}
.ant-form-item {
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
}
`;
test('should render the dropdown items', () => {
render(<NewMenu />);
dropdownItems.forEach(item => {
expect(screen.getByText(item.label)).toHaveAttribute('href', item.url);
expect(screen.getByTestId(`menu-item-${item.label}`)).toBeInTheDocument();
});
});
export default function Form(props: FormProps) {
return <StyledForm {...props} />;
}
@@ -16,27 +16,35 @@
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable no-unused-expressions */
import React from 'react';
import { shallow } from 'enzyme';
import Form, { FormItemProps } from 'antd/lib/form';
import { styled } from '@superset-ui/core';
import { AGGREGATES } from 'src/explore/constants';
import AdhocMetricStaticOption from 'src/explore/components/controls/MetricControl/AdhocMetricStaticOption';
import AdhocMetric, {
EXPRESSION_TYPES,
} from 'src/explore/components/controls/MetricControl/AdhocMetric';
const StyledItem = styled(Form.Item)`
${({ theme }) => `
.ant-form-item-label {
padding-bottom: ${theme.gridUnit}px;
& > label {
text-transform: uppercase;
font-size: ${theme.typography.sizes.s}px;
color: ${theme.colors.grayscale.base};
const sumValueAdhocMetric = new AdhocMetric({
expressionType: EXPRESSION_TYPES.SIMPLE,
column: { type: 'VARCHAR(255)', column_name: 'source' },
aggregate: AGGREGATES.SUM,
});
&.ant-form-item-required:not(.ant-form-item-required-mark-optional) {
&::before {
display: none;
}
&::after {
display: inline-block;
color: ${theme.colors.error.base};
font-size: ${theme.typography.sizes.m}px;
content: '*';
}
}
}
}
`}
`;
describe('AdhocMetricStaticOption', () => {
it('renders the adhoc metrics label', () => {
const wrapper = shallow(
<AdhocMetricStaticOption adhocMetric={sumValueAdhocMetric} />,
);
expect(wrapper.text()).toBe('SUM(source)');
});
});
export default function FormItem(props: FormItemProps) {
return <StyledItem {...props} />;
}
@@ -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.
*/
import React, { ReactNode } from 'react';
import { styled } from '@superset-ui/core';
export type FormLabelProps = {
children: ReactNode;
htmlFor?: string;
required?: boolean;
className?: string;
};
const Label = styled.label`
text-transform: uppercase;
font-size: ${({ theme }) => theme.typography.sizes.s}px;
color: ${({ theme }) => theme.colors.grayscale.base};
margin-bottom: ${({ theme }) => theme.gridUnit}px;
`;
const RequiredLabel = styled.label`
text-transform: uppercase;
font-size: ${({ theme }) => theme.typography.sizes.s}px;
color: ${({ theme }) => theme.colors.grayscale.base};
margin-bottom: ${({ theme }) => theme.gridUnit}px;
&::after {
display: inline-block;
color: ${({ theme }) => theme.colors.error.base};
font-size: ${({ theme }) => theme.typography.sizes.m}px;
content: '*';
}
`;
export default function FormLabel({
children,
htmlFor,
required = false,
className,
}: FormLabelProps) {
const StyledLabel = required ? RequiredLabel : Label;
return (
<StyledLabel htmlFor={htmlFor} className={className}>
{children}
</StyledLabel>
);
}
@@ -16,5 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
export default 'SvgrURL';
export const ReactComponent = 'svg';
import Form from './Form';
import FormItem from './FormItem';
import FormLabel from './FormLabel';
export { Form, FormItem, FormLabel };
@@ -1,52 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import { FormControl } from 'react-bootstrap';
import FormLabel from './index';
const mockedProps = {
children: 'Form label',
required: false,
htmlFor: 'form-control',
};
test('should render', () => {
const { container } = render(<FormLabel {...mockedProps} />);
expect(container).toBeInTheDocument();
});
test('should render a Label', () => {
render(
<>
<FormLabel {...mockedProps} />
<FormControl id="form-control" />
</>,
);
expect(screen.getByLabelText('Form label')).toBeInTheDocument();
});
test('should be shown as required', () => {
const requiredProps = {
...mockedProps,
required: true,
};
render(<FormLabel {...requiredProps} />);
expect(screen.getByText('*').parentNode).toBeInTheDocument();
});
@@ -19,12 +19,14 @@
import React from 'react';
import * as AntdIcons from '@ant-design/icons/lib/icons';
import Icon from './Icon';
import { StyledIcon } from './Icon';
import IconType from './IconType';
const AntdEnhancedIcons = Object.keys(AntdIcons)
.map(k => ({
[k]: (props: IconType) => <Icon component={AntdIcons[k]} {...props} />,
[k]: (props: IconType) => (
<StyledIcon component={AntdIcons[k]} {...props} />
),
}))
.reduce((l, r) => ({ ...l, ...r }));
+45 -19
View File
@@ -17,34 +17,60 @@
* under the License.
*/
import React from 'react';
import React, { useEffect, useRef, useState } from 'react';
import AntdIcon from '@ant-design/icons';
import { styled } from '@superset-ui/core';
import { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon';
import { ReactComponent as TransparentIcon } from 'images/icons/transparent.svg';
import IconType from './IconType';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const EnhancedIcon = ({ iconColor, iconSize, ...rest }: IconType) => (
<AntdIcon viewBox={rest.viewBox || '0 0 24 24'} {...rest} />
const AntdIconComponent = ({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
iconColor,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
iconSize,
viewBox,
...rest
}: Omit<IconType, 'ref' | 'css'>) => (
<AntdIcon viewBox={viewBox || '0 0 24 24'} {...rest} />
);
const Icon = styled(EnhancedIcon)<IconType>`
export const StyledIcon = styled(AntdIconComponent)<IconType>`
${({ iconColor }) => iconColor && `color: ${iconColor};`};
font-size: ${({ iconSize, theme }) =>
iconSize ? `${theme.typography.sizes[iconSize]}px` : '24px'};
iconSize
? `${theme.typography.sizes[iconSize] || theme.typography.sizes.m}px`
: '24px'};
`;
export const renderIcon = (
SVGComponent:
| React.ComponentClass<
CustomIconComponentProps | React.SVGProps<SVGSVGElement>,
any
>
| React.FunctionComponent<
CustomIconComponentProps | React.SVGProps<SVGSVGElement>
>
| undefined,
props: IconType,
) => <Icon component={SVGComponent} {...props} />;
interface IconProps extends IconType {
fileName: string;
}
export const Icon = (props: IconProps) => {
const { fileName, ...iconProps } = props;
const [, setLoaded] = useState(false);
const ImportedSVG = useRef<React.FC<React.SVGProps<SVGSVGElement>>>();
const name = fileName.replace('_', '-');
useEffect(() => {
async function importIcon(): Promise<void> {
ImportedSVG.current = (
await import(
`!!@svgr/webpack?-svgo,+titleProp,+ref!images/icons/${fileName}.svg`
)
).default;
setLoaded(true);
}
importIcon();
}, [fileName, ImportedSVG]);
return (
<StyledIcon
component={ImportedSVG.current || TransparentIcon}
aria-label={name}
{...iconProps}
/>
);
};
export default Icon;
@@ -16,12 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import { IconComponentProps } from '@ant-design/icons/lib/components/Icon';
import AntdIcon from '@ant-design/icons';
type AntdIconType = typeof AntdIcon.defaultProps;
type AntdIconType = IconComponentProps;
type IconType = AntdIconType & {
iconColor?: string;
twoToneColor?: string;
iconSize?: 's' | 'm' | 'l' | 'xl' | 'xxl';
};
@@ -78,6 +78,11 @@ InteractiveIcons.argTypes = {
defaultValue: null,
control: { type: 'select', options: palette },
},
// @TODO twoToneColor is being ignored
twoToneColor: {
defaultValue: null,
control: { type: 'select', options: palette },
},
theme: {
table: {
disable: true,
@@ -1,284 +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 { ReactComponent as AlertIcon } from 'images/icons/alert.svg';
import { ReactComponent as AlertSolidIcon } from 'images/icons/alert_solid.svg';
import { ReactComponent as AlertSolidSmallIcon } from 'images/icons/alert_solid_small.svg';
import { ReactComponent as BinocularsIcon } from 'images/icons/binoculars.svg';
import { ReactComponent as BoltIcon } from 'images/icons/bolt.svg';
import { ReactComponent as BoltSmallIcon } from 'images/icons/bolt_small.svg';
import { ReactComponent as BoltSmallRunIcon } from 'images/icons/bolt_small_run.svg';
import { ReactComponent as CalendarIcon } from 'images/icons/calendar.svg';
import { ReactComponent as CancelIcon } from 'images/icons/cancel.svg';
import { ReactComponent as CancelSolidIcon } from 'images/icons/cancel_solid.svg';
import { ReactComponent as CancelXIcon } from 'images/icons/cancel-x.svg';
import { ReactComponent as CardViewIcon } from 'images/icons/card_view.svg';
import { ReactComponent as CardsIcon } from 'images/icons/cards.svg';
import { ReactComponent as CardsLockedIcon } from 'images/icons/cards_locked.svg';
import { ReactComponent as CaretDownIcon } from 'images/icons/caret_down.svg';
import { ReactComponent as CaretLeftIcon } from 'images/icons/caret_left.svg';
import { ReactComponent as CaretRightIcon } from 'images/icons/caret_right.svg';
import { ReactComponent as CaretUpIcon } from 'images/icons/caret_up.svg';
import { ReactComponent as CertifiedIcon } from 'images/icons/certified.svg';
import { ReactComponent as CheckIcon } from 'images/icons/check.svg';
import { ReactComponent as CheckboxHalfIcon } from 'images/icons/checkbox-half.svg';
import { ReactComponent as CheckboxOffIcon } from 'images/icons/checkbox-off.svg';
import { ReactComponent as CheckboxOnIcon } from 'images/icons/checkbox-on.svg';
import { ReactComponent as CircleCheckIcon } from 'images/icons/circle_check.svg';
import { ReactComponent as CircleCheckSolidIcon } from 'images/icons/circle_check_solid.svg';
import { ReactComponent as CircleIcon } from 'images/icons/circle.svg';
import { ReactComponent as ClockIcon } from 'images/icons/clock.svg';
import { ReactComponent as CloseIcon } from 'images/icons/close.svg';
import { ReactComponent as CodeIcon } from 'images/icons/code.svg';
import { ReactComponent as CogIcon } from 'images/icons/cog.svg';
import { ReactComponent as CollapseIcon } from 'images/icons/collapse.svg';
import { ReactComponent as ColorPaletteIcon } from 'images/icons/color_palette.svg';
import { ReactComponent as ComponentsIcon } from 'images/icons/components.svg';
import { ReactComponent as CopyIcon } from 'images/icons/copy.svg';
import { ReactComponent as CursorTargeIcon } from 'images/icons/cursor_target.svg';
import { ReactComponent as DatabaseIcon } from 'images/icons/database.svg';
import { ReactComponent as DatasetPhysicalIcon } from 'images/icons/dataset_physical.svg';
import { ReactComponent as DatasetVirtualGreyscaleIcon } from 'images/icons/dataset_virtual_greyscale.svg';
import { ReactComponent as DatasetVirtualIcon } from 'images/icons/dataset_virtual.svg';
import { ReactComponent as DownloadIcon } from 'images/icons/download.svg';
import { ReactComponent as EditAltIcon } from 'images/icons/edit_alt.svg';
import { ReactComponent as EditIcon } from 'images/icons/edit.svg';
import { ReactComponent as EmailIcon } from 'images/icons/email.svg';
import { ReactComponent as ErrorIcon } from 'images/icons/error.svg';
import { ReactComponent as ErrorSolidIcon } from 'images/icons/error_solid.svg';
import { ReactComponent as ErrorSolidSmallIcon } from 'images/icons/error_solid_small.svg';
import { ReactComponent as ExclamationIcon } from 'images/icons/exclamation.svg';
import { ReactComponent as ExpandIcon } from 'images/icons/expand.svg';
import { ReactComponent as EyeIcon } from 'images/icons/eye.svg';
import { ReactComponent as EyeSlashIcon } from 'images/icons/eye_slash.svg';
import { ReactComponent as FavoriteSelectedIcon } from 'images/icons/favorite-selected.svg';
import { ReactComponent as FavoriteSmallSelectedIcon } from 'images/icons/favorite_small_selected.svg';
import { ReactComponent as FavoriteUnselectedIcon } from 'images/icons/favorite-unselected.svg';
import { ReactComponent as FieldABCIcon } from 'images/icons/field_abc.svg';
import { ReactComponent as FieldBooleanIcon } from 'images/icons/field_boolean.svg';
import { ReactComponent as FieldDateIcon } from 'images/icons/field_date.svg';
import { ReactComponent as FieldDerivedIcon } from 'images/icons/field_derived.svg';
import { ReactComponent as FieldNumIcon } from 'images/icons/field_num.svg';
import { ReactComponent as FieldStructIcon } from 'images/icons/field_struct.svg';
import { ReactComponent as FileIcon } from 'images/icons/file.svg';
import { ReactComponent as FilterIcon } from 'images/icons/filter.svg';
import { ReactComponent as FilterSmallIcon } from 'images/icons/filter_small.svg';
import { ReactComponent as FolderIcon } from 'images/icons/folder.svg';
import { ReactComponent as FullIcon } from 'images/icons/full.svg';
import { ReactComponent as FunctionIcon } from 'images/icons/function_x.svg';
import { ReactComponent as GearIcon } from 'images/icons/gear.svg';
import { ReactComponent as GridIcon } from 'images/icons/grid.svg';
import { ReactComponent as ImageIcon } from 'images/icons/image.svg';
import { ReactComponent as ImportIcon } from 'images/icons/import.svg';
import { ReactComponent as InfoIcon } from 'images/icons/info.svg';
import { ReactComponent as InfoSolidIcon } from 'images/icons/info-solid.svg';
import { ReactComponent as InfoSolidSmallIcon } from 'images/icons/info_solid_small.svg';
import { ReactComponent as JoinIcon } from 'images/icons/join.svg';
import { ReactComponent as KeyboardIcon } from 'images/icons/keyboard.svg';
import { ReactComponent as LayersIcon } from 'images/icons/layers.svg';
import { ReactComponent as LightbulbIcon } from 'images/icons/lightbulb.svg';
import { ReactComponent as LinkIcon } from 'images/icons/link.svg';
import { ReactComponent as ListIcon } from 'images/icons/list.svg';
import { ReactComponent as ListViewIcon } from 'images/icons/list_view.svg';
import { ReactComponent as LocationIcon } from 'images/icons/location.svg';
import { ReactComponent as LockLockedIcon } from 'images/icons/lock_locked.svg';
import { ReactComponent as LockUnlockedIcon } from 'images/icons/lock_unlocked.svg';
import { ReactComponent as MapIcon } from 'images/icons/map.svg';
import { ReactComponent as MessageIcon } from 'images/icons/message.svg';
import { ReactComponent as MinusIcon } from 'images/icons/minus.svg';
import { ReactComponent as MinusSolidIcon } from 'images/icons/minus_solid.svg';
import { ReactComponent as MoreHorizIcon } from 'images/icons/more_horiz.svg';
import { ReactComponent as MoveIcon } from 'images/icons/move.svg';
import { ReactComponent as NavChartsIcon } from 'images/icons/nav_charts.svg';
import { ReactComponent as NavDashboardIcon } from 'images/icons/nav_dashboard.svg';
import { ReactComponent as NavDataIcon } from 'images/icons/nav_data.svg';
import { ReactComponent as NavExploreIcon } from 'images/icons/nav_explore.svg';
import { ReactComponent as NavHomeIcon } from 'images/icons/nav_home.svg';
import { ReactComponent as NavLabIcon } from 'images/icons/nav_lab.svg';
import { ReactComponent as NoteIcon } from 'images/icons/note.svg';
import { ReactComponent as OfflineIcon } from 'images/icons/offline.svg';
import { ReactComponent as PaperclipIcon } from 'images/icons/paperclip.svg';
import { ReactComponent as PlaceholderIcon } from 'images/icons/placeholder.svg';
import { ReactComponent as PlusIcon } from 'images/icons/plus.svg';
import { ReactComponent as PlusLargeIcon } from 'images/icons/plus_large.svg';
import { ReactComponent as PlusSmallIcon } from 'images/icons/plus_small.svg';
import { ReactComponent as PlusSolidIcon } from 'images/icons/plus_solid.svg';
import { ReactComponent as QueuedIcon } from 'images/icons/queued.svg';
import { ReactComponent as RefreshIcon } from 'images/icons/refresh.svg';
import { ReactComponent as RunningIcon } from 'images/icons/running.svg';
import { ReactComponent as SaveIcon } from 'images/icons/save.svg';
import { ReactComponent as SQLIcon } from 'images/icons/sql.svg';
import { ReactComponent as SearchIcon } from 'images/icons/search.svg';
import { ReactComponent as ServerIcon } from 'images/icons/server.svg';
import { ReactComponent as ShareIcon } from 'images/icons/share.svg';
import { ReactComponent as SlackIcon } from 'images/icons/slack.svg';
import { ReactComponent as SortAscIcon } from 'images/icons/sort_asc.svg';
import { ReactComponent as SortDescIcon } from 'images/icons/sort_desc.svg';
import { ReactComponent as SortIcon } from 'images/icons/sort.svg';
import { ReactComponent as TableIcon } from 'images/icons/table.svg';
import { ReactComponent as TagIcon } from 'images/icons/tag.svg';
import { ReactComponent as TrashIcon } from 'images/icons/trash.svg';
import { ReactComponent as TriangleChangeIcon } from 'images/icons/triangle_change.svg';
import { ReactComponent as TriangleDownIcon } from 'images/icons/triangle_down.svg';
import { ReactComponent as TriangleUpIcon } from 'images/icons/triangle_up.svg';
import { ReactComponent as UpLevelIcon } from 'images/icons/up-level.svg';
import { ReactComponent as UserIcon } from 'images/icons/user.svg';
import { ReactComponent as WarningIcon } from 'images/icons/warning.svg';
import { ReactComponent as WarningSolidIcon } from 'images/icons/warning_solid.svg';
import { ReactComponent as XLargeIcon } from 'images/icons/x-large.svg';
import { ReactComponent as XSmallIcon } from 'images/icons/x-small.svg';
import AntdEnhancedIcons from './AntdEnhanced';
import { renderIcon } from './Icon';
import IconType from './IconType';
export default {
...AntdEnhancedIcons,
Alert: (props: IconType) => renderIcon(AlertIcon, props),
AlertSolid: (props: IconType) => renderIcon(AlertSolidIcon, props),
AlertSolidSmall: (props: IconType) => renderIcon(AlertSolidSmallIcon, props),
Binoculars: (props: IconType) => renderIcon(BinocularsIcon, props),
Bolt: (props: IconType) => renderIcon(BoltIcon, props),
BoltSmall: (props: IconType) => renderIcon(BoltSmallIcon, props),
BoltSmallRun: (props: IconType) => renderIcon(BoltSmallRunIcon, props),
Calendar: (props: IconType) => renderIcon(CalendarIcon, props),
Cancel: (props: IconType) => renderIcon(CancelIcon, props),
CancelSolid: (props: IconType) => renderIcon(CancelSolidIcon, props),
CancelX: (props: IconType) => renderIcon(CancelXIcon, props),
CardView: (props: IconType) => renderIcon(CardViewIcon, props),
Cards: (props: IconType) => renderIcon(CardsIcon, props),
CardsLocked: (props: IconType) => renderIcon(CardsLockedIcon, props),
CaretDown: (props: IconType) => renderIcon(CaretDownIcon, props),
CaretLeft: (props: IconType) => renderIcon(CaretLeftIcon, props),
CaretRight: (props: IconType) => renderIcon(CaretRightIcon, props),
CaretUp: (props: IconType) => renderIcon(CaretUpIcon, props),
Certified: (props: IconType) => renderIcon(CertifiedIcon, props),
Check: (props: IconType) => renderIcon(CheckIcon, props),
CheckboxHalf: (props: IconType) => renderIcon(CheckboxHalfIcon, props),
CheckboxOff: (props: IconType) => renderIcon(CheckboxOffIcon, props),
CheckboxOn: (props: IconType) => renderIcon(CheckboxOnIcon, props),
CircleCheck: (props: IconType) => renderIcon(CircleCheckIcon, props),
CircleCheckSolid: (props: IconType) =>
renderIcon(CircleCheckSolidIcon, props),
Circle: (props: IconType) => renderIcon(CircleIcon, props),
Clock: (props: IconType) => renderIcon(ClockIcon, props),
Close: (props: IconType) => renderIcon(CloseIcon, props),
Code: (props: IconType) => renderIcon(CodeIcon, props),
Cog: (props: IconType) => renderIcon(CogIcon, props),
Collapse: (props: IconType) => renderIcon(CollapseIcon, props),
ColorPalette: (props: IconType) => renderIcon(ColorPaletteIcon, props),
Components: (props: IconType) => renderIcon(ComponentsIcon, props),
Copy: (props: IconType) => renderIcon(CopyIcon, props),
CursorTarget: (props: IconType) => renderIcon(CursorTargeIcon, props),
Database: (props: IconType) => renderIcon(DatabaseIcon, props),
DatasetPhysical: (props: IconType) => renderIcon(DatasetPhysicalIcon, props),
DatasetVirtualGreyscale: (props: IconType) =>
renderIcon(DatasetVirtualGreyscaleIcon, props),
DatasetVirtual: (props: IconType) => renderIcon(DatasetVirtualIcon, props),
Download: (props: IconType) => renderIcon(DownloadIcon, props),
EditAlt: (props: IconType) => renderIcon(EditAltIcon, props),
Edit: (props: IconType) => renderIcon(EditIcon, props),
Email: (props: IconType) => renderIcon(EmailIcon, props),
Error: (props: IconType) => renderIcon(ErrorIcon, props),
ErrorSolid: (props: IconType) => renderIcon(ErrorSolidIcon, props),
ErrorSolidSmall: (props: IconType) => renderIcon(ErrorSolidSmallIcon, props),
Exclamation: (props: IconType) => renderIcon(ExclamationIcon, props),
Expand: (props: IconType) => renderIcon(ExpandIcon, props),
Eye: (props: IconType) => renderIcon(EyeIcon, props),
EyeSlash: (props: IconType) => renderIcon(EyeSlashIcon, props),
FavoriteSelected: (props: IconType) =>
renderIcon(FavoriteSelectedIcon, props),
FavoriteSmallSelected: (props: IconType) =>
renderIcon(FavoriteSmallSelectedIcon, props),
FavoriteUnselected: (props: IconType) =>
renderIcon(FavoriteUnselectedIcon, props),
FieldABCIcon: (props: IconType) => renderIcon(FieldABCIcon, props),
FieldBoolean: (props: IconType) => renderIcon(FieldBooleanIcon, props),
FieldDate: (props: IconType) => renderIcon(FieldDateIcon, props),
FieldDerived: (props: IconType) => renderIcon(FieldDerivedIcon, props),
FieldNum: (props: IconType) => renderIcon(FieldNumIcon, props),
FieldStruct: (props: IconType) => renderIcon(FieldStructIcon, props),
File: (props: IconType) => renderIcon(FileIcon, props),
Filter: (props: IconType) => renderIcon(FilterIcon, props),
FilterSmall: (props: IconType) => renderIcon(FilterSmallIcon, props),
Folder: (props: IconType) => renderIcon(FolderIcon, props),
Full: (props: IconType) => renderIcon(FullIcon, props),
Function: (props: IconType) => renderIcon(FunctionIcon, props),
Gear: (props: IconType) => renderIcon(GearIcon, props),
Grid: (props: IconType) => renderIcon(GridIcon, props),
Image: (props: IconType) => renderIcon(ImageIcon, props),
Import: (props: IconType) => renderIcon(ImportIcon, props),
Info: (props: IconType) => renderIcon(InfoIcon, props),
InfoSolid: (props: IconType) => renderIcon(InfoSolidIcon, props),
InfoSolidSmall: (props: IconType) => renderIcon(InfoSolidSmallIcon, props),
Join: (props: IconType) => renderIcon(JoinIcon, props),
Keyboard: (props: IconType) => renderIcon(KeyboardIcon, props),
Layers: (props: IconType) => renderIcon(LayersIcon, props),
Lightbulb: (props: IconType) => renderIcon(LightbulbIcon, props),
Link: (props: IconType) => renderIcon(LinkIcon, props),
List: (props: IconType) => renderIcon(ListIcon, props),
ListView: (props: IconType) => renderIcon(ListViewIcon, props),
Location: (props: IconType) => renderIcon(LocationIcon, props),
LockLocked: (props: IconType) => renderIcon(LockLockedIcon, props),
LockUnlocked: (props: IconType) => renderIcon(LockUnlockedIcon, props),
Map: (props: IconType) => renderIcon(MapIcon, props),
Message: (props: IconType) => renderIcon(MessageIcon, props),
Minus: (props: IconType) => renderIcon(MinusIcon, props),
MinusSolid: (props: IconType) => renderIcon(MinusSolidIcon, props),
MoreHoriz: (props: IconType) => renderIcon(MoreHorizIcon, props),
Move: (props: IconType) => renderIcon(MoveIcon, props),
NavCharts: (props: IconType) => renderIcon(NavChartsIcon, props),
NavDashboard: (props: IconType) => renderIcon(NavDashboardIcon, props),
NavData: (props: IconType) => renderIcon(NavDataIcon, props),
NavExplore: (props: IconType) => renderIcon(NavExploreIcon, props),
NavHome: (props: IconType) => renderIcon(NavHomeIcon, props),
NavLab: (props: IconType) => renderIcon(NavLabIcon, props),
Note: (props: IconType) => renderIcon(NoteIcon, props),
Offline: (props: IconType) => renderIcon(OfflineIcon, props),
Paperclip: (props: IconType) => renderIcon(PaperclipIcon, props),
Placeholder: (props: IconType) => renderIcon(PlaceholderIcon, props),
Plus: (props: IconType) => renderIcon(PlusIcon, props),
PlusLarge: (props: IconType) => renderIcon(PlusLargeIcon, props),
PlusSmall: (props: IconType) => renderIcon(PlusSmallIcon, props),
PlusSolid: (props: IconType) => renderIcon(PlusSolidIcon, props),
Queued: (props: IconType) => renderIcon(QueuedIcon, props),
Refresh: (props: IconType) => renderIcon(RefreshIcon, props),
Running: (props: IconType) => renderIcon(RunningIcon, props),
Save: (props: IconType) => renderIcon(SaveIcon, props),
SQL: (props: IconType) => renderIcon(SQLIcon, props),
Search: (props: IconType) => renderIcon(SearchIcon, props),
Server: (props: IconType) => renderIcon(ServerIcon, props),
Share: (props: IconType) => renderIcon(ShareIcon, props),
Slack: (props: IconType) => renderIcon(SlackIcon, props),
SortAsc: (props: IconType) => renderIcon(SortAscIcon, props),
SortDesc: (props: IconType) => renderIcon(SortDescIcon, props),
Sort: (props: IconType) => renderIcon(SortIcon, props),
Table: (props: IconType) => renderIcon(TableIcon, props),
Tag: (props: IconType) => renderIcon(TagIcon, props),
Trash: (props: IconType) => renderIcon(TrashIcon, props),
TriangleChange: (props: IconType) => renderIcon(TriangleChangeIcon, props),
TriangleDown: (props: IconType) => renderIcon(TriangleDownIcon, props),
TriangleUp: (props: IconType) => renderIcon(TriangleUpIcon, props),
UpLevel: (props: IconType) => renderIcon(UpLevelIcon, props),
User: (props: IconType) => renderIcon(UserIcon, props),
Warning: (props: IconType) => renderIcon(WarningIcon, props),
WarningSolid: (props: IconType) => renderIcon(WarningSolidIcon, props),
XLarge: (props: IconType) => renderIcon(XLargeIcon, props),
XSmall: (props: IconType) => renderIcon(XSmallIcon, props),
};
@@ -0,0 +1,166 @@
/**
* 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 React from 'react';
import _ from 'lodash';
import AntdEnhancedIcons from './AntdEnhanced';
import Icon from './Icon';
import IconType from './IconType';
const IconFileNames = [
'alert',
'alert_solid',
'alert_solid_small',
'binoculars',
'bolt',
'bolt_small',
'bolt_small_run',
'calendar',
'cancel',
'cancel_solid',
'cancel-x',
'card_view',
'cards',
'cards_locked',
'caret_down',
'caret_left',
'caret_right',
'caret_up',
'certified',
'check',
'checkbox-half',
'checkbox-off',
'checkbox-on',
'circle_check',
'circle_check_solid',
'circle',
'clock',
'close',
'code',
'cog',
'collapse',
'color_palette',
'components',
'copy',
'cursor_target',
'database',
'dataset_physical',
'dataset_virtual_greyscale',
'dataset_virtual',
'download',
'edit_alt',
'edit',
'email',
'error',
'error_solid',
'error_solid_small',
'exclamation',
'expand',
'eye',
'eye_slash',
'favorite-selected',
'favorite_small_selected',
'favorite-unselected',
'field_abc',
'field_boolean',
'field_date',
'field_derived',
'field_num',
'field_struct',
'file',
'filter',
'filter_small',
'folder',
'full',
'function_x',
'gear',
'grid',
'image',
'import',
'info',
'info-solid',
'info_solid_small',
'join',
'keyboard',
'layers',
'lightbulb',
'link',
'list',
'list_view',
'location',
'lock_locked',
'lock_unlocked',
'map',
'message',
'minus',
'minus_solid',
'more_horiz',
'move',
'nav_charts',
'nav_dashboard',
'nav_data',
'nav_explore',
'nav_home',
'nav_lab',
'note',
'offline',
'paperclip',
'placeholder',
'plus',
'plus_large',
'plus_small',
'plus_solid',
'queued',
'refresh',
'running',
'save',
'sql',
'search',
'server',
'share',
'slack',
'sort_asc',
'sort_desc',
'sort',
'table',
'tag',
'trash',
'triangle_change',
'triangle_down',
'triangle_up',
'up-level',
'user',
'warning',
'warning_solid',
'x-large',
'x-small',
];
const iconOverrides: Record<string, React.FC> = {};
IconFileNames.forEach(fileName => {
const keyName = _.startCase(fileName).replace(/ /g, '');
iconOverrides[keyName] = (props: IconType) => (
<Icon fileName={fileName} {...props} />
);
});
export default {
...AntdEnhancedIcons,
...iconOverrides,
};
@@ -0,0 +1,79 @@
/**
* 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 React from 'react';
import InfoTooltip, { InfoTooltipProps } from 'src/components/InfoTooltip';
export default {
title: 'InfoTooltip',
component: InfoTooltip,
};
export const InteractiveInfoTooltip = (props: InfoTooltipProps) => {
const styles = {
padding: '100px 0 0 200px',
};
return (
<div style={styles}>
<InfoTooltip {...props} />
</div>
);
};
InteractiveInfoTooltip.args = {
tooltip: 'This is the text that will display!',
};
InteractiveInfoTooltip.argTypes = {
placement: {
defaultValue: 'top',
control: {
type: 'select',
options: [
'bottom',
'left',
'right',
'top',
'topLeft',
'topRight',
'bottomLeft',
'bottomRight',
'leftTop',
'leftBottom',
'rightTop',
'rightBottom',
],
},
},
trigger: {
defaultValue: 'hover',
control: {
type: 'select',
options: ['hover', 'click'],
},
},
};
InteractiveInfoTooltip.story = {
parameters: {
knobs: {
disable: true,
},
},
};
@@ -22,7 +22,7 @@ import { styled } from '@superset-ui/core';
import { Tooltip } from 'src/components/Tooltip';
import Icon from 'src/components/Icon';
interface InfoTooltipProps {
export interface InfoTooltipProps {
className?: string;
tooltip: string;
placement?:
@@ -87,6 +87,8 @@ export default function Label(props: LabelProps) {
return (
<Tag
onClick={onClick}
{...rest}
css={{
transition: `background-color ${transitionTiming}s`,
whiteSpace: 'nowrap',
@@ -99,14 +101,13 @@ export default function Label(props: LabelProps) {
padding: '0.35em 0.8em',
lineHeight: 1,
color,
maxWidth: '100%',
'&:hover': {
backgroundColor: backgroundColorHover,
borderColor: borderColorHover,
opacity: 1,
},
}}
onClick={onClick}
{...rest}
>
{children}
</Tag>
@@ -34,7 +34,7 @@ describe('LastUpdated', () => {
it('renders a refresh action', () => {
const mockAction = jest.fn();
wrapper = mount(<LastUpdated updatedAt={updatedAt} update={mockAction} />);
const props = wrapper.find('[data-test="refresh"]').props();
const props = wrapper.find('[data-test="refresh"]').first().props();
if (props.onClick) {
props.onClick({} as React.MouseEvent);
}
@@ -42,6 +42,12 @@ export default function SearchFilter({
setValue('');
onSubmit('');
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.currentTarget.value);
if (e.currentTarget.value === '') {
onClear();
}
};
return (
<FilterContainer>
@@ -50,9 +56,7 @@ export default function SearchFilter({
placeholder={Header}
name={name}
value={value}
onChange={e => {
setValue(e.currentTarget.value);
}}
onChange={handleChange}
onSubmit={handleSubmit}
onClear={onClear}
/>
@@ -17,8 +17,8 @@
* under the License.
*/
import React from 'react';
import { styled } from '@superset-ui/core';
import Icon from 'src/components/Icon';
import { styled, useTheme } from '@superset-ui/core';
import Icon, { IconName } from 'src/components/Icon';
import { AntdCard, Skeleton, ThinSkeleton } from 'src/common/components';
import { Tooltip } from 'src/components/Tooltip';
import ImageLoader, { BackgroundPosition } from './ImageLoader';
@@ -97,13 +97,15 @@ const TitleContainer = styled.div`
}
`;
const TitleLink = styled.a`
color: ${({ theme }) => theme.colors.grayscale.dark1} !important;
overflow: hidden;
text-overflow: ellipsis;
const TitleLink = styled.span`
& a {
color: ${({ theme }) => theme.colors.grayscale.dark1} !important;
overflow: hidden;
text-overflow: ellipsis;
& + .title-right {
margin-left: ${({ theme }) => theme.gridUnit * 2}px;
& + .title-right {
margin-left: ${({ theme }) => theme.gridUnit * 2}px;
}
}
`;
@@ -128,18 +130,20 @@ const CoverFooterRight = styled.div`
text-overflow: ellipsis;
`;
const SkeletonTitle = styled(Skeleton.Input)`
width: ${({ theme }) => Math.trunc(theme.gridUnit * 62.5)}px;
`;
const SkeletonActions = styled(Skeleton.Button)`
width: ${({ theme }) => theme.gridUnit * 10}px;
`;
const paragraphConfig = { rows: 1, width: 150 };
interface LinkProps {
to: string;
}
const AnchorLink: React.FC<LinkProps> = ({ to, children }) => (
<a href={to}>{children}</a>
);
interface CardProps {
title?: React.ReactNode;
url?: string;
linkComponent?: React.ComponentType<LinkProps>;
imgURL?: string;
imgFallbackURL?: string;
imgPosition?: BackgroundPosition;
@@ -157,6 +161,7 @@ interface CardProps {
function ListViewCard({
title,
url,
linkComponent,
titleRight,
imgURL,
imgFallbackURL,
@@ -169,13 +174,15 @@ function ListViewCard({
imgPosition = 'top',
cover,
}: CardProps) {
const Link = url && linkComponent ? linkComponent : AnchorLink;
const theme = useTheme();
return (
<StyledCard
data-test="styled-card"
cover={
cover || (
<Cover>
<a href={url}>
<Link to={url!}>
<div className="gradient-container">
<ImageLoader
src={imgURL || ''}
@@ -184,7 +191,7 @@ function ListViewCard({
position={imgPosition}
/>
</div>
</a>
</Link>
<CoverFooter className="cover-footer">
{!loading && coverLeft && (
<CoverFooterLeft>{coverLeft}</CoverFooterLeft>
@@ -202,10 +209,21 @@ function ListViewCard({
title={
<>
<TitleContainer>
<SkeletonTitle active size="small" />
<Skeleton.Input
active
size="small"
css={{
width: Math.trunc(theme.gridUnit * 62.5),
}}
/>
<div className="card-actions">
<Skeleton.Button active shape="circle" />{' '}
<SkeletonActions active />
<Skeleton.Button
active
css={{
width: theme.gridUnit * 10,
}}
/>
</div>
</TitleContainer>
</>
@@ -225,7 +243,9 @@ function ListViewCard({
title={
<TitleContainer>
<Tooltip title={title}>
<TitleLink href={url}>{title}</TitleLink>
<TitleLink>
<Link to={url!}>{title}</Link>
</TitleLink>
</Tooltip>
{titleRight && <div className="title-right"> {titleRight}</div>}
<div className="card-actions" data-test="card-actions">
@@ -234,8 +254,7 @@ function ListViewCard({
</TitleContainer>
}
description={description}
// @ts-ignore
avatar={avatar ? <Icon name={avatar} /> : null}
avatar={avatar ? <Icon name={avatar as IconName} /> : null}
/>
)}
</StyledCard>
@@ -18,7 +18,9 @@
*/
import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { Menu } from './Menu';
import { dropdownItems } from './MenuRight';
const mockedProps = {
data: {
@@ -157,18 +159,19 @@ test('should render all the top navbar menu items', () => {
} = mockedProps;
render(<Menu {...mockedProps} />);
menu.forEach(item => {
const menuItem = screen.getByText(item.label);
expect(menuItem).toHaveAttribute('href', item.url);
expect(screen.getByText(item.label)).toBeInTheDocument();
});
});
test('should render the top navbar child menu items', () => {
test('should render the top navbar child menu items', async () => {
const {
data: { menu },
} = mockedProps;
render(<Menu {...mockedProps} />);
const datasets = screen.getByText('Datasets');
const databases = screen.getByText('Databases');
const sources = screen.getByText('Sources');
userEvent.hover(sources);
const datasets = await screen.findByText('Datasets');
const databases = await screen.findByText('Databases');
const dataset = menu[1].childs![0] as { url: string };
const database = menu[1].childs![2] as { url: string };
@@ -176,23 +179,53 @@ test('should render the top navbar child menu items', () => {
expect(databases).toHaveAttribute('href', database.url);
});
test('should render the Settings', () => {
render(<Menu {...mockedProps} />);
const settings = screen.getByText('Settings');
expect(settings).toHaveAttribute('href', '#');
test('should render the dropdown items', async () => {
render(<Menu {...notanonProps} />);
const dropdown = screen.getByTestId('new-dropdown-icon');
userEvent.hover(dropdown);
expect(await screen.findByText(dropdownItems[0].label)).toHaveAttribute(
'href',
dropdownItems[0].url,
);
expect(
screen.getByTestId(`menu-item-${dropdownItems[0].label}`),
).toBeInTheDocument();
expect(await screen.findByText(dropdownItems[1].label)).toHaveAttribute(
'href',
dropdownItems[1].url,
);
expect(
screen.getByTestId(`menu-item-${dropdownItems[1].label}`),
).toBeInTheDocument();
expect(await screen.findByText(dropdownItems[2].label)).toHaveAttribute(
'href',
dropdownItems[2].url,
);
expect(
screen.getByTestId(`menu-item-${dropdownItems[2].label}`),
).toBeInTheDocument();
});
test('should render the Settings menu item', () => {
test('should render the Settings', async () => {
render(<Menu {...mockedProps} />);
expect(screen.getByText('Security')).toBeInTheDocument();
const settings = await screen.findByText('Settings');
expect(settings).toBeInTheDocument();
});
test('should render the Settings dropdown child menu items', () => {
test('should render the Settings menu item', async () => {
render(<Menu {...mockedProps} />);
userEvent.hover(screen.getByText('Settings'));
const label = await screen.findByText('Security');
expect(label).toBeInTheDocument();
});
test('should render the Settings dropdown child menu items', async () => {
const {
data: { settings },
} = mockedProps;
render(<Menu {...mockedProps} />);
const listUsers = screen.getByText('List Users');
userEvent.hover(screen.getByText('Settings'));
const listUsers = await screen.findByText('List Users');
expect(listUsers).toHaveAttribute('href', settings[0].childs[0].url);
});
@@ -206,7 +239,7 @@ test('should NOT render the plus menu (+) when user is anonymous', () => {
expect(screen.queryByTestId('new-dropdown')).not.toBeInTheDocument();
});
test('should render the user actions when user is not anonymous', () => {
test('should render the user actions when user is not anonymous', async () => {
const {
data: {
navbar_right: { user_info_url, user_logout_url },
@@ -214,10 +247,12 @@ test('should render the user actions when user is not anonymous', () => {
} = mockedProps;
render(<Menu {...notanonProps} />);
expect(screen.getByText('User')).toBeInTheDocument();
userEvent.hover(screen.getByText('Settings'));
const user = await screen.findByText('User');
expect(user).toBeInTheDocument();
const info = screen.getByText('Info');
const logout = screen.getByText('Logout');
const info = await screen.findByText('Info');
const logout = await screen.findByText('Logout');
expect(info).toHaveAttribute('href', user_info_url);
expect(logout).toHaveAttribute('href', user_logout_url);
@@ -228,7 +263,7 @@ test('should NOT render the user actions when user is anonymous', () => {
expect(screen.queryByText('User')).not.toBeInTheDocument();
});
test('should render the Profile link when available', () => {
test('should render the Profile link when available', async () => {
const {
data: {
navbar_right: { user_profile_url },
@@ -236,11 +271,13 @@ test('should render the Profile link when available', () => {
} = mockedProps;
render(<Menu {...notanonProps} />);
const profile = screen.getByText('Profile');
userEvent.hover(screen.getByText('Settings'));
const profile = await screen.findByText('Profile');
expect(profile).toHaveAttribute('href', user_profile_url);
});
test('should render the About section and version_string or sha when available', () => {
test('should render the About section and version_string or sha when available', async () => {
const {
data: {
navbar_right: { version_sha, version_string },
@@ -248,24 +285,28 @@ test('should render the About section and version_string or sha when available',
} = mockedProps;
render(<Menu {...mockedProps} />);
expect(screen.getByText('About')).toBeInTheDocument();
expect(screen.getByText(`Version: ${version_string}`)).toBeInTheDocument();
expect(screen.getByText(`SHA: ${version_sha}`)).toBeInTheDocument();
userEvent.hover(screen.getByText('Settings'));
const about = await screen.findByText('About');
const version = await screen.findByText(`Version: ${version_string}`);
const sha = await screen.findByText(`SHA: ${version_sha}`);
expect(about).toBeInTheDocument();
expect(version).toBeInTheDocument();
expect(sha).toBeInTheDocument();
});
test('should render the Documentation link when available', () => {
test('should render the Documentation link when available', async () => {
const {
data: {
navbar_right: { documentation_url },
},
} = mockedProps;
render(<Menu {...mockedProps} />);
const doc = screen.getByTitle('Documentation');
expect(doc).toHaveAttribute('href', documentation_url);
userEvent.hover(screen.getByText('Settings'));
const doc = await screen.findByTitle('Documentation');
expect(doc.firstChild).toHaveAttribute('href', documentation_url);
});
test('should render the Bug Report link when available', () => {
test('should render the Bug Report link when available', async () => {
const {
data: {
navbar_right: { bug_report_url },
@@ -273,8 +314,8 @@ test('should render the Bug Report link when available', () => {
} = mockedProps;
render(<Menu {...mockedProps} />);
const bugReport = screen.getByTitle('Report a Bug');
expect(bugReport).toHaveAttribute('href', bug_report_url);
const bugReport = await screen.findByTitle('Report a Bug');
expect(bugReport.firstChild).toHaveAttribute('href', bug_report_url);
});
test('should render the Login link when user is anonymous', () => {
+166 -205
View File
@@ -16,18 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState } from 'react';
import { t, styled } from '@superset-ui/core';
import { Nav, Navbar, NavItem } from 'react-bootstrap';
import NavDropdown from 'src/components/NavDropdown';
import { Menu as DropdownMenu } from 'src/common/components';
import React, { useState, useEffect } from 'react';
import { styled, css } from '@superset-ui/core';
import { debounce } from 'lodash';
import { Global } from '@emotion/react';
import { getUrlParam } from 'src/utils/urlUtils';
import { MainNav as DropdownMenu, MenuMode } from 'src/common/components';
import { Link } from 'react-router-dom';
import MenuObject, {
MenuObjectProps,
MenuObjectChildProps,
} from './MenuObject';
import LanguagePicker, { Languages } from './LanguagePicker';
import NewMenu from './NewMenu';
import { Row, Col } from 'antd';
import Icon from 'src/components/Icon';
import RightMenu from './MenuRight';
import { Languages } from './LanguagePicker';
interface BrandProps {
path: string;
@@ -36,7 +35,7 @@ interface BrandProps {
width: string | number;
}
interface NavBarProps {
export interface NavBarProps {
bug_report_url?: string;
version_string?: string;
version_sha?: string;
@@ -61,7 +60,24 @@ export interface MenuProps {
isFrontendRoute?: (path?: string) => boolean;
}
interface MenuObjectChildProps {
label: string;
name?: string;
icon: string;
index: number;
url?: string;
isFrontendRoute?: boolean;
}
export interface MenuObjectProps extends MenuObjectChildProps {
childs?: (MenuObjectChildProps | string)[];
isHeader?: boolean;
}
const StyledHeader = styled.header`
background-color: white;
margin-bottom: 2px;
border-bottom: 2px solid ${({ theme }) => theme.colors.grayscale.light4}px;
&:nth-last-of-type(2) nav {
margin-bottom: 2px;
}
@@ -69,59 +85,43 @@ const StyledHeader = styled.header`
.caret {
display: none;
}
.navbar-inverse {
border: none;
}
.version-info {
padding: ${({ theme }) => theme.gridUnit * 1.5}px
${({ theme }) => theme.gridUnit * 4}px
${({ theme }) => theme.gridUnit * 1.5}px
${({ theme }) => theme.gridUnit * 7}px;
color: ${({ theme }) => theme.colors.grayscale.base};
font-size: ${({ theme }) => theme.typography.sizes.xs}px;
div {
white-space: nowrap;
}
}
.navbar-brand {
display: flex;
flex-direction: column;
justify-content: center;
}
.nav > li > a {
@media (max-width: 767px) {
.navbar-brand {
float: none;
}
}
.ant-menu-horizontal .ant-menu-item {
height: 100%;
line-height: inherit;
}
.ant-menu > .ant-menu-item > a {
padding: ${({ theme }) => theme.gridUnit * 4}px;
}
.dropdown-header {
text-transform: uppercase;
padding-left: 12px;
@media (max-width: 767px) {
.ant-menu-item {
padding: 0 ${({ theme }) => theme.gridUnit * 6}px 0
${({ theme }) => theme.gridUnit * 3}px !important;
}
.ant-menu > .ant-menu-item > a {
padding: 0px;
}
.main-nav .ant-menu-submenu-title > svg:nth-child(1) {
display: none;
}
.ant-menu-item-active > a {
&:hover {
color: ${({ theme }) => theme.colors.primary.base} !important;
background-color: transparent !important;
}
}
}
.navbar-inverse .navbar-nav li a {
color: ${({ theme }) => theme.colors.grayscale.dark1};
border-bottom: none;
transition: background-color ${({ theme }) => theme.transitionTiming}s;
&:after {
content: '';
position: absolute;
bottom: -3px;
left: 50%;
width: 0;
height: 3px;
opacity: 0;
transform: translateX(-50%);
transition: all ${({ theme }) => theme.transitionTiming}s;
background-color: ${({ theme }) => theme.colors.primary.base};
}
&:focus {
border-bottom: none;
background-color: transparent;
/* background-color: ${({ theme }) => theme.colors.primary.light5}; */
}
.ant-menu-item a {
&:hover {
color: ${({ theme }) => theme.colors.grayscale.dark1};
background-color: ${({ theme }) => theme.colors.primary.light5};
@@ -133,165 +133,126 @@ const StyledHeader = styled.header`
}
}
}
.navbar-right {
display: flex;
align-items: center;
}
.ant-menu {
.ant-menu-item-group-title {
padding-bottom: ${({ theme }) => theme.gridUnit}px;
}
.ant-menu-item {
margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
}
.about-section {
margin: ${({ theme }) => theme.gridUnit}px 0
${({ theme }) => theme.gridUnit * 2}px;
}
}
`;
const { SubMenu } = DropdownMenu;
export function Menu({
data: { menu, brand, navbar_right: navbarRight, settings },
isFrontendRoute = () => false,
}: MenuProps) {
const [dropdownOpen, setDropdownOpen] = useState(false);
const [showMenu, setMenu] = useState<MenuMode>('horizontal');
useEffect(() => {
function handleResize() {
if (window.innerWidth <= 767) {
setMenu('inline');
} else setMenu('horizontal');
}
handleResize();
const windowResize = debounce(() => handleResize(), 10);
window.addEventListener('resize', windowResize);
return () => window.removeEventListener('resize', windowResize);
}, []);
const standalone = getUrlParam('standalone', 'boolean');
if (standalone) return <></>;
const renderSubMenu = ({
label,
childs,
url,
index,
isFrontendRoute,
}: MenuObjectProps) => {
if (url && isFrontendRoute) {
return (
<DropdownMenu.Item key={label} role="presentation">
<Link role="button" to={url}>
{label}
</Link>
</DropdownMenu.Item>
);
}
if (url) {
return (
<DropdownMenu.Item key={label}>
<a href={url}>{label}</a>
</DropdownMenu.Item>
);
}
return (
<SubMenu key={index} title={label} icon={<Icon name="triangle-down" />}>
{childs?.map((child: MenuObjectChildProps | string, index1: number) => {
if (typeof child === 'string' && child === '-') {
return <DropdownMenu.Divider key={`$${index1}`} />;
}
if (typeof child !== 'string') {
return (
<DropdownMenu.Item key={`${child.label}`}>
{child.isFrontendRoute ? (
<Link to={child.url || ''}>{child.label}</Link>
) : (
<a href={child.url}>{child.label}</a>
)}
</DropdownMenu.Item>
);
}
return null;
})}
</SubMenu>
);
};
return (
<StyledHeader className="top" id="main-menu">
<Navbar inverse fluid staticTop role="navigation">
<Navbar.Header>
<Navbar.Brand>
<a className="navbar-brand" href={brand.path}>
<img width={brand.width} src={brand.icon} alt={brand.alt} />
</a>
</Navbar.Brand>
<Navbar.Toggle />
</Navbar.Header>
<Nav data-test="navbar-top">
{menu.map((item, index) => {
const props = {
...item,
isFrontendRoute: isFrontendRoute(item.url),
childs: item.childs?.map(c => {
if (typeof c === 'string') {
return c;
}
return {
...c,
isFrontendRoute: isFrontendRoute(c.url),
};
}),
};
return <MenuObject {...props} key={item.label} index={index + 1} />;
})}
</Nav>
<Nav className="navbar-right">
{!navbarRight.user_is_anonymous && <NewMenu />}
<NavDropdown
id="settings-dropdown"
title={t('Settings')}
onMouseEnter={() => setDropdownOpen(true)}
onMouseLeave={() => setDropdownOpen(false)}
onToggle={value => setDropdownOpen(value)}
open={dropdownOpen}
<StyledHeader className="top" id="main-menu" role="navigation">
<Global
styles={css`
.ant-menu-submenu.ant-menu-submenu-popup.ant-menu.ant-menu-light.ant-menu-submenu-placement-bottomLeft {
border-radius: 0px;
}
.ant-menu-submenu.ant-menu-submenu-popup.ant-menu.ant-menu-light {
border-radius: 0px;
}
`}
/>
<Row>
<Col lg={19} md={19} sm={24} xs={24}>
<a className="navbar-brand" href={brand.path}>
<img width={brand.width} src={brand.icon} alt={brand.alt} />
</a>
<DropdownMenu
mode={showMenu}
data-test="navbar-top"
className="main-nav"
>
<DropdownMenu>
{settings.map((section, index) => [
<DropdownMenu.ItemGroup
key={`${section.label}`}
title={section.label}
>
{section.childs?.map(child => {
if (typeof child !== 'string') {
return (
<DropdownMenu.Item key={`${child.label}`}>
{isFrontendRoute(child.url) ? (
<Link to={child.url || ''}>{child.label}</Link>
) : (
<a href={child.url}>{child.label}</a>
)}
</DropdownMenu.Item>
);
}
return null;
})}
</DropdownMenu.ItemGroup>,
index < settings.length - 1 && <DropdownMenu.Divider />,
])}
{menu.map((item, index) => {
const props = {
...item,
isFrontendRoute: isFrontendRoute(item.url),
childs: item.childs?.map(c => {
if (typeof c === 'string') {
return c;
}
{!navbarRight.user_is_anonymous && [
<DropdownMenu.Divider key="user-divider" />,
<DropdownMenu.ItemGroup key="user-section" title={t('User')}>
{navbarRight.user_profile_url && (
<DropdownMenu.Item key="profile">
<a href={navbarRight.user_profile_url}>{t('Profile')}</a>
</DropdownMenu.Item>
)}
<DropdownMenu.Item key="info">
<a href={navbarRight.user_info_url}>{t('Info')}</a>
</DropdownMenu.Item>
<DropdownMenu.Item key="logout">
<a href={navbarRight.user_logout_url}>{t('Logout')}</a>
</DropdownMenu.Item>
</DropdownMenu.ItemGroup>,
]}
{(navbarRight.version_string || navbarRight.version_sha) && [
<DropdownMenu.Divider key="version-info-divider" />,
<DropdownMenu.ItemGroup key="about-section" title={t('About')}>
<div className="about-section">
{navbarRight.version_string && (
<li className="version-info">
<span>Version: {navbarRight.version_string}</span>
</li>
)}
{navbarRight.version_sha && (
<li className="version-info">
<span>SHA: {navbarRight.version_sha}</span>
</li>
)}
</div>
</DropdownMenu.ItemGroup>,
]}
</DropdownMenu>
</NavDropdown>
{navbarRight.documentation_url && (
<NavItem
href={navbarRight.documentation_url}
target="_blank"
title="Documentation"
>
<i className="fa fa-question" />
&nbsp;
</NavItem>
)}
{navbarRight.bug_report_url && (
<NavItem
href={navbarRight.bug_report_url}
target="_blank"
title="Report a Bug"
>
<i className="fa fa-bug" />
&nbsp;
</NavItem>
)}
{navbarRight.show_language_picker && (
<LanguagePicker
locale={navbarRight.locale}
languages={navbarRight.languages}
/>
)}
{navbarRight.user_is_anonymous && (
<NavItem href={navbarRight.user_login_url}>
<i className="fa fa-fw fa-sign-in" />
{t('Login')}
</NavItem>
)}
</Nav>
</Navbar>
return {
...c,
isFrontendRoute: isFrontendRoute(c.url),
};
}),
};
return renderSubMenu(props);
})}
</DropdownMenu>
</Col>
<Col lg={5} md={5} sm={24} xs={24}>
<RightMenu
settings={settings}
navbarRight={navbarRight}
isFrontendRoute={isFrontendRoute}
/>
</Col>
</Row>
</StyledHeader>
);
}
@@ -1,94 +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 React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { NavItem } from 'react-bootstrap';
import { Menu } from 'src/common/components';
import NavDropdown from '../NavDropdown';
export interface MenuObjectChildProps {
label: string;
name?: string;
icon: string;
index: number;
url?: string;
isFrontendRoute?: boolean;
}
export interface MenuObjectProps extends MenuObjectChildProps {
childs?: (MenuObjectChildProps | string)[];
isHeader?: boolean;
}
export default function MenuObject({
label,
childs,
url,
index,
isFrontendRoute,
}: MenuObjectProps) {
const [dropdownOpen, setDropdownOpen] = useState(false);
if (url && isFrontendRoute) {
return (
<li role="presentation">
<Link role="button" to={url}>
{label}
</Link>
</li>
);
}
if (url) {
return (
<NavItem eventKey={index} href={url}>
{label}
</NavItem>
);
}
return (
<NavDropdown
id={`menu-dropdown-${label}`}
title={label}
onMouseEnter={() => setDropdownOpen(true)}
onMouseLeave={() => setDropdownOpen(false)}
onToggle={value => setDropdownOpen(value)}
open={dropdownOpen}
>
<Menu>
{childs?.map((child: MenuObjectChildProps | string, index1: number) => {
if (typeof child === 'string' && child === '-') {
return <Menu.Divider key={`$${index1}`} />;
}
if (typeof child !== 'string') {
return (
<Menu.Item key={`${child.label}`}>
{child.isFrontendRoute ? (
<Link to={child.url || ''}>{child.label}</Link>
) : (
<a href={child.url}>{child.label}</a>
)}
</Menu.Item>
);
}
return null;
})}
</Menu>
</NavDropdown>
);
}

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