Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fa307566d | ||
|
|
097d1fb85c | ||
|
|
5646bcb9c8 | ||
|
|
1393c5660c | ||
|
|
3cea7b4199 | ||
|
|
15126ef17d | ||
|
|
d99d173817 | ||
|
|
5f2bb51393 | ||
|
|
2da0c347db | ||
|
|
c5637dba34 | ||
|
|
574d3a1a49 | ||
|
|
b6ef99a51c | ||
|
|
8843b895ce | ||
|
|
b39dae95c5 | ||
|
|
e5c6472d4f | ||
|
|
e48d3f5315 | ||
|
|
925a05e67e | ||
|
|
4daa87fcd0 | ||
|
|
09050aee62 | ||
|
|
0fe2bf1150 | ||
|
|
adbe56c8a8 | ||
|
|
e21beb76f1 | ||
|
|
77bc51f6aa | ||
|
|
f33c433f33 | ||
|
|
37276e1af9 | ||
|
|
7a95f8f839 | ||
|
|
809983d32d | ||
|
|
cee22e28c0 | ||
|
|
e16c4d856e | ||
|
|
e7a4734742 | ||
|
|
79ff96269b | ||
|
|
4f000cc8d1 | ||
|
|
6871ad15f1 | ||
|
|
66a4c94a1e | ||
|
|
c14665cad6 | ||
|
|
d1d98d81b0 | ||
|
|
e8e838e279 | ||
|
|
680c96ec54 | ||
|
|
05c24056b5 | ||
|
|
19b408b08d | ||
|
|
10a53b7f5d | ||
|
|
52dbe311c7 | ||
|
|
1df93848d2 | ||
|
|
96289e93d7 | ||
|
|
f55882e756 | ||
|
|
4ac90ddaee | ||
|
|
68058b0ad9 | ||
|
|
331310db9b | ||
|
|
fa510df624 | ||
|
|
fa0915d9e6 | ||
|
|
9b42eec064 | ||
|
|
12baba03cc | ||
|
|
f9567efe81 | ||
|
|
c832542570 | ||
|
|
ae256a9119 | ||
|
|
8ccd19629a | ||
|
|
7182a1b390 | ||
|
|
b030c9801c | ||
|
|
93c7f5bb44 | ||
|
|
5aef6bccd1 | ||
|
|
90a72bf8d2 | ||
|
|
a816655715 | ||
|
|
23014e9233 | ||
|
|
d138b4407d | ||
|
|
2242f74818 | ||
|
|
21cf12a480 | ||
|
|
158ac302d8 | ||
|
|
582900c170 | ||
|
|
9a22fb00d9 | ||
|
|
a75e4af99b | ||
|
|
2d991ec359 | ||
|
|
e05a70fc82 | ||
|
|
0b9fd24cf3 | ||
|
|
2f9efb2e23 | ||
|
|
bbb1f2d757 | ||
|
|
abbf4bf05a | ||
|
|
1f8de1d06c | ||
|
|
7466595563 | ||
|
|
e507508b48 | ||
|
|
efcdc8cb9b | ||
|
|
b4e527a8c4 | ||
|
|
b147fa84e2 | ||
|
|
11260b3117 | ||
|
|
13cfc421d5 | ||
|
|
9097c29d39 | ||
|
|
319125ec3b | ||
|
|
982a91ee62 | ||
|
|
bc47bc8f66 | ||
|
|
a52ff13b5d | ||
|
|
545e257537 | ||
|
|
a2831382a9 | ||
|
|
6541a03d0b | ||
|
|
1c16261651 | ||
|
|
2dc3fa3f21 | ||
|
|
1d6a746a09 | ||
|
|
4410fd047e | ||
|
|
b38aadf9e4 | ||
|
|
e7f5100833 | ||
|
|
b78b7b3fad | ||
|
|
183b5ae362 | ||
|
|
bbdb4ee24f | ||
|
|
43ee52be57 | ||
|
|
fc0c1155c2 | ||
|
|
2486fd4f6d | ||
|
|
d8bb2d3e62 | ||
|
|
77d17152bc | ||
|
|
86d2a616c1 | ||
|
|
8e6803f65f | ||
|
|
44c09b5383 | ||
|
|
40fb94dcca |
@@ -43,4 +43,5 @@ install/
|
||||
superset-frontend/cypress-base/
|
||||
superset-frontend/coverage/
|
||||
superset/static/assets/
|
||||
superset-websocket/dist/
|
||||
venv
|
||||
|
||||
@@ -54,6 +54,7 @@ envpy3
|
||||
env36
|
||||
local_config.py
|
||||
/superset_config.py
|
||||
/superset_text.yml
|
||||
superset.egg-info/
|
||||
superset/bin/supersetc
|
||||
tmp
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,8 +17,7 @@ specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
Superset
|
||||
=========
|
||||
# Superset
|
||||
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://github.com/apache/superset/tree/latest)
|
||||
@@ -31,7 +30,7 @@ Superset
|
||||
[](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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. You’ll 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. You’ll 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 you’re 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 |
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -29,7 +29,7 @@ export default {
|
||||
chartUpdateStartTime: 1525852454838,
|
||||
latestQueryFormData: {},
|
||||
queryRequest: {},
|
||||
queryResponse: {},
|
||||
queriesResponse: [{}],
|
||||
triggerQuery: false,
|
||||
lastRendered: 0,
|
||||
form_data: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 |
@@ -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);
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
</NavItem>
|
||||
)}
|
||||
{navbarRight.bug_report_url && (
|
||||
<NavItem
|
||||
href={navbarRight.bug_report_url}
|
||||
target="_blank"
|
||||
title="Report a Bug"
|
||||
>
|
||||
<i className="fa fa-bug" />
|
||||
|
||||
</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>
|
||||
);
|
||||
}
|
||||