mirror of
https://github.com/apache/superset.git
synced 2026-08-31 20:41:17 +00:00
Compare commits
112
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6e460fc0e | ||
|
|
47c5334502 | ||
|
|
950a21a020 | ||
|
|
bad48d0722 | ||
|
|
c8edd1fb25 | ||
|
|
53a121d9e1 | ||
|
|
74b63a4ba4 | ||
|
|
362948324c | ||
|
|
4cac7feb67 | ||
|
|
d6b2e86155 | ||
|
|
7132d5a86a | ||
|
|
55f18a34a9 | ||
|
|
2c3ba95768 | ||
|
|
6ede3271ff | ||
|
|
0e9c0f621a | ||
|
|
47c1e09c75 | ||
|
|
9a2b1a5cf7 | ||
|
|
9c12b1c7da | ||
|
|
0db59b45b8 | ||
|
|
ef0ede7c13 | ||
|
|
62b94d5abf | ||
|
|
318eff7327 | ||
|
|
211564a6da | ||
|
|
7a8e8f890f | ||
|
|
7b47e43fd0 | ||
|
|
fc857d987b | ||
|
|
a849c29288 | ||
|
|
1f013055d2 | ||
|
|
2e887598ba | ||
|
|
aa94488065 | ||
|
|
163b71e019 | ||
|
|
a098809294 | ||
|
|
95325c4673 | ||
|
|
68c9a8177c | ||
|
|
f314685a8e | ||
|
|
0b34197815 | ||
|
|
b627011463 | ||
|
|
2aa9348759 | ||
|
|
6217cb636f | ||
|
|
2a458a4802 | ||
|
|
989744aba2 | ||
|
|
4dfee727e8 | ||
|
|
b2fd56094f | ||
|
|
03146b21be | ||
|
|
e0172a24b8 | ||
|
|
bd42e336c7 | ||
|
|
84d3e3d25f | ||
|
|
a194393efc | ||
|
|
bc8e9513bf | ||
|
|
f4094829c2 | ||
|
|
657377b0ab | ||
|
|
8019c8b665 | ||
|
|
4c73b040c8 | ||
|
|
d3a208eb8a | ||
|
|
7d0cb39f2a | ||
|
|
553932dc72 | ||
|
|
da7a74e604 | ||
|
|
15f3ea8d05 | ||
|
|
62350f2e5f | ||
|
|
e5ddb3bbd6 | ||
|
|
f425c9d315 | ||
|
|
a8500abd8f | ||
|
|
ee9ece8992 | ||
|
|
7468904968 | ||
|
|
8a68a96f77 | ||
|
|
fa34e0fae3 | ||
|
|
54679c5f49 | ||
|
|
d1d9011f4d | ||
|
|
9f476b2ce2 | ||
|
|
70ef40eb33 | ||
|
|
2e5016713a | ||
|
|
9a5e8a4b70 | ||
|
|
daa4b1dc65 | ||
|
|
c7aeb2160e | ||
|
|
4834390e6a | ||
|
|
bdd50c7553 | ||
|
|
999dca76c1 | ||
|
|
63e17ca546 | ||
|
|
a3bfbd0186 | ||
|
|
cc9fd88c0d | ||
|
|
96b0bcfd78 | ||
|
|
4ac0bb1bed | ||
|
|
92680b3fe4 | ||
|
|
4f5f8fd216 | ||
|
|
36f7a3f524 | ||
|
|
0fdcd8b27e | ||
|
|
20b1eabb9b | ||
|
|
b7ac697dce | ||
|
|
abf2943e4d | ||
|
|
43721f1206 | ||
|
|
0e8fa54f81 | ||
|
|
ad2998598f | ||
|
|
69d5f766a8 | ||
|
|
18c2376b50 | ||
|
|
39f1b714a5 | ||
|
|
0baac62099 | ||
|
|
046c096d86 | ||
|
|
de3af85ee1 | ||
|
|
cc1bb69671 | ||
|
|
30ca30370d | ||
|
|
710406aa76 | ||
|
|
ee3a56714e | ||
|
|
a095b08758 | ||
|
|
bfdab40aaf | ||
|
|
55d9d57112 | ||
|
|
f3e4347a8b | ||
|
|
d4351668ba | ||
|
|
ac66ae813f | ||
|
|
038ef32454 | ||
|
|
994de1f812 | ||
|
|
36f3cd1f70 | ||
|
|
ef9e5e523d |
@@ -15,14 +15,9 @@ body:
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: repro-steps
|
||||
attributes:
|
||||
label: How to reproduce the bug
|
||||
description: A clear description of what the bug is, including reproduction steps and expected behavior.
|
||||
placeholder: |
|
||||
The bug is that...
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
|
||||
@@ -162,7 +162,11 @@ cypress-run-all() {
|
||||
USE_DASHBOARD_FLAG='--use-dashboard'
|
||||
fi
|
||||
|
||||
python ../../scripts/cypress_run.py --parallelism $PARALLELISM --parallelism-id $PARALLEL_ID $USE_DASHBOARD_FLAG
|
||||
# UNCOMMENT the next few commands to monitor memory usage
|
||||
# monitor_memory & # Start memory monitoring in the background
|
||||
# memoryMonitorPid=$!
|
||||
python ../../scripts/cypress_run.py --parallelism $PARALLELISM --parallelism-id $PARALLEL_ID --group $PARALLEL_ID --retries 5 $USE_DASHBOARD_FLAG
|
||||
# kill $memoryMonitorPid
|
||||
|
||||
# After job is done, print out Flask log for debugging
|
||||
echo "::group::Flask log for default run"
|
||||
@@ -178,6 +182,21 @@ eyes-storybook-dependencies() {
|
||||
say "::endgroup::"
|
||||
}
|
||||
|
||||
monitor_memory() {
|
||||
# This is a small utility to monitor memory usage. Useful for debugging memory in GHA.
|
||||
# To use wrap your command as follows
|
||||
#
|
||||
# monitor_memory & # Start memory monitoring in the background
|
||||
# memoryMonitorPid=$!
|
||||
# YOUR_COMMAND_HERE
|
||||
# kill $memoryMonitorPid
|
||||
while true; do
|
||||
echo "$(date) - Top 5 memory-consuming processes:"
|
||||
ps -eo pid,comm,%mem --sort=-%mem | head -n 6 # First line is the header, next 5 are top processes
|
||||
sleep 2
|
||||
done
|
||||
}
|
||||
|
||||
cypress-run-applitools() {
|
||||
cd "$GITHUB_WORKSPACE/superset-frontend/cypress-base"
|
||||
|
||||
|
||||
@@ -48,7 +48,8 @@ jobs:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
REDIS_PORT: 16379
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
USE_DASHBOARD: ${{ github.event.inputs.use_dashboard || (github.ref == 'refs/heads/master' && 'true') || 'false' }}
|
||||
# use the dashboard feature when running manually OR merging to master
|
||||
USE_DASHBOARD: ${{ github.event.inputs.use_dashboard == 'true'|| (github.ref == 'refs/heads/master' && 'true') || 'false' }}
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
@@ -131,6 +132,7 @@ jobs:
|
||||
PARALLEL_ID: ${{ matrix.parallel_id }}
|
||||
PARALLELISM: 6
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
with:
|
||||
run: cypress-run-all ${{ env.USE_DASHBOARD }}
|
||||
- name: Upload Artifacts
|
||||
|
||||
+3
-3
@@ -80,9 +80,9 @@ If you believe someone is violating this code of conduct, you may reply to them
|
||||
|
||||
Or one of our volunteers:
|
||||
|
||||
* [Mark Thomas](http://home.apache.org/~markt/coc.html)
|
||||
* [Joan Touzet](http://home.apache.org/~wohali/)
|
||||
* [Sharan Foga](http://home.apache.org/~sharan/coc.html)
|
||||
* [Mark Thomas](https://www.linkedin.com/in/mark-thomas-b16751158/)
|
||||
* [Joan Touzet](https://www.apache.org/foundation/conduct-team/wohali.html)
|
||||
* [Sharan Foga](https://www.linkedin.com/in/sfoga/)
|
||||
|
||||
If the violation is in documentation or code, for example inappropriate pronoun usage or word choice within official documentation, we ask that people report these privately to the project in question at <private@project.apache.org>, and, if they have sufficient ability within the project, to resolve or remove the concerning material, being mindful of the perspective of the person originally reporting the issue.
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ Here are some of the major database solutions that are supported:
|
||||
<img src="https://superset.apache.org/img/databases/starrocks.png" alt="starrocks" border="0" width="200" />
|
||||
<img src="https://superset.apache.org/img/databases/doris.png" alt="doris" border="0" width="200" />
|
||||
<img src="https://superset.apache.org/img/databases/oceanbase.svg" alt="oceanbase" border="0" width="220" />
|
||||
<img src="https://superset.apache.org/img/databases/sap-hana.png" alt="oceanbase" border="0" width="220" />
|
||||
</p>
|
||||
|
||||
**A more comprehensive list of supported databases** along with the configuration instructions can be found [here](https://superset.apache.org/docs/configuration/databases).
|
||||
|
||||
@@ -137,4 +137,4 @@ There is now a [metadata bar](https://github.com/apache/superset/pull/27857) add
|
||||
|
||||
## Change to Docker image builds
|
||||
|
||||
Starting in 4.1.0, the release's docker image does not ship with drivers needed to operate Superset. Users may need to install a driver for their metadata database (MySQL or Postgres) as well as the driver for their data warehouse. This is a result of changes to the `lean` docker image that official releases come from; see [Docker Build Presets](/docs/installation/docker-builds/#build-presets) for more details.
|
||||
Starting in 4.1.0, the release's docker image does not ship with drivers needed to operate Superset. Users may need to install a driver for their metadata database (MySQL or Postgres) as well as the driver for their data warehouse. This is a result of changes to the `lean` docker image that official releases come from; see [Docker Build Presets](https://superset.apache.org/docs/installation/docker-builds/#build-presets) for more details.
|
||||
|
||||
@@ -80,7 +80,6 @@ Join our growing community!
|
||||
- [Caizin](https://caizin.com/) [@tejaskatariya]
|
||||
- [Careem](https://www.careem.com/) [@SamraHanifCareem]
|
||||
- [Cloudsmith](https://cloudsmith.io) [@alancarson]
|
||||
- [CnOvit](https://www.cnovit.com/) [@xieshaohu]
|
||||
- [Cyberhaven](https://www.cyberhaven.com/) [@toliver-ch]
|
||||
- [Deepomatic](https://deepomatic.com/) [@Zanoellia]
|
||||
- [Dial Once](https://www.dial-once.com/)
|
||||
|
||||
@@ -22,7 +22,11 @@ set -e
|
||||
#
|
||||
/app/docker/docker-bootstrap.sh
|
||||
|
||||
STEP_CNT=4
|
||||
if [ "$SUPERSET_LOAD_EXAMPLES" = "yes" ]; then
|
||||
STEP_CNT=4
|
||||
else
|
||||
STEP_CNT=3
|
||||
fi
|
||||
|
||||
echo_step() {
|
||||
cat <<EOF
|
||||
|
||||
@@ -16,6 +16,7 @@ KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
This is the public documentation site for Superset, built using
|
||||
[Docusaurus 2](https://docusaurus.io/). See
|
||||
[CONTRIBUTING.md](../CONTRIBUTING.md#documentation) for documentation on
|
||||
|
||||
@@ -13,7 +13,7 @@ SimpleCache (in-memory), or the local filesystem.
|
||||
[Custom cache backends](https://flask-caching.readthedocs.io/en/latest/#custom-cache-backends)
|
||||
are also supported.
|
||||
|
||||
Caching can be configured by providing a dictionaries in
|
||||
Caching can be configured by providing dictionaries in
|
||||
`superset_config.py` that comply with [the Flask-Caching config specifications](https://flask-caching.readthedocs.io/en/latest/#configuring-flask-caching).
|
||||
|
||||
The following cache configurations can be customized in this way:
|
||||
@@ -22,7 +22,7 @@ The following cache configurations can be customized in this way:
|
||||
- Metadata cache (optional): `CACHE_CONFIG`
|
||||
- Charting data queried from datasets (optional): `DATA_CACHE_CONFIG`
|
||||
|
||||
For example, to configure the filter state cache using redis:
|
||||
For example, to configure the filter state cache using Redis:
|
||||
|
||||
```python
|
||||
FILTER_STATE_CACHE_CONFIG = {
|
||||
|
||||
@@ -55,7 +55,7 @@ are compatible with Superset.
|
||||
| [ClickHouse](/docs/configuration/databases#clickhouse) | `pip install clickhouse-connect` | `clickhousedb://{username}:{password}@{hostname}:{port}/{database}` |
|
||||
| [CockroachDB](/docs/configuration/databases#cockroachdb) | `pip install cockroachdb` | `cockroachdb://root@{hostname}:{port}/{database}?sslmode=disable` |
|
||||
| [Couchbase](/docs/configuration/databases#couchbase) | `pip install couchbase-sqlalchemy` | `couchbase://{username}:{password}@{hostname}:{port}?truststorepath={ssl certificate path}` |
|
||||
| [Dremio](/docs/configuration/databases#dremio) | `pip install sqlalchemy_dremio` |`dremio+flight://{username}:{password}@{host}:31010, For ODBC dremio+pyodbc://{username}:{password}@{host}:32010` |
|
||||
| [Dremio](/docs/configuration/databases#dremio) | `pip install sqlalchemy_dremio` |`dremio+flight://{username}:{password}@{host}:32010`, often useful: `?UseEncryption=true/false`. For Legacy ODBC: `dremio+pyodbc://{username}:{password}@{host}:31010` |
|
||||
| [Elasticsearch](/docs/configuration/databases#elasticsearch) | `pip install elasticsearch-dbapi` | `elasticsearch+http://{user}:{password}@{host}:9200/` |
|
||||
| [Exasol](/docs/configuration/databases#exasol) | `pip install sqlalchemy-exasol` | `exa+pyodbc://{username}:{password}@{hostname}:{port}/my_schema?CONNECTIONLCALL=en_US.UTF-8&driver=EXAODBC` |
|
||||
| [Google BigQuery](/docs/configuration/databases#google-bigquery) | `pip install sqlalchemy-bigquery` | `bigquery://{project_id}` |
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
---
|
||||
title: Network and Security Settings
|
||||
sidebar_position: 7
|
||||
|
||||
+6
-3
@@ -174,13 +174,16 @@ You can take a look at this Flask-AppBuilder
|
||||
## Is there a way to force the dashboard to use specific colors?
|
||||
|
||||
It is possible on a per-dashboard basis by providing a mapping of labels to colors in the JSON
|
||||
Metadata attribute using the `label_colors` key.
|
||||
Metadata attribute using the `label_colors` key. You can use either the full hex color, a named color,
|
||||
like `red`, `coral` or `lightblue`, or the index in the current color palette (0 for first color, 1 for
|
||||
second etc). Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"label_colors": {
|
||||
"Girls": "#FF69B4",
|
||||
"Boys": "#ADD8E6"
|
||||
"foo": "#FF69B4",
|
||||
"bar": "lightblue",
|
||||
"baz": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -22,18 +22,18 @@ level dependencies.
|
||||
|
||||
**Debian and Ubuntu**
|
||||
|
||||
The following command will ensure that the required dependencies are installed:
|
||||
|
||||
```bash
|
||||
sudo apt-get install build-essential libssl-dev libffi-dev python-dev python-pip libsasl2-dev libldap2-dev default-libmysqlclient-dev
|
||||
```
|
||||
|
||||
In Ubuntu 20.04 the following command will ensure that the required dependencies are installed:
|
||||
In Ubuntu **20.04 and 22.04** the following command will ensure that the required dependencies are installed:
|
||||
|
||||
```bash
|
||||
sudo apt-get install build-essential libssl-dev libffi-dev python3-dev python3-pip libsasl2-dev libldap2-dev default-libmysqlclient-dev
|
||||
```
|
||||
|
||||
In Ubuntu **before 20.04** the following command will ensure that the required dependencies are installed:
|
||||
|
||||
```bash
|
||||
sudo apt-get install build-essential libssl-dev libffi-dev python-dev python-pip libsasl2-dev libldap2-dev default-libmysqlclient-dev
|
||||
```
|
||||
|
||||
**Fedora and RHEL-derivative Linux distributions**
|
||||
|
||||
Install the following packages using the `yum` package manager:
|
||||
|
||||
+3
-3
@@ -19,7 +19,7 @@
|
||||
"dependencies": {
|
||||
"@algolia/client-search": "^4.24.0",
|
||||
"@ant-design/icons": "^5.4.0",
|
||||
"@docsearch/react": "^3.6.1",
|
||||
"@docsearch/react": "^3.6.2",
|
||||
"@docusaurus/core": "^3.5.2",
|
||||
"@docusaurus/plugin-client-redirects": "^3.5.2",
|
||||
"@docusaurus/preset-classic": "^3.5.2",
|
||||
@@ -48,8 +48,8 @@
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.5.2",
|
||||
"@docusaurus/tsconfig": "^3.5.2",
|
||||
"@types/react": "^18.3.5",
|
||||
"typescript": "^5.5.4",
|
||||
"@types/react": "^18.3.10",
|
||||
"typescript": "^5.6.2",
|
||||
"webpack": "^5.94.0"
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
Vendored
+8
-1
@@ -116,7 +116,8 @@
|
||||
"GENERIC_BACKEND_ERROR",
|
||||
"INVALID_PAYLOAD_FORMAT_ERROR",
|
||||
"INVALID_PAYLOAD_SCHEMA_ERROR",
|
||||
"REPORT_NOTIFICATION_ERROR"
|
||||
"REPORT_NOTIFICATION_ERROR",
|
||||
"RESULT_TOO_LARGE_ERROR"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -3052,6 +3053,12 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"column_names": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"currency_formats": {
|
||||
"type": "object"
|
||||
},
|
||||
|
||||
+17
-17
@@ -1335,19 +1335,19 @@
|
||||
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
|
||||
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
||||
|
||||
"@docsearch/css@3.6.1":
|
||||
version "3.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.6.1.tgz#f0a728ecb486c81f2d282650fc1820c914913408"
|
||||
integrity sha512-VtVb5DS+0hRIprU2CO6ZQjK2Zg4QU5HrDM1+ix6rT0umsYvFvatMAnf97NHZlVWDaaLlx7GRfR/7FikANiM2Fg==
|
||||
"@docsearch/css@3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.6.2.tgz#ccd9c83dbfeaf34efe4e3547ee596714ae7e5891"
|
||||
integrity sha512-vKNZepO2j7MrYBTZIGXvlUOIR+v9KRf70FApRgovWrj3GTs1EITz/Xb0AOlm1xsQBp16clVZj1SY/qaOJbQtZw==
|
||||
|
||||
"@docsearch/react@^3.5.2", "@docsearch/react@^3.6.1":
|
||||
version "3.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.6.1.tgz#0f826df08693293806d64277d6d9c38636211b97"
|
||||
integrity sha512-qXZkEPvybVhSXj0K7U3bXc233tk5e8PfhoZ6MhPOiik/qUQxYC+Dn9DnoS7CxHQQhHfCvTiN0eY9M12oRghEXw==
|
||||
"@docsearch/react@^3.5.2", "@docsearch/react@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.6.2.tgz#32b16dd7d5614f0d39e6bc018549816b68d171b8"
|
||||
integrity sha512-rtZce46OOkVflCQH71IdbXSFK+S8iJZlUF56XBW5rIgx/eG5qoomC7Ag3anZson1bBac/JFQn7XOBfved/IMRA==
|
||||
dependencies:
|
||||
"@algolia/autocomplete-core" "1.9.3"
|
||||
"@algolia/autocomplete-preset-algolia" "1.9.3"
|
||||
"@docsearch/css" "3.6.1"
|
||||
"@docsearch/css" "3.6.2"
|
||||
algoliasearch "^4.19.1"
|
||||
|
||||
"@docusaurus/core@3.5.2", "@docusaurus/core@^3.5.2":
|
||||
@@ -2939,10 +2939,10 @@
|
||||
"@types/history" "^4.7.11"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^18.3.5":
|
||||
version "18.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.5.tgz#5f524c2ad2089c0ff372bbdabc77ca2c4dbadf8f"
|
||||
integrity sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==
|
||||
"@types/react@*", "@types/react@^18.3.10":
|
||||
version "18.3.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.10.tgz#6edc26dc22ff8c9c226d3c7bf8357b013c842219"
|
||||
integrity sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
csstype "^3.0.2"
|
||||
@@ -9984,10 +9984,10 @@ types-ramda@^0.30.0:
|
||||
dependencies:
|
||||
ts-toolbelt "^9.6.0"
|
||||
|
||||
typescript@^5.5.4:
|
||||
version "5.5.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
|
||||
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
|
||||
typescript@^5.6.2:
|
||||
version "5.6.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0"
|
||||
integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
|
||||
+3
-1
@@ -57,6 +57,7 @@ dependencies = [
|
||||
"geopy",
|
||||
"gunicorn>=22.0.0; sys_platform != 'win32'",
|
||||
"hashids>=1.3.1, <2",
|
||||
# known issue with holidays 0.26.0 and above related to prophet lib #25017
|
||||
"holidays>=0.25, <0.26",
|
||||
"humanize",
|
||||
"importlib_metadata",
|
||||
@@ -89,7 +90,7 @@ dependencies = [
|
||||
"slack_sdk>=3.19.0, <4",
|
||||
"sqlalchemy>=1.4, <2",
|
||||
"sqlalchemy-utils>=0.38.3, <0.39",
|
||||
"sqlglot>=23.0.2,<24",
|
||||
"sqlglot>=25.24.0,<26",
|
||||
"sqlparse>=0.5.0",
|
||||
"tabulate>=0.8.9, <0.9",
|
||||
"typing-extensions>=4, <5",
|
||||
@@ -445,6 +446,7 @@ select = [
|
||||
"E7",
|
||||
"E9",
|
||||
"F",
|
||||
"PT009",
|
||||
"TRY201",
|
||||
]
|
||||
ignore = []
|
||||
|
||||
@@ -350,7 +350,7 @@ sqlalchemy-utils==0.38.3
|
||||
# via
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
sqlglot==23.6.3
|
||||
sqlglot==25.24.0
|
||||
# via apache-superset
|
||||
sqlparse==0.5.0
|
||||
# via apache-superset
|
||||
|
||||
+78
-41
@@ -16,26 +16,18 @@
|
||||
# under the License.
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
XVFB_PRE_CMD = "xvfb-run --auto-servernum --server-args='-screen 0, 1024x768x24' "
|
||||
REPO = os.getenv("GITHUB_REPOSITORY") or "apache/superset"
|
||||
GITHUB_EVENT_NAME = os.getenv("GITHUB_REPOSITORY") or "push"
|
||||
GITHUB_EVENT_NAME = os.getenv("GITHUB_EVENT_NAME") or "push"
|
||||
CYPRESS_RECORD_KEY = os.getenv("CYPRESS_RECORD_KEY") or ""
|
||||
|
||||
|
||||
def compute_hash(file_path: str) -> str:
|
||||
return hashlib.md5(file_path.encode()).hexdigest()
|
||||
|
||||
|
||||
def compute_group_index(hash_value: str, num_groups: int) -> int:
|
||||
return int(hash_value, 16) % num_groups
|
||||
|
||||
|
||||
def generate_build_id() -> str:
|
||||
"""Generates a build ID based on the current timestamp."""
|
||||
now = datetime.now()
|
||||
rounded_minute = now.minute - (now.minute % 20)
|
||||
rounded_time = now.replace(minute=rounded_minute, second=0, microsecond=0)
|
||||
@@ -44,42 +36,72 @@ def generate_build_id() -> str:
|
||||
)
|
||||
|
||||
|
||||
def get_cypress_cmd(
|
||||
spec_list: list[str], _filter: str, group: str, use_dashboard: bool
|
||||
) -> str:
|
||||
def run_cypress_for_test_file(
|
||||
test_file: str, retries: int, use_dashboard: bool, group: str, dry_run: bool, i: int
|
||||
) -> int:
|
||||
"""Runs Cypress for a single test file and retries upon failure."""
|
||||
cypress_cmd = "./node_modules/.bin/cypress run"
|
||||
|
||||
os.environ["TERM"] = "xterm"
|
||||
os.environ["ELECTRON_DISABLE_GPU"] = "true"
|
||||
build_id = generate_build_id()
|
||||
browser = os.getenv("CYPRESS_BROWSER", "chrome")
|
||||
chrome_flags = "--disable-dev-shm-usage"
|
||||
|
||||
if use_dashboard:
|
||||
# Run using cypress.io service
|
||||
spec: str = "cypress/e2e/*/**/*"
|
||||
cmd = (
|
||||
f"{XVFB_PRE_CMD} "
|
||||
f'{cypress_cmd} --spec "{spec}" --browser {browser} '
|
||||
f"--record --group {group} --tag {REPO},{GITHUB_EVENT_NAME} "
|
||||
f"--parallel --ci-build-id {build_id}"
|
||||
for attempt in range(retries):
|
||||
# Create Cypress command for a single test file
|
||||
cmd: str = ""
|
||||
if use_dashboard:
|
||||
# If/when we want to use cypress' dashboard feature to record the run
|
||||
group_id = f"matrix{group}-file{i}-{attempt}"
|
||||
cmd = (
|
||||
f"{XVFB_PRE_CMD} "
|
||||
f'{cypress_cmd} --spec "{test_file}" --browser {browser} '
|
||||
f"--record --group {group_id} --tag {REPO},{GITHUB_EVENT_NAME} "
|
||||
f"--ci-build-id {build_id} "
|
||||
f"-- {chrome_flags}"
|
||||
)
|
||||
else:
|
||||
os.environ.pop("CYPRESS_RECORD_KEY", None)
|
||||
cmd = (
|
||||
f"{XVFB_PRE_CMD} "
|
||||
f"{cypress_cmd} --browser {browser} "
|
||||
f'--spec "{test_file}" '
|
||||
f"-- {chrome_flags}"
|
||||
)
|
||||
print(f"RUN: {cmd} (Attempt {attempt + 1}/{retries})")
|
||||
if dry_run:
|
||||
# Print the command instead of executing it
|
||||
print(f"DRY RUN: {cmd}")
|
||||
return 0
|
||||
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True,
|
||||
)
|
||||
else:
|
||||
# Run local, but split the execution
|
||||
os.environ.pop("CYPRESS_RECORD_KEY", None)
|
||||
spec_list_str = ",".join(sorted(spec_list))
|
||||
if _filter:
|
||||
spec_list_str = ",".join(sorted([s for s in spec_list if _filter in s]))
|
||||
cmd = (
|
||||
f"{XVFB_PRE_CMD} "
|
||||
f"{cypress_cmd} --browser {browser} "
|
||||
f'--spec "{spec_list_str}" '
|
||||
)
|
||||
return cmd
|
||||
|
||||
# Stream stdout in real-time
|
||||
if process.stdout:
|
||||
for stdout_line in iter(process.stdout.readline, ""):
|
||||
print(stdout_line, end="")
|
||||
|
||||
process.wait()
|
||||
|
||||
if process.returncode == 0:
|
||||
print(f"Test {test_file} succeeded on attempt {attempt + 1}")
|
||||
return 0
|
||||
else:
|
||||
print(f"Test {test_file} failed on attempt {attempt + 1}")
|
||||
|
||||
print(f"Test {test_file} failed after {retries} retries.")
|
||||
return process.returncode
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate Cypress commands based on test file hash"
|
||||
description="Run Cypress tests with retries per test file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use-dashboard",
|
||||
@@ -93,9 +115,12 @@ def main() -> None:
|
||||
"--parallelism-id", type=int, required=True, help="ID of the parallelism group"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--filter", type=str, required=False, default=None, help="filter to test"
|
||||
"--filter", type=str, required=False, default=None, help="Filter to test"
|
||||
)
|
||||
parser.add_argument("--group", type=str, default="Default", help="Group name")
|
||||
parser.add_argument(
|
||||
"--retries", type=int, default=3, help="Number of retries per test file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
@@ -109,14 +134,17 @@ def main() -> None:
|
||||
cypress_tests_path = os.path.join(cypress_base_full_path, "cypress/e2e")
|
||||
|
||||
test_files = []
|
||||
file_count = 0
|
||||
for root, _, files in os.walk(cypress_tests_path):
|
||||
for file in files:
|
||||
if file.endswith("test.ts") or file.endswith("test.js"):
|
||||
file_count += 1
|
||||
test_files.append(
|
||||
os.path.join(root, file).replace(cypress_base_full_path, "")
|
||||
)
|
||||
print(f"Found {file_count} test files.")
|
||||
|
||||
# Initialize groups
|
||||
# Initialize groups for round-robin distribution
|
||||
groups: dict[int, list[str]] = {i: [] for i in range(args.parallelism)}
|
||||
|
||||
# Sort test files to ensure deterministic distribution
|
||||
@@ -127,12 +155,21 @@ def main() -> None:
|
||||
group_index = index % args.parallelism
|
||||
groups[group_index].append(test_file)
|
||||
|
||||
# Only run tests for the group that matches the parallelism ID
|
||||
group_id = args.parallelism_id
|
||||
spec_list = groups[group_id]
|
||||
cmd = get_cypress_cmd(spec_list, args.filter, args.group, args.use_dashboard)
|
||||
print(f"RUN: {cmd}")
|
||||
if not args.dry_run:
|
||||
subprocess.run(cmd, shell=True, check=True, stdout=None, stderr=None)
|
||||
|
||||
# Run each test file independently with retry logic or dry-run
|
||||
processed_file_count: int = 0
|
||||
for i, test_file in enumerate(spec_list):
|
||||
result = run_cypress_for_test_file(
|
||||
test_file, args.retries, args.use_dashboard, args.group, args.dry_run, i
|
||||
)
|
||||
if result != 0:
|
||||
print(f"Exiting due to failure in {test_file}")
|
||||
exit(result)
|
||||
processed_file_count += 1
|
||||
print(f"Ran {processed_file_count} test files successfully.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* 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 { nativeFilters } from 'cypress/support/directories';
|
||||
|
||||
import {
|
||||
addCountryNameFilter,
|
||||
applyNativeFilterValueWithIndex,
|
||||
enterNativeFilterEditModal,
|
||||
inputNativeFilterDefaultValue,
|
||||
saveNativeFilterSettings,
|
||||
validateFilterNameOnDashboard,
|
||||
testItems,
|
||||
interceptFilterState,
|
||||
} from './utils';
|
||||
import {
|
||||
prepareDashboardFilters,
|
||||
SAMPLE_CHART,
|
||||
visitDashboard,
|
||||
} from './shared_dashboard_functions';
|
||||
|
||||
function openMoreFilters(waitFilterState = true) {
|
||||
interceptFilterState();
|
||||
cy.getBySel('dropdown-container-btn').click();
|
||||
|
||||
if (waitFilterState) {
|
||||
cy.wait('@postFilterState');
|
||||
}
|
||||
}
|
||||
|
||||
function openVerticalFilterBar() {
|
||||
cy.getBySel('dashboard-filters-panel').should('exist');
|
||||
cy.getBySel('filter-bar__expand-button').click();
|
||||
}
|
||||
|
||||
function setFilterBarOrientation(orientation: 'vertical' | 'horizontal') {
|
||||
cy.getBySel('filterbar-orientation-icon').click();
|
||||
cy.wait(250);
|
||||
cy.getBySel('dropdown-selectable-icon-submenu')
|
||||
.contains('Orientation of filter bar')
|
||||
.should('exist')
|
||||
.trigger('mouseover');
|
||||
|
||||
if (orientation === 'vertical') {
|
||||
cy.get('.ant-dropdown-menu-item-selected')
|
||||
.contains('Horizontal (Top)')
|
||||
.should('exist');
|
||||
cy.get('.ant-dropdown-menu-item').contains('Vertical (Left)').click();
|
||||
cy.getBySel('dashboard-filters-panel').should('exist');
|
||||
} else {
|
||||
cy.get('.ant-dropdown-menu-item-selected')
|
||||
.contains('Vertical (Left)')
|
||||
.should('exist');
|
||||
cy.get('.ant-dropdown-menu-item').contains('Horizontal (Top)').click();
|
||||
cy.getBySel('loading-indicator').should('exist');
|
||||
cy.getBySel('filter-bar').should('exist');
|
||||
cy.getBySel('dashboard-filters-panel').should('not.exist');
|
||||
}
|
||||
}
|
||||
|
||||
describe('Horizontal FilterBar', () => {
|
||||
it('should go from vertical to horizontal and the opposite', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
setFilterBarOrientation('vertical');
|
||||
});
|
||||
|
||||
it('should show all default actions in horizontal mode', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.getBySel('horizontal-filterbar-empty')
|
||||
.contains('No filters are currently added to this dashboard.')
|
||||
.should('exist');
|
||||
cy.getBySel('filter-bar__create-filter').should('exist');
|
||||
cy.getBySel('filterbar-action-buttons').should('exist');
|
||||
});
|
||||
|
||||
it('should stay in horizontal mode when reloading', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.reload();
|
||||
cy.getBySel('dashboard-filters-panel').should('not.exist');
|
||||
});
|
||||
|
||||
it('should show all filters in available space on load', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.get('.filter-item-wrapper').should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should show "more filters" on window resizing up and down', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
|
||||
cy.getBySel('form-item-value').should('have.length', 3);
|
||||
cy.viewport(768, 1024);
|
||||
cy.getBySel('form-item-value').should('have.length', 0);
|
||||
openMoreFilters(false);
|
||||
cy.getBySel('form-item-value').should('have.length', 3);
|
||||
|
||||
cy.getBySel('filter-bar').click();
|
||||
cy.viewport(1000, 1024);
|
||||
openMoreFilters(false);
|
||||
cy.getBySel('form-item-value').should('have.length', 3);
|
||||
|
||||
cy.getBySel('filter-bar').click();
|
||||
cy.viewport(1300, 1024);
|
||||
cy.getBySel('form-item-value').should('have.length', 3);
|
||||
cy.getBySel('dropdown-container-btn').should('not.exist');
|
||||
});
|
||||
|
||||
it('should show "more filters" and scroll', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_4', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_5', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_6', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_7', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_8', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_9', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_10', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_11', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_12', column: 'year', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.get('.filter-item-wrapper').should('have.length', 3);
|
||||
openMoreFilters();
|
||||
cy.getBySel('form-item-value').should('have.length', 12);
|
||||
cy.getBySel('filter-control-name').contains('test_10').should('be.visible');
|
||||
cy.getBySel('filter-control-name')
|
||||
.contains('test_12')
|
||||
.should('not.be.visible');
|
||||
cy.get('.ant-popover-inner-content').scrollTo('bottom');
|
||||
cy.getBySel('filter-control-name').contains('test_12').should('be.visible');
|
||||
});
|
||||
|
||||
it('should display newly added filter', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
|
||||
enterNativeFilterEditModal(false);
|
||||
addCountryNameFilter();
|
||||
saveNativeFilterSettings([]);
|
||||
validateFilterNameOnDashboard(testItems.topTenChart.filterColumn);
|
||||
});
|
||||
|
||||
it('should spot changes in "more filters" and apply their values', () => {
|
||||
cy.intercept(`/api/v1/chart/data?form_data=**`).as('chart');
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_4', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_5', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_6', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_7', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_8', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_9', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_10', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_11', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_12', column: 'year', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
openMoreFilters();
|
||||
applyNativeFilterValueWithIndex(8, testItems.filterDefaultValue);
|
||||
cy.get(nativeFilters.applyFilter).click({ force: true });
|
||||
cy.wait('@chart');
|
||||
cy.get('.antd5-scroll-number.antd5-badge-count').should(
|
||||
'have.attr',
|
||||
'title',
|
||||
'1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should focus filter and open "more filters" programmatically', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_4', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_5', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_6', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_7', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_8', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_9', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_10', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_11', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_12', column: 'year', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
openMoreFilters();
|
||||
applyNativeFilterValueWithIndex(8, testItems.filterDefaultValue);
|
||||
cy.get(nativeFilters.applyFilter).click({ force: true });
|
||||
cy.getBySel('slice-header').within(() => {
|
||||
cy.get('.filter-counts').trigger('mouseover');
|
||||
});
|
||||
cy.get('.filterStatusPopover').contains('test_9').click();
|
||||
cy.getBySel('dropdown-content').should('be.visible');
|
||||
cy.get('.ant-select-focused').should('be.visible');
|
||||
});
|
||||
|
||||
it('should show tag count and one plain tag on focus and only count on blur in select ', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
enterNativeFilterEditModal();
|
||||
inputNativeFilterDefaultValue('Albania');
|
||||
cy.get('.ant-select-selection-search-input').clear({ force: true });
|
||||
inputNativeFilterDefaultValue('Algeria', true);
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
cy.getBySel('filter-bar').within(() => {
|
||||
cy.get(nativeFilters.filterItem).contains('Albania').should('be.visible');
|
||||
cy.get(nativeFilters.filterItem).contains('+ 1 ...').should('be.visible');
|
||||
cy.get('.ant-select-selection-search-input').click();
|
||||
cy.get(nativeFilters.filterItem).contains('+ 2 ...').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 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 qs from 'querystring';
|
||||
import {
|
||||
dashboardView,
|
||||
nativeFilters,
|
||||
exploreView,
|
||||
dataTestChartName,
|
||||
} from 'cypress/support/directories';
|
||||
|
||||
import {
|
||||
addCountryNameFilter,
|
||||
addParentFilterWithValue,
|
||||
applyAdvancedTimeRangeFilterOnDashboard,
|
||||
applyNativeFilterValueWithIndex,
|
||||
cancelNativeFilterSettings,
|
||||
checkNativeFilterTooltip,
|
||||
clickOnAddFilterInModal,
|
||||
collapseFilterOnLeftPanel,
|
||||
deleteNativeFilter,
|
||||
enterNativeFilterEditModal,
|
||||
expandFilterOnLeftPanel,
|
||||
fillNativeFilterForm,
|
||||
getNativeFilterPlaceholderWithIndex,
|
||||
inputNativeFilterDefaultValue,
|
||||
saveNativeFilterSettings,
|
||||
nativeFilterTooltips,
|
||||
undoDeleteNativeFilter,
|
||||
validateFilterContentOnDashboard,
|
||||
valueNativeFilterOptions,
|
||||
validateFilterNameOnDashboard,
|
||||
testItems,
|
||||
WORLD_HEALTH_CHARTS,
|
||||
} from './utils';
|
||||
import {
|
||||
prepareDashboardFilters,
|
||||
SAMPLE_CHART,
|
||||
visitDashboard,
|
||||
} from './shared_dashboard_functions';
|
||||
|
||||
function selectFilter(index: number) {
|
||||
cy.get("[data-test='filter-title-container'] [draggable='true']")
|
||||
.eq(index)
|
||||
.click();
|
||||
}
|
||||
|
||||
function closeFilterModal() {
|
||||
cy.get('body').then($body => {
|
||||
if ($body.find('[data-test="native-filter-modal-cancel-button"]').length) {
|
||||
cy.getBySel('native-filter-modal-cancel-button').click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('Native filters', () => {
|
||||
describe('Nativefilters initial state not required', () => {
|
||||
it("User can check 'Filter has default value'", () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_name', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
inputNativeFilterDefaultValue(testItems.filterDefaultValue);
|
||||
});
|
||||
|
||||
it('User can add a new native filter', () => {
|
||||
prepareDashboardFilters([]);
|
||||
|
||||
let filterKey: string;
|
||||
const removeFirstChar = (search: string) =>
|
||||
search.split('').slice(1, search.length).join('');
|
||||
|
||||
cy.location().then(loc => {
|
||||
cy.url().should('contain', 'native_filters_key');
|
||||
const queryParams = qs.parse(removeFirstChar(loc.search));
|
||||
filterKey = queryParams.native_filters_key as string;
|
||||
expect(typeof filterKey).eq('string');
|
||||
});
|
||||
enterNativeFilterEditModal();
|
||||
addCountryNameFilter();
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
cy.location().then(loc => {
|
||||
cy.url().should('contain', 'native_filters_key');
|
||||
const queryParams = qs.parse(removeFirstChar(loc.search));
|
||||
const newfilterKey = queryParams.native_filters_key;
|
||||
expect(newfilterKey).eq(filterKey);
|
||||
});
|
||||
cy.get(nativeFilters.modal.container).should('not.exist');
|
||||
});
|
||||
|
||||
it('User can restore a deleted native filter', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_code', column: 'country_code', datasetId: 2 },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
cy.get(nativeFilters.filtersList.removeIcon).first().click();
|
||||
cy.get('[data-test="restore-filter-button"]')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.get(nativeFilters.modal.container)
|
||||
.find(nativeFilters.filtersPanel.filterName)
|
||||
.should(
|
||||
'have.attr',
|
||||
'value',
|
||||
testItems.topTenChart.filterColumnCountryCode,
|
||||
);
|
||||
});
|
||||
|
||||
it('User can create a time grain filter', () => {
|
||||
prepareDashboardFilters([]);
|
||||
enterNativeFilterEditModal();
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.timeGrain,
|
||||
testItems.filterType.timeGrain,
|
||||
testItems.datasetForNativeFilter,
|
||||
);
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
applyNativeFilterValueWithIndex(0, testItems.filterTimeGrain);
|
||||
cy.get(nativeFilters.applyFilter).click();
|
||||
cy.url().then(u => {
|
||||
const ur = new URL(u);
|
||||
expect(ur.search).to.include('native_filters');
|
||||
});
|
||||
validateFilterNameOnDashboard(testItems.filterType.timeGrain);
|
||||
validateFilterContentOnDashboard(testItems.filterTimeGrain);
|
||||
});
|
||||
|
||||
it.skip('User can create a time range filter', () => {
|
||||
enterNativeFilterEditModal();
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.timeRange,
|
||||
testItems.filterType.timeRange,
|
||||
);
|
||||
saveNativeFilterSettings(WORLD_HEALTH_CHARTS);
|
||||
cy.get(dashboardView.salesDashboardSpecific.vehicleSalesFilterTimeRange)
|
||||
.should('be.visible')
|
||||
.click();
|
||||
applyAdvancedTimeRangeFilterOnDashboard('2005-12-17', '2006-12-17');
|
||||
cy.url().then(u => {
|
||||
const ur = new URL(u);
|
||||
expect(ur.search).to.include('native_filters');
|
||||
});
|
||||
validateFilterNameOnDashboard(testItems.filterType.timeRange);
|
||||
cy.get(nativeFilters.filterFromDashboardView.timeRangeFilterContent)
|
||||
.contains('2005-12-17')
|
||||
.should('be.visible');
|
||||
});
|
||||
|
||||
it.skip('User can create a time column filter', () => {
|
||||
enterNativeFilterEditModal();
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.timeColumn,
|
||||
testItems.filterType.timeColumn,
|
||||
testItems.datasetForNativeFilter,
|
||||
);
|
||||
saveNativeFilterSettings(WORLD_HEALTH_CHARTS);
|
||||
cy.intercept(`/api/v1/chart/data?form_data=**`).as('chart');
|
||||
cy.get(nativeFilters.modal.container).should('not.exist');
|
||||
// assert that native filter is created
|
||||
validateFilterNameOnDashboard(testItems.filterType.timeColumn);
|
||||
applyNativeFilterValueWithIndex(
|
||||
0,
|
||||
testItems.topTenChart.filterColumnYear,
|
||||
);
|
||||
cy.get(nativeFilters.applyFilter).click({ force: true });
|
||||
cy.wait('@chart');
|
||||
validateFilterContentOnDashboard(testItems.topTenChart.filterColumnYear);
|
||||
});
|
||||
|
||||
it('User can create a numerical range filter', () => {
|
||||
visitDashboard();
|
||||
enterNativeFilterEditModal(false);
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.numerical,
|
||||
testItems.filterNumericalColumn,
|
||||
testItems.datasetForNativeFilter,
|
||||
testItems.filterNumericalColumn,
|
||||
);
|
||||
saveNativeFilterSettings([]);
|
||||
// assertions
|
||||
cy.get(nativeFilters.slider.slider).should('be.visible').click('center');
|
||||
cy.get(nativeFilters.applyFilter).click();
|
||||
// assert that the url contains 'native_filters' in the url
|
||||
cy.url().then(u => {
|
||||
const ur = new URL(u);
|
||||
expect(ur.search).to.include('native_filters');
|
||||
// assert that the start handle has a value
|
||||
cy.get(nativeFilters.slider.startHandle)
|
||||
.invoke('attr', 'aria-valuenow')
|
||||
.should('exist');
|
||||
// assert that the end handle has a value
|
||||
cy.get(nativeFilters.slider.endHandle)
|
||||
.invoke('attr', 'aria-valuenow')
|
||||
.should('exist');
|
||||
// assert slider text matches what we should have
|
||||
cy.get(nativeFilters.slider.sliderText).should('have.text', '49');
|
||||
});
|
||||
});
|
||||
|
||||
it('User can undo deleting a native filter', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_name', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
undoDeleteNativeFilter();
|
||||
cy.get(nativeFilters.modal.container)
|
||||
.find(nativeFilters.filtersPanel.filterName)
|
||||
.should('have.attr', 'value', testItems.topTenChart.filterColumn);
|
||||
});
|
||||
|
||||
it('User can cancel changes in native filter', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_name', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
cy.getBySel('filters-config-modal__name-input').type('|EDITED', {
|
||||
force: true,
|
||||
});
|
||||
cancelNativeFilterSettings();
|
||||
enterNativeFilterEditModal(false);
|
||||
cy.get(nativeFilters.filtersList.removeIcon).first().click();
|
||||
cy.contains('You have removed this filter.').should('be.visible');
|
||||
});
|
||||
|
||||
it('User can create a value filter', () => {
|
||||
visitDashboard();
|
||||
enterNativeFilterEditModal(false);
|
||||
addCountryNameFilter();
|
||||
cy.get(nativeFilters.filtersPanel.filterTypeInput)
|
||||
.find(nativeFilters.filtersPanel.filterTypeItem)
|
||||
.should('have.text', testItems.filterType.value);
|
||||
saveNativeFilterSettings([]);
|
||||
validateFilterNameOnDashboard(testItems.topTenChart.filterColumn);
|
||||
});
|
||||
|
||||
it('User can apply value filter with selected values', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_name', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
applyNativeFilterValueWithIndex(0, testItems.filterDefaultValue);
|
||||
cy.get(nativeFilters.applyFilter).click();
|
||||
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
|
||||
cy.contains(testItems.filterDefaultValue).should('be.visible');
|
||||
cy.contains(testItems.filterOtherCountry).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
it('User can stop filtering when filter is removed', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_name', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
inputNativeFilterDefaultValue(testItems.filterDefaultValue);
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
|
||||
cy.contains(testItems.filterDefaultValue).should('be.visible');
|
||||
cy.contains(testItems.filterOtherCountry).should('not.exist');
|
||||
});
|
||||
cy.get(nativeFilters.filterItem)
|
||||
.contains(testItems.filterDefaultValue)
|
||||
.should('be.visible');
|
||||
validateFilterNameOnDashboard(testItems.topTenChart.filterColumn);
|
||||
enterNativeFilterEditModal(false);
|
||||
deleteNativeFilter();
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
|
||||
cy.contains(testItems.filterDefaultValue).should('be.visible');
|
||||
cy.contains(testItems.filterOtherCountry).should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
exploreView,
|
||||
dataTestChartName,
|
||||
} from 'cypress/support/directories';
|
||||
import { SAMPLE_DASHBOARD_1 } from 'cypress/utils/urls';
|
||||
|
||||
import {
|
||||
addCountryNameFilter,
|
||||
@@ -48,138 +47,12 @@ import {
|
||||
validateFilterNameOnDashboard,
|
||||
testItems,
|
||||
WORLD_HEALTH_CHARTS,
|
||||
interceptGet,
|
||||
interceptCharts,
|
||||
interceptDatasets,
|
||||
interceptFilterState,
|
||||
} from './utils';
|
||||
|
||||
const SAMPLE_CHART = { name: 'Most Populated Countries', viz: 'table' };
|
||||
|
||||
function visitDashboard(createSample = true) {
|
||||
interceptCharts();
|
||||
interceptGet();
|
||||
interceptDatasets();
|
||||
|
||||
if (createSample) {
|
||||
cy.createSampleDashboards([0]);
|
||||
}
|
||||
|
||||
cy.visit(SAMPLE_DASHBOARD_1);
|
||||
cy.wait('@get');
|
||||
cy.wait('@getCharts');
|
||||
cy.wait('@getDatasets');
|
||||
cy.url().should('contain', 'native_filters_key');
|
||||
}
|
||||
|
||||
function prepareDashboardFilters(
|
||||
filters: { name: string; column: string; datasetId: number }[],
|
||||
) {
|
||||
cy.createSampleDashboards([0]);
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: `api/v1/dashboard/1-sample-dashboard`,
|
||||
}).then(res => {
|
||||
const { body } = res;
|
||||
const dashboardId = body.result.id;
|
||||
const allFilters: Record<string, unknown>[] = [];
|
||||
filters.forEach((f, i) => {
|
||||
allFilters.push({
|
||||
id: `NATIVE_FILTER-fLH0pxFQ${i}`,
|
||||
controlValues: {
|
||||
enableEmptyFilter: false,
|
||||
defaultToFirstItem: false,
|
||||
multiSelect: true,
|
||||
searchAllOptions: false,
|
||||
inverseSelection: false,
|
||||
},
|
||||
name: f.name,
|
||||
filterType: 'filter_select',
|
||||
targets: [
|
||||
{
|
||||
datasetId: f.datasetId,
|
||||
column: { name: f.column },
|
||||
},
|
||||
],
|
||||
defaultDataMask: {
|
||||
extraFormData: {},
|
||||
filterState: {},
|
||||
ownState: {},
|
||||
},
|
||||
cascadeParentIds: [],
|
||||
scope: {
|
||||
rootPath: ['ROOT_ID'],
|
||||
excluded: [],
|
||||
},
|
||||
type: 'NATIVE_FILTER',
|
||||
description: '',
|
||||
chartsInScope: [5],
|
||||
tabsInScope: [],
|
||||
});
|
||||
});
|
||||
if (dashboardId) {
|
||||
const jsonMetadata = {
|
||||
native_filter_configuration: allFilters,
|
||||
timed_refresh_immune_slices: [],
|
||||
expanded_slices: {},
|
||||
refresh_frequency: 0,
|
||||
color_scheme: '',
|
||||
label_colors: {},
|
||||
shared_label_colors: {},
|
||||
color_scheme_domain: [],
|
||||
cross_filters_enabled: false,
|
||||
positions: {
|
||||
DASHBOARD_VERSION_KEY: 'v2',
|
||||
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: ['GRID_ID'] },
|
||||
GRID_ID: {
|
||||
type: 'GRID',
|
||||
id: 'GRID_ID',
|
||||
children: ['ROW-0rHnUz4nMA'],
|
||||
parents: ['ROOT_ID'],
|
||||
},
|
||||
HEADER_ID: {
|
||||
id: 'HEADER_ID',
|
||||
type: 'HEADER',
|
||||
meta: { text: '1 - Sample dashboard' },
|
||||
},
|
||||
'CHART-DF6EfI55F-': {
|
||||
type: 'CHART',
|
||||
id: 'CHART-DF6EfI55F-',
|
||||
children: [],
|
||||
parents: ['ROOT_ID', 'GRID_ID', 'ROW-0rHnUz4nMA'],
|
||||
meta: {
|
||||
width: 4,
|
||||
height: 50,
|
||||
chartId: 5,
|
||||
sliceName: 'Most Populated Countries',
|
||||
},
|
||||
},
|
||||
'ROW-0rHnUz4nMA': {
|
||||
type: 'ROW',
|
||||
id: 'ROW-0rHnUz4nMA',
|
||||
children: ['CHART-DF6EfI55F-'],
|
||||
parents: ['ROOT_ID', 'GRID_ID'],
|
||||
meta: { background: 'BACKGROUND_TRANSPARENT' },
|
||||
},
|
||||
},
|
||||
default_filters: '{}',
|
||||
filter_scopes: {},
|
||||
chart_configuration: {},
|
||||
};
|
||||
|
||||
return cy
|
||||
.request({
|
||||
method: 'PUT',
|
||||
url: `api/v1/dashboard/${dashboardId}`,
|
||||
body: {
|
||||
json_metadata: JSON.stringify(jsonMetadata),
|
||||
},
|
||||
})
|
||||
.then(() => visitDashboard(false));
|
||||
}
|
||||
return cy;
|
||||
});
|
||||
}
|
||||
import {
|
||||
prepareDashboardFilters,
|
||||
SAMPLE_CHART,
|
||||
visitDashboard,
|
||||
} from './shared_dashboard_functions';
|
||||
|
||||
function selectFilter(index: number) {
|
||||
cy.get("[data-test='filter-title-container'] [draggable='true']")
|
||||
@@ -195,219 +68,6 @@ function closeFilterModal() {
|
||||
});
|
||||
}
|
||||
|
||||
function openVerticalFilterBar() {
|
||||
cy.getBySel('dashboard-filters-panel').should('exist');
|
||||
cy.getBySel('filter-bar__expand-button').click();
|
||||
}
|
||||
|
||||
function setFilterBarOrientation(orientation: 'vertical' | 'horizontal') {
|
||||
cy.getBySel('filterbar-orientation-icon').click();
|
||||
cy.wait(250);
|
||||
cy.getBySel('dropdown-selectable-icon-submenu')
|
||||
.contains('Orientation of filter bar')
|
||||
.should('exist')
|
||||
.trigger('mouseover');
|
||||
|
||||
if (orientation === 'vertical') {
|
||||
cy.get('.ant-dropdown-menu-item-selected')
|
||||
.contains('Horizontal (Top)')
|
||||
.should('exist');
|
||||
cy.get('.ant-dropdown-menu-item').contains('Vertical (Left)').click();
|
||||
cy.getBySel('dashboard-filters-panel').should('exist');
|
||||
} else {
|
||||
cy.get('.ant-dropdown-menu-item-selected')
|
||||
.contains('Vertical (Left)')
|
||||
.should('exist');
|
||||
cy.get('.ant-dropdown-menu-item').contains('Horizontal (Top)').click();
|
||||
cy.getBySel('loading-indicator').should('exist');
|
||||
cy.getBySel('filter-bar').should('exist');
|
||||
cy.getBySel('dashboard-filters-panel').should('not.exist');
|
||||
}
|
||||
}
|
||||
|
||||
function openMoreFilters(waitFilterState = true) {
|
||||
interceptFilterState();
|
||||
cy.getBySel('dropdown-container-btn').click();
|
||||
|
||||
if (waitFilterState) {
|
||||
cy.wait('@postFilterState');
|
||||
}
|
||||
}
|
||||
|
||||
describe('Horizontal FilterBar', () => {
|
||||
it('should go from vertical to horizontal and the opposite', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
setFilterBarOrientation('vertical');
|
||||
});
|
||||
|
||||
it('should show all default actions in horizontal mode', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.getBySel('horizontal-filterbar-empty')
|
||||
.contains('No filters are currently added to this dashboard.')
|
||||
.should('exist');
|
||||
cy.getBySel('filter-bar__create-filter').should('exist');
|
||||
cy.getBySel('filterbar-action-buttons').should('exist');
|
||||
});
|
||||
|
||||
it('should stay in horizontal mode when reloading', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.reload();
|
||||
cy.getBySel('dashboard-filters-panel').should('not.exist');
|
||||
});
|
||||
|
||||
it('should show all filters in available space on load', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.get('.filter-item-wrapper').should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should show "more filters" on window resizing up and down', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
|
||||
cy.getBySel('form-item-value').should('have.length', 3);
|
||||
cy.viewport(768, 1024);
|
||||
cy.getBySel('form-item-value').should('have.length', 0);
|
||||
openMoreFilters(false);
|
||||
cy.getBySel('form-item-value').should('have.length', 3);
|
||||
|
||||
cy.getBySel('filter-bar').click();
|
||||
cy.viewport(1000, 1024);
|
||||
openMoreFilters(false);
|
||||
cy.getBySel('form-item-value').should('have.length', 3);
|
||||
|
||||
cy.getBySel('filter-bar').click();
|
||||
cy.viewport(1300, 1024);
|
||||
cy.getBySel('form-item-value').should('have.length', 3);
|
||||
cy.getBySel('dropdown-container-btn').should('not.exist');
|
||||
});
|
||||
|
||||
it('should show "more filters" and scroll', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_4', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_5', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_6', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_7', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_8', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_9', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_10', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_11', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_12', column: 'year', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.get('.filter-item-wrapper').should('have.length', 3);
|
||||
openMoreFilters();
|
||||
cy.getBySel('form-item-value').should('have.length', 12);
|
||||
cy.getBySel('filter-control-name').contains('test_10').should('be.visible');
|
||||
cy.getBySel('filter-control-name')
|
||||
.contains('test_12')
|
||||
.should('not.be.visible');
|
||||
cy.get('.ant-popover-inner-content').scrollTo('bottom');
|
||||
cy.getBySel('filter-control-name').contains('test_12').should('be.visible');
|
||||
});
|
||||
|
||||
it('should display newly added filter', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
|
||||
enterNativeFilterEditModal(false);
|
||||
addCountryNameFilter();
|
||||
saveNativeFilterSettings([]);
|
||||
validateFilterNameOnDashboard(testItems.topTenChart.filterColumn);
|
||||
});
|
||||
|
||||
it('should spot changes in "more filters" and apply their values', () => {
|
||||
cy.intercept(`/api/v1/chart/data?form_data=**`).as('chart');
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_4', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_5', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_6', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_7', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_8', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_9', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_10', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_11', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_12', column: 'year', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
openMoreFilters();
|
||||
applyNativeFilterValueWithIndex(8, testItems.filterDefaultValue);
|
||||
cy.get(nativeFilters.applyFilter).click({ force: true });
|
||||
cy.wait('@chart');
|
||||
cy.get('.antd5-scroll-number.antd5-badge-count').should(
|
||||
'have.attr',
|
||||
'title',
|
||||
'1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should focus filter and open "more filters" programmatically', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_4', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_5', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_6', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_7', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_8', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_9', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_10', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_11', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_12', column: 'year', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
openMoreFilters();
|
||||
applyNativeFilterValueWithIndex(8, testItems.filterDefaultValue);
|
||||
cy.get(nativeFilters.applyFilter).click({ force: true });
|
||||
cy.getBySel('slice-header').within(() => {
|
||||
cy.get('.filter-counts').trigger('mouseover');
|
||||
});
|
||||
cy.get('.filterStatusPopover').contains('test_9').click();
|
||||
cy.getBySel('dropdown-content').should('be.visible');
|
||||
cy.get('.ant-select-focused').should('be.visible');
|
||||
});
|
||||
|
||||
it('should show tag count and one plain tag on focus and only count on blur in select ', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
enterNativeFilterEditModal();
|
||||
inputNativeFilterDefaultValue('Albania');
|
||||
cy.get('.ant-select-selection-search-input').clear({ force: true });
|
||||
inputNativeFilterDefaultValue('Algeria', true);
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
cy.getBySel('filter-bar').within(() => {
|
||||
cy.get(nativeFilters.filterItem).contains('Albania').should('be.visible');
|
||||
cy.get(nativeFilters.filterItem).contains('+ 1 ...').should('be.visible');
|
||||
cy.get('.ant-select-selection-search-input').click();
|
||||
cy.get(nativeFilters.filterItem).contains('+ 2 ...').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Native filters', () => {
|
||||
describe('Nativefilters tests initial state required', () => {
|
||||
beforeEach(() => {
|
||||
@@ -650,220 +310,4 @@ describe('Native filters', () => {
|
||||
cy.contains('Values are dependent on other filters').should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Nativefilters initial state not required', () => {
|
||||
it("User can check 'Filter has default value'", () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_name', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
inputNativeFilterDefaultValue(testItems.filterDefaultValue);
|
||||
});
|
||||
|
||||
it('User can add a new native filter', () => {
|
||||
prepareDashboardFilters([]);
|
||||
|
||||
let filterKey: string;
|
||||
const removeFirstChar = (search: string) =>
|
||||
search.split('').slice(1, search.length).join('');
|
||||
|
||||
cy.location().then(loc => {
|
||||
cy.url().should('contain', 'native_filters_key');
|
||||
const queryParams = qs.parse(removeFirstChar(loc.search));
|
||||
filterKey = queryParams.native_filters_key as string;
|
||||
expect(typeof filterKey).eq('string');
|
||||
});
|
||||
enterNativeFilterEditModal();
|
||||
addCountryNameFilter();
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
cy.location().then(loc => {
|
||||
cy.url().should('contain', 'native_filters_key');
|
||||
const queryParams = qs.parse(removeFirstChar(loc.search));
|
||||
const newfilterKey = queryParams.native_filters_key;
|
||||
expect(newfilterKey).eq(filterKey);
|
||||
});
|
||||
cy.get(nativeFilters.modal.container).should('not.exist');
|
||||
});
|
||||
|
||||
it('User can restore a deleted native filter', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_code', column: 'country_code', datasetId: 2 },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
cy.get(nativeFilters.filtersList.removeIcon).first().click();
|
||||
cy.get('[data-test="restore-filter-button"]')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.get(nativeFilters.modal.container)
|
||||
.find(nativeFilters.filtersPanel.filterName)
|
||||
.should(
|
||||
'have.attr',
|
||||
'value',
|
||||
testItems.topTenChart.filterColumnCountryCode,
|
||||
);
|
||||
});
|
||||
|
||||
it('User can create a time grain filter', () => {
|
||||
prepareDashboardFilters([]);
|
||||
enterNativeFilterEditModal();
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.timeGrain,
|
||||
testItems.filterType.timeGrain,
|
||||
testItems.datasetForNativeFilter,
|
||||
);
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
applyNativeFilterValueWithIndex(0, testItems.filterTimeGrain);
|
||||
cy.get(nativeFilters.applyFilter).click();
|
||||
cy.url().then(u => {
|
||||
const ur = new URL(u);
|
||||
expect(ur.search).to.include('native_filters');
|
||||
});
|
||||
validateFilterNameOnDashboard(testItems.filterType.timeGrain);
|
||||
validateFilterContentOnDashboard(testItems.filterTimeGrain);
|
||||
});
|
||||
|
||||
it.skip('User can create a time range filter', () => {
|
||||
enterNativeFilterEditModal();
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.timeRange,
|
||||
testItems.filterType.timeRange,
|
||||
);
|
||||
saveNativeFilterSettings(WORLD_HEALTH_CHARTS);
|
||||
cy.get(dashboardView.salesDashboardSpecific.vehicleSalesFilterTimeRange)
|
||||
.should('be.visible')
|
||||
.click();
|
||||
applyAdvancedTimeRangeFilterOnDashboard('2005-12-17', '2006-12-17');
|
||||
cy.url().then(u => {
|
||||
const ur = new URL(u);
|
||||
expect(ur.search).to.include('native_filters');
|
||||
});
|
||||
validateFilterNameOnDashboard(testItems.filterType.timeRange);
|
||||
cy.get(nativeFilters.filterFromDashboardView.timeRangeFilterContent)
|
||||
.contains('2005-12-17')
|
||||
.should('be.visible');
|
||||
});
|
||||
|
||||
it.skip('User can create a time column filter', () => {
|
||||
enterNativeFilterEditModal();
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.timeColumn,
|
||||
testItems.filterType.timeColumn,
|
||||
testItems.datasetForNativeFilter,
|
||||
);
|
||||
saveNativeFilterSettings(WORLD_HEALTH_CHARTS);
|
||||
cy.intercept(`/api/v1/chart/data?form_data=**`).as('chart');
|
||||
cy.get(nativeFilters.modal.container).should('not.exist');
|
||||
// assert that native filter is created
|
||||
validateFilterNameOnDashboard(testItems.filterType.timeColumn);
|
||||
applyNativeFilterValueWithIndex(
|
||||
0,
|
||||
testItems.topTenChart.filterColumnYear,
|
||||
);
|
||||
cy.get(nativeFilters.applyFilter).click({ force: true });
|
||||
cy.wait('@chart');
|
||||
validateFilterContentOnDashboard(testItems.topTenChart.filterColumnYear);
|
||||
});
|
||||
|
||||
it('User can create a numerical range filter', () => {
|
||||
visitDashboard();
|
||||
enterNativeFilterEditModal(false);
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.numerical,
|
||||
testItems.filterNumericalColumn,
|
||||
testItems.datasetForNativeFilter,
|
||||
testItems.filterNumericalColumn,
|
||||
);
|
||||
saveNativeFilterSettings([]);
|
||||
// assertions
|
||||
cy.get(nativeFilters.slider.slider).should('be.visible').click('center');
|
||||
cy.get(nativeFilters.applyFilter).click();
|
||||
// assert that the url contains 'native_filters' in the url
|
||||
cy.url().then(u => {
|
||||
const ur = new URL(u);
|
||||
expect(ur.search).to.include('native_filters');
|
||||
// assert that the start handle has a value
|
||||
cy.get(nativeFilters.slider.startHandle)
|
||||
.invoke('attr', 'aria-valuenow')
|
||||
.should('exist');
|
||||
// assert that the end handle has a value
|
||||
cy.get(nativeFilters.slider.endHandle)
|
||||
.invoke('attr', 'aria-valuenow')
|
||||
.should('exist');
|
||||
// assert slider text matches what we should have
|
||||
cy.get(nativeFilters.slider.sliderText).should('have.text', '49');
|
||||
});
|
||||
});
|
||||
|
||||
it('User can undo deleting a native filter', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_name', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
undoDeleteNativeFilter();
|
||||
cy.get(nativeFilters.modal.container)
|
||||
.find(nativeFilters.filtersPanel.filterName)
|
||||
.should('have.attr', 'value', testItems.topTenChart.filterColumn);
|
||||
});
|
||||
|
||||
it('User can cancel changes in native filter', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_name', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
cy.getBySel('filters-config-modal__name-input').type('|EDITED', {
|
||||
force: true,
|
||||
});
|
||||
cancelNativeFilterSettings();
|
||||
enterNativeFilterEditModal(false);
|
||||
cy.get(nativeFilters.filtersList.removeIcon).first().click();
|
||||
cy.contains('You have removed this filter.').should('be.visible');
|
||||
});
|
||||
|
||||
it('User can create a value filter', () => {
|
||||
visitDashboard();
|
||||
enterNativeFilterEditModal(false);
|
||||
addCountryNameFilter();
|
||||
cy.get(nativeFilters.filtersPanel.filterTypeInput)
|
||||
.find(nativeFilters.filtersPanel.filterTypeItem)
|
||||
.should('have.text', testItems.filterType.value);
|
||||
saveNativeFilterSettings([]);
|
||||
validateFilterNameOnDashboard(testItems.topTenChart.filterColumn);
|
||||
});
|
||||
|
||||
it('User can apply value filter with selected values', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_name', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
applyNativeFilterValueWithIndex(0, testItems.filterDefaultValue);
|
||||
cy.get(nativeFilters.applyFilter).click();
|
||||
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
|
||||
cy.contains(testItems.filterDefaultValue).should('be.visible');
|
||||
cy.contains(testItems.filterOtherCountry).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
it('User can stop filtering when filter is removed', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_name', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
inputNativeFilterDefaultValue(testItems.filterDefaultValue);
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
|
||||
cy.contains(testItems.filterDefaultValue).should('be.visible');
|
||||
cy.contains(testItems.filterOtherCountry).should('not.exist');
|
||||
});
|
||||
cy.get(nativeFilters.filterItem)
|
||||
.contains(testItems.filterDefaultValue)
|
||||
.should('be.visible');
|
||||
validateFilterNameOnDashboard(testItems.topTenChart.filterColumn);
|
||||
enterNativeFilterEditModal(false);
|
||||
deleteNativeFilter();
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
|
||||
cy.contains(testItems.filterDefaultValue).should('be.visible');
|
||||
cy.contains(testItems.filterOtherCountry).should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 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 { SAMPLE_DASHBOARD_1 } from 'cypress/utils/urls';
|
||||
import { interceptCharts, interceptDatasets, interceptGet } from './utils';
|
||||
|
||||
export const SAMPLE_CHART = { name: 'Most Populated Countries', viz: 'table' };
|
||||
|
||||
export function visitDashboard(createSample = true) {
|
||||
interceptCharts();
|
||||
interceptGet();
|
||||
interceptDatasets();
|
||||
|
||||
if (createSample) {
|
||||
cy.createSampleDashboards([0]);
|
||||
}
|
||||
|
||||
cy.visit(SAMPLE_DASHBOARD_1);
|
||||
cy.wait('@get');
|
||||
cy.wait('@getCharts');
|
||||
cy.wait('@getDatasets');
|
||||
cy.url().should('contain', 'native_filters_key');
|
||||
}
|
||||
|
||||
export function prepareDashboardFilters(
|
||||
filters: { name: string; column: string; datasetId: number }[],
|
||||
) {
|
||||
cy.createSampleDashboards([0]);
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: `api/v1/dashboard/1-sample-dashboard`,
|
||||
}).then(res => {
|
||||
const { body } = res;
|
||||
const dashboardId = body.result.id;
|
||||
const allFilters: Record<string, unknown>[] = [];
|
||||
filters.forEach((f, i) => {
|
||||
allFilters.push({
|
||||
id: `NATIVE_FILTER-fLH0pxFQ${i}`,
|
||||
controlValues: {
|
||||
enableEmptyFilter: false,
|
||||
defaultToFirstItem: false,
|
||||
multiSelect: true,
|
||||
searchAllOptions: false,
|
||||
inverseSelection: false,
|
||||
},
|
||||
name: f.name,
|
||||
filterType: 'filter_select',
|
||||
targets: [
|
||||
{
|
||||
datasetId: f.datasetId,
|
||||
column: { name: f.column },
|
||||
},
|
||||
],
|
||||
defaultDataMask: {
|
||||
extraFormData: {},
|
||||
filterState: {},
|
||||
ownState: {},
|
||||
},
|
||||
cascadeParentIds: [],
|
||||
scope: {
|
||||
rootPath: ['ROOT_ID'],
|
||||
excluded: [],
|
||||
},
|
||||
type: 'NATIVE_FILTER',
|
||||
description: '',
|
||||
chartsInScope: [5],
|
||||
tabsInScope: [],
|
||||
});
|
||||
});
|
||||
if (dashboardId) {
|
||||
const jsonMetadata = {
|
||||
native_filter_configuration: allFilters,
|
||||
timed_refresh_immune_slices: [],
|
||||
expanded_slices: {},
|
||||
refresh_frequency: 0,
|
||||
color_scheme: '',
|
||||
label_colors: {},
|
||||
shared_label_colors: {},
|
||||
color_scheme_domain: [],
|
||||
cross_filters_enabled: false,
|
||||
positions: {
|
||||
DASHBOARD_VERSION_KEY: 'v2',
|
||||
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: ['GRID_ID'] },
|
||||
GRID_ID: {
|
||||
type: 'GRID',
|
||||
id: 'GRID_ID',
|
||||
children: ['ROW-0rHnUz4nMA'],
|
||||
parents: ['ROOT_ID'],
|
||||
},
|
||||
HEADER_ID: {
|
||||
id: 'HEADER_ID',
|
||||
type: 'HEADER',
|
||||
meta: { text: '1 - Sample dashboard' },
|
||||
},
|
||||
'CHART-DF6EfI55F-': {
|
||||
type: 'CHART',
|
||||
id: 'CHART-DF6EfI55F-',
|
||||
children: [],
|
||||
parents: ['ROOT_ID', 'GRID_ID', 'ROW-0rHnUz4nMA'],
|
||||
meta: {
|
||||
width: 4,
|
||||
height: 50,
|
||||
chartId: 5,
|
||||
sliceName: 'Most Populated Countries',
|
||||
},
|
||||
},
|
||||
'ROW-0rHnUz4nMA': {
|
||||
type: 'ROW',
|
||||
id: 'ROW-0rHnUz4nMA',
|
||||
children: ['CHART-DF6EfI55F-'],
|
||||
parents: ['ROOT_ID', 'GRID_ID'],
|
||||
meta: { background: 'BACKGROUND_TRANSPARENT' },
|
||||
},
|
||||
},
|
||||
default_filters: '{}',
|
||||
filter_scopes: {},
|
||||
chart_configuration: {},
|
||||
};
|
||||
|
||||
return cy
|
||||
.request({
|
||||
method: 'PUT',
|
||||
url: `api/v1/dashboard/${dashboardId}`,
|
||||
body: {
|
||||
json_metadata: JSON.stringify(jsonMetadata),
|
||||
},
|
||||
})
|
||||
.then(() => visitDashboard(false));
|
||||
}
|
||||
return cy;
|
||||
});
|
||||
}
|
||||
@@ -25,7 +25,7 @@ describe('AdhocMetrics', () => {
|
||||
});
|
||||
|
||||
it('Clear metric and set simple adhoc metric', () => {
|
||||
const metric = 'SUM(num_girls)';
|
||||
const metric = 'sum(num_girls)';
|
||||
const metricName = 'Sum Girls';
|
||||
cy.get('[data-test=metrics]')
|
||||
.find('[data-test="remove-control-button"]')
|
||||
|
||||
@@ -100,7 +100,7 @@ describe('Visualization > Table', () => {
|
||||
});
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@chartData',
|
||||
querySubstring: /group by\n.*name/i,
|
||||
querySubstring: /GROUP BY.*name/i,
|
||||
chartSelector: 'table',
|
||||
});
|
||||
});
|
||||
@@ -247,7 +247,7 @@ describe('Visualization > Table', () => {
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@chartData',
|
||||
querySubstring: /group by\n.*state/i,
|
||||
querySubstring: /GROUP BY.*state/i,
|
||||
chartSelector: 'table',
|
||||
});
|
||||
cy.get('td').contains(/\d*%/);
|
||||
|
||||
+6
-6
@@ -8445,9 +8445,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
|
||||
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
|
||||
"integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==",
|
||||
"dependencies": {
|
||||
"isarray": "0.0.1"
|
||||
}
|
||||
@@ -16793,9 +16793,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
|
||||
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
|
||||
"integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==",
|
||||
"requires": {
|
||||
"isarray": "0.0.1"
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ module.exports = {
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
|
||||
snapshotSerializers: ['@emotion/jest/enzyme-serializer'],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!d3-(interpolate|color|time)|remark-gfm|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender)',
|
||||
'node_modules/(?!d3-(interpolate|color|time)|remark-gfm|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms)',
|
||||
],
|
||||
globals: {
|
||||
__DEV__: true,
|
||||
|
||||
Generated
+4316
-1460
File diff suppressed because it is too large
Load Diff
@@ -44,8 +44,8 @@
|
||||
"build-storybook": "storybook build",
|
||||
"build-translation": "scripts/po2json.sh",
|
||||
"bundle-stats": "cross-env BUNDLE_ANALYZER=true npm run build && npx open-cli ../superset/static/stats/statistics.html",
|
||||
"core:cover": "cross-env NODE_ENV=test jest --coverage --coverageThreshold='{\"global\":{\"statements\":100,\"branches\":100,\"functions\":100,\"lines\":100}}' --collectCoverageFrom='[\"packages/**/src/**/*.{js,ts}\", \"!packages/superset-ui-demo/**/*\"]' packages",
|
||||
"cover": "cross-env NODE_ENV=test jest --coverage",
|
||||
"core:cover": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --coverage --coverageThreshold='{\"global\":{\"statements\":100,\"branches\":100,\"functions\":100,\"lines\":100}}' --collectCoverageFrom='[\"packages/**/src/**/*.{js,ts}\", \"!packages/superset-ui-demo/**/*\"]' packages",
|
||||
"cover": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --coverage",
|
||||
"dev": "webpack --mode=development --color --watch",
|
||||
"dev-server": "cross-env NODE_ENV=development BABEL_ENV=development node --max_old_space_size=4096 ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --mode=development",
|
||||
"eslint": "eslint --ignore-path=.eslintignore --ext .js,.jsx,.ts,tsx",
|
||||
@@ -67,8 +67,8 @@
|
||||
"prod": "npm run build",
|
||||
"prune": "rm -rf ./{packages,plugins}/*/{lib,esm,tsconfig.tsbuildinfo,package-lock.json}",
|
||||
"storybook": "cross-env NODE_ENV=development BABEL_ENV=development storybook dev -p 6006",
|
||||
"tdd": "cross-env NODE_ENV=test jest --watch",
|
||||
"test": "cross-env NODE_ENV=test jest --max-workers=50%",
|
||||
"tdd": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --watch",
|
||||
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --max-workers=50%",
|
||||
"type": "tsc --noEmit",
|
||||
"update-maps": "jupyter nbconvert --to notebook --execute --inplace 'plugins/legacy-plugin-chart-country-map/scripts/Country Map GeoJSON Generator.ipynb' -Xfrozen_modules=off",
|
||||
"validate-release": "../RELEASING/validate_this_release.sh"
|
||||
@@ -82,12 +82,12 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.13.0",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@fontsource/fira-code": "^5.0.18",
|
||||
"@fontsource/inter": "^5.0.20",
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"@rjsf/core": "^5.19.3",
|
||||
"@rjsf/core": "^5.21.1",
|
||||
"@rjsf/utils": "^5.19.3",
|
||||
"@rjsf/validator-ajv8": "^5.19.3",
|
||||
"@scarf/scarf": "^1.3.0",
|
||||
@@ -135,6 +135,7 @@
|
||||
"classnames": "^2.2.5",
|
||||
"core-js": "^3.38.1",
|
||||
"d3-scale": "^2.1.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"dom-to-image-more": "^3.2.0",
|
||||
"emotion-rgba": "0.0.12",
|
||||
"fast-glob": "^3.3.2",
|
||||
@@ -164,7 +165,7 @@
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^6.13.7",
|
||||
"rc-trigger": "^5.3.4",
|
||||
"re-resizable": "^6.9.11",
|
||||
"re-resizable": "^6.10.0",
|
||||
"react": "^16.13.1",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-checkbox-tree": "^1.8.0",
|
||||
@@ -210,7 +211,7 @@
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@applitools/eyes-storybook": "^3.50.7",
|
||||
"@applitools/eyes-storybook": "^3.50.9",
|
||||
"@babel/cli": "^7.22.6",
|
||||
"@babel/compat-data": "^7.22.6",
|
||||
"@babel/core": "^7.23.9",
|
||||
@@ -226,7 +227,7 @@
|
||||
"@babel/preset-react": "^7.22.5",
|
||||
"@babel/register": "^7.23.7",
|
||||
"@babel/types": "^7.24.9",
|
||||
"@cypress/react": "^5.10.0",
|
||||
"@cypress/react": "^8.0.2",
|
||||
"@emotion/babel-plugin": "^11.11.0",
|
||||
"@emotion/jest": "^11.11.0",
|
||||
"@hot-loader/react-dom": "^16.14.0",
|
||||
@@ -243,10 +244,10 @@
|
||||
"@storybook/react-webpack5": "8.1.11",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@testing-library/dom": "^8.20.1",
|
||||
"@testing-library/jest-dom": "^5.11.6",
|
||||
"@testing-library/jest-dom": "^6.5.0",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^5.1.3",
|
||||
"@testing-library/user-event": "^12.7.0",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
"@types/enzyme": "^3.10.18",
|
||||
@@ -261,7 +262,7 @@
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-gravatar": "^2.6.14",
|
||||
"@types/react-json-tree": "^0.6.11",
|
||||
"@types/react-loadable": "^5.5.6",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
"@types/react-redux": "^7.1.10",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
@@ -290,13 +291,13 @@
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.7",
|
||||
"esbuild": "^0.20.0",
|
||||
"esbuild-loader": "^4.1.0",
|
||||
"esbuild-loader": "^4.2.2",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-cypress": "^3.5.0",
|
||||
"eslint-plugin-file-progress": "^1.4.0",
|
||||
"eslint-plugin-file-progress": "^1.5.0",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-jest": "^27.8.0",
|
||||
"eslint-plugin-jest-dom": "^3.6.5",
|
||||
@@ -353,7 +354,7 @@
|
||||
"webpack-bundle-analyzer": "^4.10.1",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.15.1",
|
||||
"webpack-manifest-plugin": "^4.1.1",
|
||||
"webpack-manifest-plugin": "^5.0.0",
|
||||
"webpack-sources": "^3.2.3",
|
||||
"webpack-visualizer-plugin2": "^1.1.0",
|
||||
"xdm": "^3.4.0"
|
||||
|
||||
+1
@@ -3,4 +3,5 @@ module.exports = {
|
||||
moduleNameMapper: {
|
||||
'\\.(gif|ttf|eot|png|jpg)$': '<rootDir>/test/__mocks__/mockExportString.js',
|
||||
},
|
||||
testEnvironment: 'jsdom',
|
||||
};
|
||||
|
||||
+7562
-3774
File diff suppressed because it is too large
Load Diff
+1
@@ -37,6 +37,7 @@
|
||||
"@babel/cli": "^7.16.0",
|
||||
"@types/jest": "^26.0.4",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"typescript": "^4.1.2"
|
||||
}
|
||||
|
||||
-1
@@ -19,7 +19,6 @@
|
||||
import { t, validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
sections,
|
||||
sharedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
|
||||
@@ -35,10 +35,10 @@
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/dom": "^8.20.1",
|
||||
"@testing-library/jest-dom": "^5.11.6",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^5.0.3",
|
||||
"@testing-library/user-event": "^12.7.0",
|
||||
"@testing-library/react-hooks": "*",
|
||||
"@testing-library/user-event": "*",
|
||||
"ace-builds": "^1.4.14",
|
||||
"antd": "4.10.3",
|
||||
"brace": "^0.11.1",
|
||||
|
||||
+17
-1
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { useState, ReactNode, useLayoutEffect, RefObject } from 'react';
|
||||
import { css, styled, SupersetTheme } from '@superset-ui/core';
|
||||
import { css, SafeMarkdown, styled, SupersetTheme } from '@superset-ui/core';
|
||||
import { Tooltip } from './Tooltip';
|
||||
import { ColumnTypeLabel } from './ColumnTypeLabel/ColumnTypeLabel';
|
||||
import CertifiedIconWithTooltip from './CertifiedIconWithTooltip';
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
getColumnTypeTooltipNode,
|
||||
} from './labelUtils';
|
||||
import { SQLPopover } from './SQLPopover';
|
||||
import InfoTooltipWithTrigger from './InfoTooltipWithTrigger';
|
||||
|
||||
export type ColumnOptionProps = {
|
||||
column: ColumnMeta;
|
||||
@@ -50,6 +51,8 @@ export function ColumnOption({
|
||||
}: ColumnOptionProps) {
|
||||
const { expression, column_name, type_generic } = column;
|
||||
const hasExpression = expression && expression !== column_name;
|
||||
const warningMarkdown =
|
||||
column.warning_markdown || column.warning_text || column.error_text;
|
||||
const type = hasExpression ? 'expression' : type_generic;
|
||||
const [tooltipText, setTooltipText] = useState<ReactNode>(column.column_name);
|
||||
const [columnTypeTooltipText, setcolumnTypeTooltipText] = useState<ReactNode>(
|
||||
@@ -94,6 +97,19 @@ export function ColumnOption({
|
||||
details={column.certification_details}
|
||||
/>
|
||||
)}
|
||||
{warningMarkdown && (
|
||||
<InfoTooltipWithTrigger
|
||||
className="text-warning"
|
||||
icon="warning"
|
||||
tooltip={<SafeMarkdown source={warningMarkdown} />}
|
||||
label={`warn-${column.column_name}`}
|
||||
iconsStyle={{ marginLeft: 0 }}
|
||||
{...(column.error_text && {
|
||||
className: 'text-danger',
|
||||
icon: 'exclamation-circle',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</StyleOverrides>
|
||||
);
|
||||
}
|
||||
|
||||
+6
-1
@@ -81,7 +81,8 @@ export function MetricOption({
|
||||
</span>
|
||||
);
|
||||
|
||||
const warningMarkdown = metric.warning_markdown || metric.warning_text;
|
||||
const warningMarkdown =
|
||||
metric.warning_markdown || metric.warning_text || metric.error_text;
|
||||
|
||||
const [tooltipText, setTooltipText] = useState<ReactNode>(metric.metric_name);
|
||||
|
||||
@@ -116,6 +117,10 @@ export function MetricOption({
|
||||
tooltip={<SafeMarkdown source={warningMarkdown} />}
|
||||
label={`warn-${metric.metric_name}`}
|
||||
iconsStyle={{ marginLeft: 0 }}
|
||||
{...(metric.error_text && {
|
||||
className: 'text-danger',
|
||||
icon: 'exclamation-circle',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</FlexRowContainer>
|
||||
|
||||
+1
-21
@@ -39,6 +39,7 @@ import {
|
||||
SORT_SERIES_CHOICES,
|
||||
} from '../constants';
|
||||
import { checkColumnType } from '../utils/checkColumnType';
|
||||
import { isSortable } from '../utils/isSortable';
|
||||
|
||||
export const contributionModeControl = {
|
||||
name: 'contributionMode',
|
||||
@@ -55,27 +56,6 @@ export const contributionModeControl = {
|
||||
},
|
||||
};
|
||||
|
||||
function isForcedCategorical(controls: ControlStateMapping): boolean {
|
||||
return (
|
||||
checkColumnType(
|
||||
getColumnLabel(controls?.x_axis?.value as QueryFormColumn),
|
||||
controls?.datasource?.datasource,
|
||||
[GenericDataType.Numeric],
|
||||
) && !!controls?.xAxisForceCategorical?.value
|
||||
);
|
||||
}
|
||||
|
||||
function isSortable(controls: ControlStateMapping): boolean {
|
||||
return (
|
||||
isForcedCategorical(controls) ||
|
||||
checkColumnType(
|
||||
getColumnLabel(controls?.x_axis?.value as QueryFormColumn),
|
||||
controls?.datasource?.datasource,
|
||||
[GenericDataType.String, GenericDataType.Boolean],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const xAxisSortVisibility = ({ controls }: { controls: ControlStateMapping }) =>
|
||||
isSortable(controls) &&
|
||||
ensureIsArray(controls?.groupby?.value).length === 0 &&
|
||||
|
||||
@@ -83,6 +83,7 @@ export interface Dataset {
|
||||
owners?: Owner[];
|
||||
filter_select?: boolean;
|
||||
filter_select_enabled?: boolean;
|
||||
column_names?: string[];
|
||||
}
|
||||
|
||||
export interface ControlPanelState {
|
||||
|
||||
@@ -57,6 +57,8 @@ export const D3_FORMAT_OPTIONS: [string, string][] = [
|
||||
...d3Formatted,
|
||||
['DURATION', t('Duration in ms (66000 => 1m 6s)')],
|
||||
['DURATION_SUB', t('Duration in ms (1.40008 => 1ms 400µs 80ns)')],
|
||||
['MEMORY_DECIMAL', t('Memory in bytes - decimal (1024B => 1.024kB)')],
|
||||
['MEMORY_BINARY', t('Memory in bytes - binary (1024B => 1KiB)')],
|
||||
];
|
||||
|
||||
export const D3_TIME_FORMAT_DOCS = t(
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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 {
|
||||
GenericDataType,
|
||||
getColumnLabel,
|
||||
isPhysicalColumn,
|
||||
QueryFormColumn,
|
||||
} from '@superset-ui/core';
|
||||
import { checkColumnType, ControlStateMapping } from '..';
|
||||
|
||||
export function isSortable(controls: ControlStateMapping): boolean {
|
||||
const isForcedCategorical =
|
||||
checkColumnType(
|
||||
getColumnLabel(controls?.x_axis?.value as QueryFormColumn),
|
||||
controls?.datasource?.datasource,
|
||||
[GenericDataType.Numeric],
|
||||
) && !!controls?.xAxisForceCategorical?.value;
|
||||
|
||||
const xAxisValue = controls?.x_axis?.value as QueryFormColumn;
|
||||
|
||||
// Given that we don't know the type of a custom SQL column,
|
||||
// we treat it as sortable and give the responsibility to the
|
||||
// user to provide a sortable result.
|
||||
const isCustomSQL = !isPhysicalColumn(xAxisValue);
|
||||
|
||||
return (
|
||||
isForcedCategorical ||
|
||||
isCustomSQL ||
|
||||
checkColumnType(
|
||||
getColumnLabel(xAxisValue),
|
||||
controls?.datasource?.datasource,
|
||||
[GenericDataType.String, GenericDataType.Boolean],
|
||||
)
|
||||
);
|
||||
}
|
||||
+17
@@ -34,6 +34,9 @@ jest.mock('../../src/components/ColumnTypeLabel/ColumnTypeLabel', () => ({
|
||||
<div data-test="mock-column-type-label">{type}</div>
|
||||
),
|
||||
}));
|
||||
jest.mock('../../src/components/InfoTooltipWithTrigger', () => () => (
|
||||
<div data-test="mock-info-tooltip-with-trigger" />
|
||||
));
|
||||
|
||||
const defaultProps: ColumnOptionProps = {
|
||||
column: {
|
||||
@@ -111,3 +114,17 @@ test('dttm column has correct column label if showType is true', () => {
|
||||
String(GenericDataType.Temporal),
|
||||
);
|
||||
});
|
||||
test('doesnt show InfoTooltipWithTrigger when no warning', () => {
|
||||
const { queryByText } = setup();
|
||||
expect(queryByText('mock-info-tooltip-with-trigger')).not.toBeInTheDocument();
|
||||
});
|
||||
test('shows a warning with InfoTooltipWithTrigger when it contains warning', () => {
|
||||
const { getByTestId } = setup({
|
||||
...defaultProps,
|
||||
column: {
|
||||
...defaultProps.column,
|
||||
warning_text: 'This is a warning',
|
||||
},
|
||||
});
|
||||
expect(getByTestId('mock-info-tooltip-with-trigger')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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 { ControlStateMapping } from '@superset-ui/chart-controls';
|
||||
import { GenericDataType } from '@superset-ui/core';
|
||||
import { isSortable } from '../../src/utils/isSortable';
|
||||
|
||||
const controls: ControlStateMapping = {
|
||||
datasource: {
|
||||
datasource: {
|
||||
columns: [
|
||||
{ column_name: 'a', type_generic: GenericDataType.String },
|
||||
{ column_name: 'b', type_generic: GenericDataType.Numeric },
|
||||
{ column_name: 'c', type_generic: GenericDataType.Boolean },
|
||||
],
|
||||
},
|
||||
type: 'Select',
|
||||
},
|
||||
};
|
||||
|
||||
test('should return true if the column is forced to be categorical', () => {
|
||||
const c: ControlStateMapping = {
|
||||
...controls,
|
||||
x_axis: { value: 'b', type: 'Select' },
|
||||
xAxisForceCategorical: { value: true, type: 'Checkbox' },
|
||||
};
|
||||
expect(isSortable(c)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true if the column is a custom SQL column', () => {
|
||||
const c: ControlStateMapping = {
|
||||
...controls,
|
||||
x_axis: {
|
||||
value: { label: 'custom_sql', sqlExpression: 'MAX(ID)' },
|
||||
type: 'Select',
|
||||
},
|
||||
};
|
||||
expect(isSortable(c)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true if the column type is String or Boolean', () => {
|
||||
const c: ControlStateMapping = {
|
||||
...controls,
|
||||
x_axis: { value: 'c', type: 'Checkbox' },
|
||||
};
|
||||
expect(isSortable(c)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false if none of the conditions are met', () => {
|
||||
const c: ControlStateMapping = {
|
||||
...controls,
|
||||
x_axis: { value: 'b', type: 'Input' },
|
||||
};
|
||||
expect(isSortable(c)).toBe(false);
|
||||
});
|
||||
@@ -25,21 +25,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"@testing-library/react-hooks": "^5.1.3",
|
||||
"@types/d3-format": "^1.3.0",
|
||||
"@types/d3-interpolate": "^3.0.4",
|
||||
"@types/d3-scale": "^2.1.1",
|
||||
"@types/d3-time": "^3.0.3",
|
||||
"@types/d3-time-format": "^4.0.3",
|
||||
"@types/enzyme": "^3.10.18",
|
||||
"@types/fetch-mock": "^7.3.8",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/math-expression-evaluator": "^1.3.3",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/prop-types": "^15.7.2",
|
||||
"@types/rison": "0.0.9",
|
||||
"@types/seedrandom": "^3.0.8",
|
||||
"@vx/responsive": "^0.0.199",
|
||||
"csstype": "^3.1.3",
|
||||
"d3-format": "^1.3.2",
|
||||
@@ -49,9 +35,9 @@
|
||||
"d3-time-format": "^4.1.0",
|
||||
"fetch-retry": "^6.0.0",
|
||||
"jed": "^1.1.1",
|
||||
"lodash": "^4.17.11",
|
||||
"lodash": "^4.17.21",
|
||||
"math-expression-evaluator": "^1.3.8",
|
||||
"pretty-ms": "^7.0.0",
|
||||
"pretty-ms": "^9.1.0",
|
||||
"react-error-boundary": "^1.2.5",
|
||||
"react-markdown": "^8.0.7",
|
||||
"rehype-raw": "^7.0.0",
|
||||
@@ -65,8 +51,21 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"fetch-mock": "^6.5.2",
|
||||
"jest-mock-console": "^1.0.0",
|
||||
"@types/d3-format": "^1.3.0",
|
||||
"@types/d3-interpolate": "^3.0.4",
|
||||
"@types/d3-scale": "^2.1.1",
|
||||
"@types/d3-time": "^3.0.3",
|
||||
"@types/d3-time-format": "^4.0.3",
|
||||
"@types/enzyme": "^3.10.18",
|
||||
"@types/fetch-mock": "^7.3.8",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/math-expression-evaluator": "^1.3.3",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/prop-types": "^15.7.2",
|
||||
"@types/rison": "0.0.9",
|
||||
"@types/seedrandom": "^3.0.8",
|
||||
"fetch-mock": "^11.1.4",
|
||||
"jest-mock-console": "^2.0.0",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"timezone-mock": "1.3.6"
|
||||
},
|
||||
@@ -75,9 +74,10 @@
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@testing-library/dom": "^8.20.1",
|
||||
"@testing-library/jest-dom": "^5.11.6",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/user-event": "^12.7.0",
|
||||
"@testing-library/react-hooks": "*",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-loadable": "*",
|
||||
"@types/tinycolor2": "*",
|
||||
|
||||
@@ -50,6 +50,9 @@ export interface ChartMetadataConfig {
|
||||
labelExplanation?: string | null;
|
||||
queryObjectCount?: number;
|
||||
parseMethod?: ParseMethod;
|
||||
// suppressContextMenu: true hides the default context menu for the chart.
|
||||
// This is useful for viz plugins that define their own context menu.
|
||||
suppressContextMenu?: boolean;
|
||||
}
|
||||
|
||||
export default class ChartMetadata {
|
||||
@@ -91,6 +94,8 @@ export default class ChartMetadata {
|
||||
|
||||
parseMethod: ParseMethod;
|
||||
|
||||
suppressContextMenu?: boolean;
|
||||
|
||||
constructor(config: ChartMetadataConfig) {
|
||||
const {
|
||||
name,
|
||||
@@ -111,6 +116,7 @@ export default class ChartMetadata {
|
||||
labelExplanation = null,
|
||||
queryObjectCount = 1,
|
||||
parseMethod = 'json-bigint',
|
||||
suppressContextMenu = false,
|
||||
} = config;
|
||||
|
||||
this.name = name;
|
||||
@@ -140,6 +146,7 @@ export default class ChartMetadata {
|
||||
this.labelExplanation = labelExplanation;
|
||||
this.queryObjectCount = queryObjectCount;
|
||||
this.parseMethod = parseMethod;
|
||||
this.suppressContextMenu = suppressContextMenu;
|
||||
}
|
||||
|
||||
canBeAnnotationType(type: string): boolean {
|
||||
|
||||
+4
-3
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import prettyMsFormatter from 'pretty-ms';
|
||||
import prettyMilliseconds, { Options } from 'pretty-ms';
|
||||
import NumberFormatter from '../NumberFormatter';
|
||||
|
||||
export default function createDurationFormatter(
|
||||
@@ -26,13 +26,14 @@ export default function createDurationFormatter(
|
||||
id?: string;
|
||||
label?: string;
|
||||
multiplier?: number;
|
||||
} & prettyMsFormatter.Options = {},
|
||||
} & Options = {},
|
||||
) {
|
||||
const { description, id, label, multiplier = 1, ...prettyMsOptions } = config;
|
||||
|
||||
return new NumberFormatter({
|
||||
description,
|
||||
formatFunc: value => prettyMsFormatter(value * multiplier, prettyMsOptions),
|
||||
formatFunc: value =>
|
||||
prettyMilliseconds(value * multiplier, prettyMsOptions),
|
||||
id: id ?? 'duration_format',
|
||||
label: label ?? `Duration formatter`,
|
||||
});
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 NumberFormatter from '../NumberFormatter';
|
||||
|
||||
export default function createMemoryFormatter(
|
||||
config: {
|
||||
description?: string;
|
||||
id?: string;
|
||||
label?: string;
|
||||
binary?: boolean;
|
||||
decimals?: number;
|
||||
} = {},
|
||||
) {
|
||||
const { description, id, label, binary, decimals = 2 } = config;
|
||||
|
||||
return new NumberFormatter({
|
||||
description,
|
||||
formatFunc: value => {
|
||||
if (value === 0) return '0B';
|
||||
|
||||
const sign = value > 0 ? '' : '-';
|
||||
const absValue = Math.abs(value);
|
||||
|
||||
const suffixes = binary
|
||||
? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
||||
: ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'RB', 'QB'];
|
||||
const base = binary ? 1024 : 1000;
|
||||
|
||||
const i = Math.min(
|
||||
suffixes.length - 1,
|
||||
Math.floor(Math.log(absValue) / Math.log(base)),
|
||||
);
|
||||
return `${sign}${parseFloat((absValue / Math.pow(base, i)).toFixed(decimals))}${suffixes[i]}`;
|
||||
},
|
||||
id: id ?? 'memory_format',
|
||||
label: label ?? `Memory formatter`,
|
||||
});
|
||||
}
|
||||
@@ -31,5 +31,6 @@ export {
|
||||
export { default as NumberFormatterRegistry } from './NumberFormatterRegistry';
|
||||
export { default as createD3NumberFormatter } from './factories/createD3NumberFormatter';
|
||||
export { default as createDurationFormatter } from './factories/createDurationFormatter';
|
||||
export { default as createMemoryFormatter } from './factories/createMemoryFormatter';
|
||||
export { default as createSiAtMostNDigitFormatter } from './factories/createSiAtMostNDigitFormatter';
|
||||
export { default as createSmartNumberFormatter } from './factories/createSmartNumberFormatter';
|
||||
|
||||
@@ -80,6 +80,24 @@ export type Filter = {
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type AppliedFilter = {
|
||||
values: {
|
||||
filters: Record<string, any>[];
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type AppliedCrossFilterType = {
|
||||
filterType: undefined;
|
||||
targets: number[];
|
||||
scope: number[];
|
||||
} & AppliedFilter;
|
||||
|
||||
export type AppliedNativeFilterType = {
|
||||
filterType: 'filter_select';
|
||||
scope: number[];
|
||||
targets: Partial<NativeFilterTarget>[];
|
||||
} & AppliedFilter;
|
||||
|
||||
export type FilterWithDataMask = Filter & { dataMask: DataMaskWithId };
|
||||
|
||||
export type Divider = Partial<Omit<Filter, 'id' | 'type'>> & {
|
||||
@@ -89,6 +107,24 @@ export type Divider = Partial<Omit<Filter, 'id' | 'type'>> & {
|
||||
type: typeof NativeFilterType.Divider;
|
||||
};
|
||||
|
||||
export function isAppliedCrossFilterType(
|
||||
filterElement: AppliedCrossFilterType | AppliedNativeFilterType | Filter,
|
||||
): filterElement is AppliedCrossFilterType {
|
||||
return (
|
||||
filterElement.filterType === undefined &&
|
||||
filterElement.hasOwnProperty('values')
|
||||
);
|
||||
}
|
||||
|
||||
export function isAppliedNativeFilterType(
|
||||
filterElement: AppliedCrossFilterType | AppliedNativeFilterType | Filter,
|
||||
): filterElement is AppliedNativeFilterType {
|
||||
return (
|
||||
filterElement.filterType === 'filter_select' &&
|
||||
filterElement.hasOwnProperty('values')
|
||||
);
|
||||
}
|
||||
|
||||
export function isNativeFilter(
|
||||
filterElement: Filter | Divider,
|
||||
): filterElement is Filter {
|
||||
|
||||
@@ -71,6 +71,7 @@ export interface Metric {
|
||||
verbose_name?: string;
|
||||
warning_markdown?: Maybe<string>;
|
||||
warning_text?: Maybe<string>;
|
||||
error_text?: string;
|
||||
}
|
||||
|
||||
export function isSavedMetric(metric: any): metric is SavedMetric {
|
||||
|
||||
@@ -227,6 +227,7 @@ export const ErrorTypeEnum = {
|
||||
ASYNC_WORKERS_ERROR: 'ASYNC_WORKERS_ERROR',
|
||||
ADHOC_SUBQUERY_NOT_ALLOWED_ERROR: 'ADHOC_SUBQUERY_NOT_ALLOWED_ERROR',
|
||||
INVALID_SQL_ERROR: 'INVALID_SQL_ERROR',
|
||||
RESULT_TOO_LARGE_ERROR: 'RESULT_TOO_LARGE_ERROR',
|
||||
|
||||
// Generic errors
|
||||
GENERIC_COMMAND_ERROR: 'GENERIC_COMMAND_ERROR',
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('ChartClient', () => {
|
||||
chartClient = new ChartClient();
|
||||
});
|
||||
|
||||
afterEach(fetchMock.restore);
|
||||
afterEach(() => fetchMock.restore());
|
||||
|
||||
describe('new ChartClient(config)', () => {
|
||||
it('creates a client without argument', () => {
|
||||
|
||||
+9
-8
@@ -17,9 +17,10 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import { ComponentType } from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import mockConsole, { RestoreConsole } from 'jest-mock-console';
|
||||
import { render as renderTestComponent, screen } from '@testing-library/react';
|
||||
import createLoadableRenderer, {
|
||||
LoadableRenderer as LoadableRendererType,
|
||||
} from '../../../src/chart/components/createLoadableRenderer';
|
||||
@@ -67,7 +68,7 @@ describe('createLoadableRenderer', () => {
|
||||
it('calls onRenderSuccess when succeeds', async () => {
|
||||
const onRenderSuccess = jest.fn();
|
||||
const onRenderFailure = jest.fn();
|
||||
shallow(
|
||||
renderTestComponent(
|
||||
<LoadableRenderer
|
||||
onRenderSuccess={onRenderSuccess}
|
||||
onRenderFailure={onRenderFailure}
|
||||
@@ -95,7 +96,7 @@ describe('createLoadableRenderer', () => {
|
||||
});
|
||||
const onRenderSuccess = jest.fn();
|
||||
const onRenderFailure = jest.fn();
|
||||
shallow(
|
||||
renderTestComponent(
|
||||
<FailedRenderer
|
||||
onRenderSuccess={onRenderSuccess}
|
||||
onRenderFailure={onRenderFailure}
|
||||
@@ -122,7 +123,7 @@ describe('createLoadableRenderer', () => {
|
||||
loading,
|
||||
render,
|
||||
});
|
||||
shallow(<FailedRenderer />);
|
||||
renderTestComponent(<FailedRenderer />);
|
||||
expect(loadChartFailure).toHaveBeenCalledTimes(1);
|
||||
setTimeout(() => {
|
||||
expect(render).not.toHaveBeenCalled();
|
||||
@@ -132,12 +133,12 @@ describe('createLoadableRenderer', () => {
|
||||
|
||||
it('renders the lazy-load components', () =>
|
||||
new Promise(done => {
|
||||
const wrapper = shallow(<LoadableRenderer />);
|
||||
renderTestComponent(<LoadableRenderer />);
|
||||
// lazy-loaded component not rendered immediately
|
||||
expect(wrapper.find(TestComponent)).toHaveLength(0);
|
||||
expect(screen.queryByText('test')).not.toBeInTheDocument();
|
||||
setTimeout(() => {
|
||||
// but rendered after the component is loaded.
|
||||
expect(wrapper.find(TestComponent)).toHaveLength(1);
|
||||
expect(screen.queryByText('test')).toBeInTheDocument();
|
||||
done(undefined);
|
||||
}, 10);
|
||||
}));
|
||||
@@ -149,7 +150,7 @@ describe('createLoadableRenderer', () => {
|
||||
render: () => <div />,
|
||||
});
|
||||
|
||||
expect(() => shallow(<NeverLoadingRenderer />)).not.toThrow();
|
||||
expect(() => renderTestComponent(<NeverLoadingRenderer />)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,13 +22,11 @@ import { SupersetClient, SupersetClientClass } from '@superset-ui/core';
|
||||
import { LOGIN_GLOB } from './fixtures/constants';
|
||||
|
||||
describe('SupersetClient', () => {
|
||||
beforeAll(() => {
|
||||
fetchMock.get(LOGIN_GLOB, { result: '' });
|
||||
});
|
||||
beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '' }));
|
||||
|
||||
afterAll(fetchMock.restore);
|
||||
afterAll(() => fetchMock.restore());
|
||||
|
||||
afterEach(SupersetClient.reset);
|
||||
afterEach(() => SupersetClient.reset());
|
||||
|
||||
it('exposes reset, configure, init, get, post, postForm, isAuthenticated, and reAuthenticate methods', () => {
|
||||
expect(typeof SupersetClient.configure).toBe('function');
|
||||
|
||||
+21
-16
@@ -21,11 +21,12 @@ import { SupersetClientClass, ClientConfig, CallApi } from '@superset-ui/core';
|
||||
import { LOGIN_GLOB } from './fixtures/constants';
|
||||
|
||||
describe('SupersetClientClass', () => {
|
||||
beforeAll(() => {
|
||||
beforeEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.get(LOGIN_GLOB, { result: '' });
|
||||
});
|
||||
|
||||
afterAll(fetchMock.restore);
|
||||
afterAll(() => fetchMock.restore());
|
||||
|
||||
describe('new SupersetClientClass()', () => {
|
||||
it('fallback protocol to https when setting only host', () => {
|
||||
@@ -78,11 +79,10 @@ describe('SupersetClientClass', () => {
|
||||
});
|
||||
|
||||
describe('.init()', () => {
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
// reset
|
||||
fetchMock.get(LOGIN_GLOB, { result: 1234 }, { overwriteRoutes: true });
|
||||
});
|
||||
beforeEach(() =>
|
||||
fetchMock.get(LOGIN_GLOB, { result: 1234 }, { overwriteRoutes: true }),
|
||||
);
|
||||
afterEach(() => fetchMock.reset());
|
||||
|
||||
it('calls api/v1/security/csrf_token/ when init() is called if no CSRF token is passed', async () => {
|
||||
expect.assertions(1);
|
||||
@@ -165,7 +165,7 @@ describe('SupersetClientClass', () => {
|
||||
});
|
||||
|
||||
describe('.isAuthenticated()', () => {
|
||||
afterEach(fetchMock.reset);
|
||||
afterEach(() => fetchMock.reset());
|
||||
|
||||
it('returns true if there is a token and false if not', async () => {
|
||||
expect.assertions(2);
|
||||
@@ -254,7 +254,8 @@ describe('SupersetClientClass', () => {
|
||||
});
|
||||
|
||||
describe('requests', () => {
|
||||
afterEach(fetchMock.reset);
|
||||
afterEach(() => fetchMock.restore());
|
||||
|
||||
const protocol = 'https:';
|
||||
const host = 'host';
|
||||
const mockGetEndpoint = '/get/url';
|
||||
@@ -272,13 +273,15 @@ describe('SupersetClientClass', () => {
|
||||
const mockTextJsonResponse = '{ "value": 9223372036854775807 }';
|
||||
const mockPayload = { json: () => Promise.resolve('payload') };
|
||||
|
||||
fetchMock.get(mockGetUrl, mockPayload);
|
||||
fetchMock.post(mockPostUrl, mockPayload);
|
||||
fetchMock.put(mockPutUrl, mockPayload);
|
||||
fetchMock.delete(mockDeleteUrl, mockPayload);
|
||||
fetchMock.delete(mockRequestUrl, mockPayload);
|
||||
fetchMock.get(mockTextUrl, mockTextJsonResponse);
|
||||
fetchMock.post(mockTextUrl, mockTextJsonResponse);
|
||||
beforeEach(() => {
|
||||
fetchMock.get(mockGetUrl, mockPayload);
|
||||
fetchMock.post(mockPostUrl, mockPayload);
|
||||
fetchMock.put(mockPutUrl, mockPayload);
|
||||
fetchMock.delete(mockDeleteUrl, mockPayload);
|
||||
fetchMock.delete(mockRequestUrl, mockPayload);
|
||||
fetchMock.get(mockTextUrl, mockTextJsonResponse);
|
||||
fetchMock.post(mockTextUrl, mockTextJsonResponse);
|
||||
});
|
||||
|
||||
it('checks for authentication before every get and post request', async () => {
|
||||
expect.assertions(6);
|
||||
@@ -623,6 +626,8 @@ describe('SupersetClientClass', () => {
|
||||
let createElement: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.get(LOGIN_GLOB, { result: 1234 }, { overwriteRoutes: true });
|
||||
|
||||
client = new SupersetClientClass({ protocol, host });
|
||||
authSpy = jest.spyOn(SupersetClientClass.prototype, 'ensureAuth');
|
||||
await client.init();
|
||||
|
||||
+39
-39
@@ -29,43 +29,41 @@ const corruptObject = new BadObject();
|
||||
/* @ts-expect-error */
|
||||
BadObject.prototype.toString = undefined;
|
||||
|
||||
const mockGetUrl = '/mock/get/url';
|
||||
const mockPostUrl = '/mock/post/url';
|
||||
const mockPutUrl = '/mock/put/url';
|
||||
const mockPatchUrl = '/mock/patch/url';
|
||||
const mockCacheUrl = '/mock/cache/url';
|
||||
const mockNotFound = '/mock/notfound';
|
||||
const mockErrorUrl = '/mock/error/url';
|
||||
const mock503 = '/mock/503';
|
||||
|
||||
const mockGetPayload = { get: 'payload' };
|
||||
const mockPostPayload = { post: 'payload' };
|
||||
const mockPutPayload = { post: 'payload' };
|
||||
const mockPatchPayload = { post: 'payload' };
|
||||
const mockCachePayload = {
|
||||
status: 200,
|
||||
body: 'BODY',
|
||||
headers: { Etag: 'etag' },
|
||||
};
|
||||
const mockErrorPayload = { status: 500, statusText: 'Internal error' };
|
||||
|
||||
describe('callApi()', () => {
|
||||
beforeAll(() => {
|
||||
fetchMock.get(LOGIN_GLOB, { result: '1234' });
|
||||
beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '1234' }));
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.get(mockGetUrl, mockGetPayload);
|
||||
fetchMock.post(mockPostUrl, mockPostPayload);
|
||||
fetchMock.put(mockPutUrl, mockPutPayload);
|
||||
fetchMock.patch(mockPatchUrl, mockPatchPayload);
|
||||
fetchMock.get(mockCacheUrl, mockCachePayload);
|
||||
fetchMock.get(mockNotFound, { status: 404 });
|
||||
fetchMock.get(mock503, { status: 503 });
|
||||
fetchMock.get(mockErrorUrl, () => Promise.reject(mockErrorPayload));
|
||||
});
|
||||
|
||||
afterAll(fetchMock.restore);
|
||||
|
||||
const mockGetUrl = '/mock/get/url';
|
||||
const mockPostUrl = '/mock/post/url';
|
||||
const mockPutUrl = '/mock/put/url';
|
||||
const mockPatchUrl = '/mock/patch/url';
|
||||
const mockCacheUrl = '/mock/cache/url';
|
||||
const mockNotFound = '/mock/notfound';
|
||||
const mockErrorUrl = '/mock/error/url';
|
||||
const mock503 = '/mock/503';
|
||||
|
||||
const mockGetPayload = { get: 'payload' };
|
||||
const mockPostPayload = { post: 'payload' };
|
||||
const mockPutPayload = { post: 'payload' };
|
||||
const mockPatchPayload = { post: 'payload' };
|
||||
const mockCachePayload = {
|
||||
status: 200,
|
||||
body: 'BODY',
|
||||
headers: { Etag: 'etag' },
|
||||
};
|
||||
const mockErrorPayload = { status: 500, statusText: 'Internal error' };
|
||||
|
||||
fetchMock.get(mockGetUrl, mockGetPayload);
|
||||
fetchMock.post(mockPostUrl, mockPostPayload);
|
||||
fetchMock.put(mockPutUrl, mockPutPayload);
|
||||
fetchMock.patch(mockPatchUrl, mockPatchPayload);
|
||||
fetchMock.get(mockCacheUrl, mockCachePayload);
|
||||
fetchMock.get(mockNotFound, { status: 404 });
|
||||
fetchMock.get(mock503, { status: 503 });
|
||||
fetchMock.get(mockErrorUrl, () => Promise.reject(mockErrorPayload));
|
||||
|
||||
afterEach(fetchMock.reset);
|
||||
afterEach(() => fetchMock.reset());
|
||||
|
||||
describe('request config', () => {
|
||||
it('calls the right url with the specified method', async () => {
|
||||
@@ -401,7 +399,7 @@ describe('callApi()', () => {
|
||||
Object.defineProperty(constants, 'CACHE_AVAILABLE', { value: false });
|
||||
|
||||
const firstResponse = await callApi({ url: mockCacheUrl, method: 'GET' });
|
||||
const calls = fetchMock.calls(mockCacheUrl);
|
||||
let calls = fetchMock.calls(mockCacheUrl);
|
||||
expect(calls).toHaveLength(1);
|
||||
const firstBody = await firstResponse.text();
|
||||
expect(firstBody).toEqual('BODY');
|
||||
@@ -410,6 +408,7 @@ describe('callApi()', () => {
|
||||
url: mockCacheUrl,
|
||||
method: 'GET',
|
||||
});
|
||||
calls = fetchMock.calls(mockCacheUrl);
|
||||
const fetchParams = calls[1][1] as RequestInit;
|
||||
expect(calls).toHaveLength(2);
|
||||
// second call should not have If-None-Match header
|
||||
@@ -425,11 +424,12 @@ describe('callApi()', () => {
|
||||
expect.assertions(3);
|
||||
// first call sets the cache
|
||||
await callApi({ url: mockCacheUrl, method: 'GET' });
|
||||
const calls = fetchMock.calls(mockCacheUrl);
|
||||
let calls = fetchMock.calls(mockCacheUrl);
|
||||
expect(calls).toHaveLength(1);
|
||||
|
||||
// second call sends the Etag in the If-None-Match header
|
||||
await callApi({ url: mockCacheUrl, method: 'GET' });
|
||||
calls = fetchMock.calls(mockCacheUrl);
|
||||
const fetchParams = calls[1][1] as RequestInit;
|
||||
const headers = { 'If-None-Match': 'etag' };
|
||||
expect(calls).toHaveLength(2);
|
||||
@@ -442,8 +442,7 @@ describe('callApi()', () => {
|
||||
expect.assertions(3);
|
||||
// first call sets the cache
|
||||
await callApi({ url: mockCacheUrl, method: 'GET' });
|
||||
const calls = fetchMock.calls(mockCacheUrl);
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(fetchMock.calls(mockCacheUrl)).toHaveLength(1);
|
||||
// second call reuses the cached payload on a 304
|
||||
const mockCachedPayload = { status: 304 };
|
||||
fetchMock.get(mockCacheUrl, mockCachedPayload, { overwriteRoutes: true });
|
||||
@@ -452,7 +451,7 @@ describe('callApi()', () => {
|
||||
url: mockCacheUrl,
|
||||
method: 'GET',
|
||||
});
|
||||
expect(calls).toHaveLength(2);
|
||||
expect(fetchMock.calls(mockCacheUrl)).toHaveLength(2);
|
||||
const secondBody = await secondResponse.text();
|
||||
expect(secondBody).toEqual('BODY');
|
||||
});
|
||||
@@ -641,6 +640,7 @@ describe('callApi()', () => {
|
||||
it('should ignore "null" postPayload string', async () => {
|
||||
expect.assertions(1);
|
||||
fetchMock.post('/post-null-postpayload', {});
|
||||
fetchMock.post('/post-formdata', {});
|
||||
await callApi({
|
||||
url: '/post-formdata',
|
||||
method: 'POST',
|
||||
|
||||
+6
-7
@@ -27,16 +27,15 @@ import * as rejectAfterTimeout from '../../../src/connection/callApi/rejectAfter
|
||||
|
||||
import { LOGIN_GLOB } from '../fixtures/constants';
|
||||
|
||||
const mockGetUrl = '/mock/get/url';
|
||||
const mockGetPayload = { get: 'payload' };
|
||||
|
||||
describe('callApiAndParseWithTimeout()', () => {
|
||||
beforeAll(() => {
|
||||
fetchMock.get(LOGIN_GLOB, { result: '1234' });
|
||||
});
|
||||
beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '1234' }));
|
||||
|
||||
afterAll(fetchMock.restore);
|
||||
beforeEach(() => fetchMock.get(mockGetUrl, mockGetPayload));
|
||||
|
||||
const mockGetUrl = '/mock/get/url';
|
||||
const mockGetPayload = { get: 'payload' };
|
||||
fetchMock.get(mockGetUrl, mockGetPayload);
|
||||
afterAll(() => fetchMock.restore());
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
|
||||
+8
-6
@@ -27,7 +27,7 @@ describe('parseResponse()', () => {
|
||||
fetchMock.get(LOGIN_GLOB, { result: '1234' });
|
||||
});
|
||||
|
||||
afterAll(fetchMock.restore);
|
||||
afterAll(() => fetchMock.restore());
|
||||
|
||||
const mockGetUrl = '/mock/get/url';
|
||||
const mockPostUrl = '/mock/post/url';
|
||||
@@ -38,12 +38,14 @@ describe('parseResponse()', () => {
|
||||
const mockPostPayload = { post: 'payload' };
|
||||
const mockErrorPayload = { status: 500, statusText: 'Internal error' };
|
||||
|
||||
fetchMock.get(mockGetUrl, mockGetPayload);
|
||||
fetchMock.post(mockPostUrl, mockPostPayload);
|
||||
fetchMock.get(mockErrorUrl, () => Promise.reject(mockErrorPayload));
|
||||
fetchMock.get(mockNoParseUrl, new Response('test response'));
|
||||
beforeEach(() => {
|
||||
fetchMock.get(mockGetUrl, mockGetPayload);
|
||||
fetchMock.post(mockPostUrl, mockPostPayload);
|
||||
fetchMock.get(mockErrorUrl, () => Promise.reject(mockErrorPayload));
|
||||
fetchMock.get(mockNoParseUrl, new Response('test response'));
|
||||
});
|
||||
|
||||
afterEach(fetchMock.reset);
|
||||
afterEach(() => fetchMock.reset());
|
||||
|
||||
it('returns a Promise', () => {
|
||||
const apiPromise = callApi({ url: mockGetUrl, method: 'GET' });
|
||||
|
||||
+34
-33
@@ -19,40 +19,41 @@
|
||||
|
||||
import { NumberFormatter, createDurationFormatter } from '@superset-ui/core';
|
||||
|
||||
describe('createDurationFormatter()', () => {
|
||||
it('creates an instance of NumberFormatter', () => {
|
||||
const formatter = createDurationFormatter();
|
||||
expect(formatter).toBeInstanceOf(NumberFormatter);
|
||||
test('creates an instance of NumberFormatter', () => {
|
||||
const formatter = createDurationFormatter();
|
||||
expect(formatter).toBeInstanceOf(NumberFormatter);
|
||||
});
|
||||
test('format milliseconds in human readable format with default options', () => {
|
||||
const formatter = createDurationFormatter();
|
||||
expect(formatter(-1000)).toBe('-1s');
|
||||
expect(formatter(0)).toBe('0ms');
|
||||
expect(formatter(1000)).toBe('1s');
|
||||
expect(formatter(1337)).toBe('1.3s');
|
||||
expect(formatter(10500)).toBe('10.5s');
|
||||
expect(formatter(60 * 1000)).toBe('1m');
|
||||
expect(formatter(90 * 1000)).toBe('1m 30s');
|
||||
});
|
||||
test('format seconds in human readable format with default options', () => {
|
||||
const formatter = createDurationFormatter({ multiplier: 1000 });
|
||||
expect(formatter(-0.5)).toBe('-500ms');
|
||||
expect(formatter(0.5)).toBe('500ms');
|
||||
expect(formatter(1)).toBe('1s');
|
||||
expect(formatter(30)).toBe('30s');
|
||||
expect(formatter(60)).toBe('1m');
|
||||
expect(formatter(90)).toBe('1m 30s');
|
||||
});
|
||||
test('format milliseconds in human readable format with additional pretty-ms options', () => {
|
||||
const colonNotationFormatter = createDurationFormatter({
|
||||
colonNotation: true,
|
||||
});
|
||||
it('format milliseconds in human readable format with default options', () => {
|
||||
const formatter = createDurationFormatter();
|
||||
expect(formatter(0)).toBe('0ms');
|
||||
expect(formatter(1000)).toBe('1s');
|
||||
expect(formatter(1337)).toBe('1.3s');
|
||||
expect(formatter(10500)).toBe('10.5s');
|
||||
expect(formatter(60 * 1000)).toBe('1m');
|
||||
expect(formatter(90 * 1000)).toBe('1m 30s');
|
||||
expect(colonNotationFormatter(-10500)).toBe('-0:10.5');
|
||||
expect(colonNotationFormatter(10500)).toBe('0:10.5');
|
||||
const zeroDecimalFormatter = createDurationFormatter({
|
||||
secondsDecimalDigits: 0,
|
||||
});
|
||||
it('format seconds in human readable format with default options', () => {
|
||||
const formatter = createDurationFormatter({ multiplier: 1000 });
|
||||
expect(formatter(0.5)).toBe('500ms');
|
||||
expect(formatter(1)).toBe('1s');
|
||||
expect(formatter(30)).toBe('30s');
|
||||
expect(formatter(60)).toBe('1m');
|
||||
expect(formatter(90)).toBe('1m 30s');
|
||||
expect(zeroDecimalFormatter(10500)).toBe('10s');
|
||||
const subMillisecondFormatter = createDurationFormatter({
|
||||
formatSubMilliseconds: true,
|
||||
});
|
||||
it('format milliseconds in human readable format with additional pretty-ms options', () => {
|
||||
const colonNotationFormatter = createDurationFormatter({
|
||||
colonNotation: true,
|
||||
});
|
||||
expect(colonNotationFormatter(10500)).toBe('0:10.5');
|
||||
const zeroDecimalFormatter = createDurationFormatter({
|
||||
secondsDecimalDigits: 0,
|
||||
});
|
||||
expect(zeroDecimalFormatter(10500)).toBe('10s');
|
||||
const subMillisecondFormatter = createDurationFormatter({
|
||||
formatSubMilliseconds: true,
|
||||
});
|
||||
expect(subMillisecondFormatter(100.40008)).toBe('100ms 400µs 80ns');
|
||||
});
|
||||
expect(subMillisecondFormatter(100.40008)).toBe('100ms 400µs 80ns');
|
||||
});
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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 { NumberFormatter, createMemoryFormatter } from '@superset-ui/core';
|
||||
|
||||
test('creates an instance of MemoryFormatter', () => {
|
||||
const formatter = createMemoryFormatter();
|
||||
expect(formatter).toBeInstanceOf(NumberFormatter);
|
||||
});
|
||||
|
||||
test('formats bytes in human readable format with default options', () => {
|
||||
const formatter = createMemoryFormatter();
|
||||
expect(formatter(0)).toBe('0B');
|
||||
expect(formatter(50)).toBe('50B');
|
||||
expect(formatter(555)).toBe('555B');
|
||||
expect(formatter(1000)).toBe('1kB');
|
||||
expect(formatter(1111)).toBe('1.11kB');
|
||||
expect(formatter(1024)).toBe('1.02kB');
|
||||
expect(formatter(1337)).toBe('1.34kB');
|
||||
expect(formatter(1999)).toBe('2kB');
|
||||
expect(formatter(10 * 1000)).toBe('10kB');
|
||||
expect(formatter(100 * 1000)).toBe('100kB');
|
||||
expect(formatter(Math.pow(1000, 2))).toBe('1MB');
|
||||
expect(formatter(Math.pow(1000, 3))).toBe('1GB');
|
||||
expect(formatter(Math.pow(1000, 4))).toBe('1TB');
|
||||
expect(formatter(Math.pow(1000, 5))).toBe('1PB');
|
||||
expect(formatter(Math.pow(1000, 6))).toBe('1EB');
|
||||
expect(formatter(Math.pow(1000, 7))).toBe('1ZB');
|
||||
expect(formatter(Math.pow(1000, 8))).toBe('1YB');
|
||||
expect(formatter(Math.pow(1000, 9))).toBe('1RB');
|
||||
expect(formatter(Math.pow(1000, 10))).toBe('1QB');
|
||||
expect(formatter(Math.pow(1000, 11))).toBe('1000QB');
|
||||
expect(formatter(Math.pow(1000, 12))).toBe('1000000QB');
|
||||
});
|
||||
|
||||
test('formats negative bytes in human readable format with default options', () => {
|
||||
const formatter = createMemoryFormatter();
|
||||
expect(formatter(-50)).toBe('-50B');
|
||||
});
|
||||
|
||||
test('formats float bytes in human readable format with default options', () => {
|
||||
const formatter = createMemoryFormatter();
|
||||
expect(formatter(10.666)).toBe('10.67B');
|
||||
expect(formatter(1200.666)).toBe('1.2kB');
|
||||
});
|
||||
|
||||
test('formats bytes in human readable format with additional binary option', () => {
|
||||
const formatter = createMemoryFormatter({ binary: true });
|
||||
expect(formatter(0)).toBe('0B');
|
||||
expect(formatter(50)).toBe('50B');
|
||||
expect(formatter(555)).toBe('555B');
|
||||
expect(formatter(1000)).toBe('1000B');
|
||||
expect(formatter(1111)).toBe('1.08KiB');
|
||||
expect(formatter(1024)).toBe('1KiB');
|
||||
expect(formatter(1337)).toBe('1.31KiB');
|
||||
expect(formatter(2047)).toBe('2KiB');
|
||||
expect(formatter(10 * 1024)).toBe('10KiB');
|
||||
expect(formatter(100 * 1024)).toBe('100KiB');
|
||||
expect(formatter(Math.pow(1024, 2))).toBe('1MiB');
|
||||
expect(formatter(Math.pow(1024, 3))).toBe('1GiB');
|
||||
expect(formatter(Math.pow(1024, 4))).toBe('1TiB');
|
||||
expect(formatter(Math.pow(1024, 5))).toBe('1PiB');
|
||||
expect(formatter(Math.pow(1024, 6))).toBe('1EiB');
|
||||
expect(formatter(Math.pow(1024, 7))).toBe('1ZiB');
|
||||
expect(formatter(Math.pow(1024, 8))).toBe('1YiB');
|
||||
expect(formatter(Math.pow(1024, 9))).toBe('1024YiB');
|
||||
expect(formatter(Math.pow(1024, 10))).toBe('1048576YiB');
|
||||
});
|
||||
|
||||
test('formats bytes in human readable format with additional decimals option', () => {
|
||||
const formatter0decimals = createMemoryFormatter({ decimals: 0 });
|
||||
expect(formatter0decimals(0)).toBe('0B');
|
||||
expect(formatter0decimals(1111)).toBe('1kB');
|
||||
|
||||
const formatter3decimals = createMemoryFormatter({ decimals: 3 });
|
||||
expect(formatter3decimals(0)).toBe('0B');
|
||||
expect(formatter3decimals(1111)).toBe('1.111kB');
|
||||
});
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
createD3NumberFormatter,
|
||||
createDurationFormatter,
|
||||
createSiAtMostNDigitFormatter,
|
||||
createMemoryFormatter,
|
||||
formatNumber,
|
||||
getNumberFormatter,
|
||||
getNumberFormatterRegistry,
|
||||
@@ -35,6 +36,7 @@ describe('index', () => {
|
||||
createD3NumberFormatter,
|
||||
createDurationFormatter,
|
||||
createSiAtMostNDigitFormatter,
|
||||
createMemoryFormatter,
|
||||
formatNumber,
|
||||
getNumberFormatter,
|
||||
getNumberFormatterRegistry,
|
||||
|
||||
+2
-2
@@ -22,9 +22,9 @@ import { getDatasourceMetadata } from '../../../../src/query/api/legacy';
|
||||
import setupClientForTest from '../setupClientForTest';
|
||||
|
||||
describe('getFormData()', () => {
|
||||
beforeAll(setupClientForTest);
|
||||
beforeAll(() => setupClientForTest());
|
||||
|
||||
afterEach(fetchMock.restore);
|
||||
afterEach(() => fetchMock.restore());
|
||||
|
||||
it('returns datasource metadata for given datasource key', () => {
|
||||
const mockData = {
|
||||
|
||||
+2
-2
@@ -22,9 +22,9 @@ import { getFormData } from '../../../../src/query/api/legacy';
|
||||
import setupClientForTest from '../setupClientForTest';
|
||||
|
||||
describe('getFormData()', () => {
|
||||
beforeAll(setupClientForTest);
|
||||
beforeAll(() => setupClientForTest());
|
||||
|
||||
afterEach(fetchMock.restore);
|
||||
afterEach(() => fetchMock.restore());
|
||||
|
||||
const mockData = {
|
||||
datasource: '1__table',
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies -- The below fetch-mock import shouldn't be considered as direct dependency
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ import { buildQueryContext, ApiV1 } from '@superset-ui/core';
|
||||
import setupClientForTest from '../setupClientForTest';
|
||||
|
||||
describe('API v1 > getChartData()', () => {
|
||||
beforeAll(setupClientForTest);
|
||||
afterEach(fetchMock.restore);
|
||||
beforeAll(() => setupClientForTest());
|
||||
afterEach(() => fetchMock.restore());
|
||||
|
||||
it('returns a promise of ChartDataResponse', async () => {
|
||||
const response = {
|
||||
|
||||
@@ -22,8 +22,8 @@ import { makeApi, SupersetApiError } from '../../../../src/query';
|
||||
import setupClientForTest from '../setupClientForTest';
|
||||
|
||||
describe('makeApi()', () => {
|
||||
beforeAll(setupClientForTest);
|
||||
afterEach(fetchMock.restore);
|
||||
beforeAll(() => setupClientForTest());
|
||||
afterEach(() => fetchMock.restore());
|
||||
|
||||
it('should expose method and endpoint', () => {
|
||||
const api = makeApi({
|
||||
|
||||
@@ -24,6 +24,10 @@ import {
|
||||
FilterWithDataMask,
|
||||
Divider,
|
||||
isNativeFilterWithDataMask,
|
||||
isAppliedCrossFilterType,
|
||||
isAppliedNativeFilterType,
|
||||
AppliedCrossFilterType,
|
||||
AppliedNativeFilterType,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
const filter: Filter = {
|
||||
@@ -51,6 +55,20 @@ const filterDivider: Divider = {
|
||||
description: 'Divider description.',
|
||||
};
|
||||
|
||||
const appliedCrossFilter: AppliedCrossFilterType = {
|
||||
filterType: undefined,
|
||||
targets: [1, 2],
|
||||
scope: [1, 2],
|
||||
values: null,
|
||||
};
|
||||
|
||||
const appliedNativeFilter: AppliedNativeFilterType = {
|
||||
filterType: 'filter_select',
|
||||
scope: [1, 2],
|
||||
targets: [{}],
|
||||
values: null,
|
||||
};
|
||||
|
||||
test('filter type guard', () => {
|
||||
expect(isNativeFilter(filter)).toBeTruthy();
|
||||
expect(isNativeFilter(filterWithDataMask)).toBeTruthy();
|
||||
@@ -68,3 +86,13 @@ test('filter divider type guard', () => {
|
||||
expect(isFilterDivider(filterWithDataMask)).toBeFalsy();
|
||||
expect(isFilterDivider(filterDivider)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('applied cross filter type guard', () => {
|
||||
expect(isAppliedCrossFilterType(appliedCrossFilter)).toBeTruthy();
|
||||
expect(isAppliedCrossFilterType(appliedNativeFilter)).toBeFalsy();
|
||||
});
|
||||
|
||||
test('applied native filter type guard', () => {
|
||||
expect(isAppliedNativeFilterType(appliedNativeFilter)).toBeTruthy();
|
||||
expect(isAppliedNativeFilterType(appliedCrossFilter)).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { mount } from 'enzyme';
|
||||
import {
|
||||
styled,
|
||||
supersetTheme,
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
EmotionCacheProvider,
|
||||
emotionCache,
|
||||
} from '@superset-ui/core';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
describe('@superset-ui/style package', () => {
|
||||
it('exports a theme', () => {
|
||||
@@ -50,8 +50,8 @@ describe('@superset-ui/style package', () => {
|
||||
expect(useTheme()).toStrictEqual(supersetTheme);
|
||||
return <div>test</div>;
|
||||
}
|
||||
mount(<ThemeUser />, {
|
||||
wrappingComponent: ({ children }) => (
|
||||
render(<ThemeUser />, {
|
||||
wrapper: ({ children }) => (
|
||||
<EmotionCacheProvider value={emotionCache}>
|
||||
<ThemeProvider theme={supersetTheme}>{children}</ThemeProvider>
|
||||
</EmotionCacheProvider>
|
||||
@@ -64,8 +64,8 @@ describe('@superset-ui/style package', () => {
|
||||
expect(useTheme).toThrow(/could not find a ThemeContext/);
|
||||
return <div>test</div>;
|
||||
}
|
||||
mount(<ThemeUser />, {
|
||||
wrappingComponent: ({ children }) => <div>{children}</div>,
|
||||
render(<ThemeUser />, {
|
||||
wrapper: ({ children }) => <div>{children}</div>,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+10
-10
@@ -25,7 +25,7 @@ import {
|
||||
formatTimeRangeComparison,
|
||||
} from '../../src/time-comparison/fetchTimeRange';
|
||||
|
||||
afterEach(fetchMock.restore);
|
||||
afterEach(() => fetchMock.restore());
|
||||
|
||||
test('generates proper time range string', () => {
|
||||
expect(
|
||||
@@ -56,7 +56,7 @@ test('generates a readable time range', () => {
|
||||
});
|
||||
|
||||
test('returns a formatted time range from response', async () => {
|
||||
fetchMock.get("glob:*/api/v1/time_range/?q='Last+day'", {
|
||||
fetchMock.get('glob:*/api/v1/time_range/?q=%27Last+day%27', {
|
||||
result: [
|
||||
{
|
||||
since: '2021-04-13T00:00:00',
|
||||
@@ -73,7 +73,7 @@ test('returns a formatted time range from response', async () => {
|
||||
});
|
||||
|
||||
test('returns a formatted time range from empty response', async () => {
|
||||
fetchMock.get("glob:*/api/v1/time_range/?q='Last+day'", {
|
||||
fetchMock.get('glob:*/api/v1/time_range/?q=%27Last+day%27', {
|
||||
result: [],
|
||||
});
|
||||
|
||||
@@ -84,7 +84,7 @@ test('returns a formatted time range from empty response', async () => {
|
||||
});
|
||||
|
||||
test('returns a formatted error message from response', async () => {
|
||||
fetchMock.getOnce("glob:*/api/v1/time_range/?q='Last+day'", {
|
||||
fetchMock.get('glob:*/api/v1/time_range/?q=%27Last+day%27', {
|
||||
throws: new Response(JSON.stringify({ message: 'Network error' })),
|
||||
});
|
||||
let timeRange = await fetchTimeRange('Last day');
|
||||
@@ -92,8 +92,8 @@ test('returns a formatted error message from response', async () => {
|
||||
error: 'Network error',
|
||||
});
|
||||
|
||||
fetchMock.getOnce(
|
||||
"glob:*/api/v1/time_range/?q='Last+day'",
|
||||
fetchMock.get(
|
||||
'glob:*/api/v1/time_range/?q=%27Last+day%27',
|
||||
{
|
||||
throws: new Error('Internal Server Error'),
|
||||
},
|
||||
@@ -104,8 +104,8 @@ test('returns a formatted error message from response', async () => {
|
||||
error: 'Internal Server Error',
|
||||
});
|
||||
|
||||
fetchMock.getOnce(
|
||||
"glob:*/api/v1/time_range/?q='Last+day'",
|
||||
fetchMock.get(
|
||||
'glob:*/api/v1/time_range/?q=%27Last+day%27',
|
||||
{
|
||||
throws: new Response(JSON.stringify({ statusText: 'Network error' }), {
|
||||
statusText: 'Network error',
|
||||
@@ -117,11 +117,11 @@ test('returns a formatted error message from response', async () => {
|
||||
expect(timeRange).toEqual({
|
||||
error: 'Network error',
|
||||
});
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
test('fetchTimeRange with shift', async () => {
|
||||
fetchMock.getOnce(
|
||||
"glob:*/api/v1/time_range/?q=!((timeRange:'Last+day'),(shift%3A'last%20month'%2CtimeRange%3A'Last%20day'))",
|
||||
'glob:*/api/v1/time_range/?q=!((timeRange:%27Last+day%27),(shift%3A%27last%20month%27%2CtimeRange%3A%27Last%20day%27))',
|
||||
{
|
||||
result: [
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"dependencies": {
|
||||
"@data-ui/event-flow": "^0.0.84",
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"@storybook/addon-actions": "8.1.11",
|
||||
@@ -41,7 +41,7 @@
|
||||
"@storybook/addon-links": "8.1.11",
|
||||
"@storybook/react": "8.1.11",
|
||||
"@storybook/types": "8.1.11",
|
||||
"@types/react-loadable": "^5.5.3",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
"antd": "4.10.3",
|
||||
"core-js": "3.38.1",
|
||||
"gh-pages": "^5.0.0",
|
||||
@@ -58,7 +58,7 @@
|
||||
"@babel/preset-env": "^7.23.9",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@storybook/react-webpack5": "8.1.11",
|
||||
"@storybook/react-webpack5": "8.2.9",
|
||||
"babel-loader": "^9.1.3",
|
||||
"fork-ts-checker-webpack-plugin": "^9.0.2",
|
||||
"ts-loader": "^9.5.1",
|
||||
|
||||
@@ -57,6 +57,6 @@ To update the country maps or add a new country, run scripts in the Jupyter note
|
||||
`scripts/Country Map GeoJSON Generator.ipynb`.
|
||||
|
||||
```bash
|
||||
pip install jupyter notebook
|
||||
pip install geopandas shapely matplotlib notebook
|
||||
jupyter notebook
|
||||
```
|
||||
|
||||
+138
-13
File diff suppressed because one or more lines are too long
+9
-9
File diff suppressed because one or more lines are too long
@@ -24,6 +24,12 @@ import {
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Time'),
|
||||
expanded: true,
|
||||
description: t('Time related form attributes'),
|
||||
controlSetRows: [['granularity_sqla'], ['time_range']],
|
||||
},
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1",
|
||||
"react-map-gl": "^6.1.19",
|
||||
"supercluster": "^4.1.1",
|
||||
"supercluster": "^8.0.1",
|
||||
"viewport-mercator-project": "^6.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import supercluster from 'supercluster';
|
||||
import Supercluster from 'supercluster';
|
||||
import { DEFAULT_POINT_RADIUS, DEFAULT_MAX_ZOOM } from './MapBox';
|
||||
|
||||
const NOOP = () => {};
|
||||
@@ -72,7 +72,7 @@ export default function transformProps(chartProps) {
|
||||
/* eslint-enable no-param-reassign */
|
||||
};
|
||||
}
|
||||
const clusterer = supercluster(opts);
|
||||
const clusterer = new Supercluster(opts);
|
||||
clusterer.load(geoJSON.features);
|
||||
|
||||
return {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1"
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"d3-array": "^1.2.4",
|
||||
"d3-color": "^1.4.1",
|
||||
"d3-scale": "^3.0.0",
|
||||
"deck.gl": "9.0.24",
|
||||
"deck.gl": "9.0.28",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
"mousetrap": "^1.6.5",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@data-ui/xy-chart": "^0.0.84",
|
||||
"d3": "^3.5.17",
|
||||
"d3-tip": "^0.9.1",
|
||||
"dompurify": "^3.1.0",
|
||||
"dompurify": "^3.1.3",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"d3-array": "^1.2.0",
|
||||
"echarts": "^5.5.1",
|
||||
"echarts": "^5.4.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1"
|
||||
},
|
||||
|
||||
@@ -25,7 +25,6 @@ export default function buildQuery(formData: HistogramFormData) {
|
||||
return buildQueryContext(formData, baseQueryObject => [
|
||||
{
|
||||
...baseQueryObject,
|
||||
extras: { where: `${column} IS NOT NULL` },
|
||||
columns: [...groupby, column],
|
||||
post_processing: [histogramOperator(formData, baseQueryObject)],
|
||||
metrics: undefined,
|
||||
|
||||
+5
-5
@@ -593,6 +593,7 @@ export default function transformProps(
|
||||
extractForecastValuesFromTooltipParams(forecastValue);
|
||||
|
||||
const keys = Object.keys(forecastValues);
|
||||
let focusedRow;
|
||||
keys.forEach(key => {
|
||||
const value = forecastValues[key];
|
||||
// if there are no dimensions, key is a verbose name of a metric,
|
||||
@@ -627,12 +628,11 @@ export default function transformProps(
|
||||
: tooltipFormatterSecondary,
|
||||
});
|
||||
rows.push(row);
|
||||
if (key === focusedSeries) {
|
||||
focusedRow = rows.length - 1;
|
||||
}
|
||||
});
|
||||
return tooltipHtml(
|
||||
rows,
|
||||
tooltipFormatter(xValue),
|
||||
keys.findIndex(key => key === focusedSeries),
|
||||
);
|
||||
return tooltipHtml(rows, tooltipFormatter(xValue), focusedRow);
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
|
||||
@@ -56,6 +56,22 @@ const radarMetricMaxValue: { name: string; config: ControlFormItemSpec } = {
|
||||
},
|
||||
};
|
||||
|
||||
const radarMetricMinValue: { name: string; config: ControlFormItemSpec } = {
|
||||
name: 'radarMetricMinValue',
|
||||
config: {
|
||||
controlType: 'InputNumber',
|
||||
label: t('Min'),
|
||||
description: t(
|
||||
'The minimum value of metrics. It is an optional configuration. If not set, it will be the minimum value of the data',
|
||||
),
|
||||
defaultValue: '0',
|
||||
width: 120,
|
||||
placeholder: t('auto'),
|
||||
debounceDelay: 400,
|
||||
validators: [validateNumber],
|
||||
},
|
||||
};
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
@@ -164,7 +180,9 @@ const config: ControlPanelConfig = {
|
||||
description: t('Further customize how to display each metric'),
|
||||
renderTrigger: true,
|
||||
configFormLayout: {
|
||||
[GenericDataType.Numeric]: [[radarMetricMaxValue]],
|
||||
[GenericDataType.Numeric]: [
|
||||
[radarMetricMinValue, radarMetricMaxValue],
|
||||
],
|
||||
},
|
||||
shouldMapStateToProps() {
|
||||
return true;
|
||||
@@ -179,11 +197,17 @@ const config: ControlPanelConfig = {
|
||||
}
|
||||
return value.label;
|
||||
});
|
||||
const { colnames: _colnames, coltypes: _coltypes } =
|
||||
chart?.queriesResponse?.[0] ?? {};
|
||||
const colnames: string[] = _colnames || [];
|
||||
const coltypes: GenericDataType[] = _coltypes || [];
|
||||
|
||||
return {
|
||||
queryResponse: chart?.queriesResponse?.[0] as
|
||||
| ChartDataResponseResult
|
||||
| undefined,
|
||||
appliedColumnNames: metricColumn,
|
||||
columnsPropsObject: { colnames, coltypes },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -123,6 +123,7 @@ export default function transformProps(
|
||||
const groupbyLabels = groupby.map(getColumnLabel);
|
||||
|
||||
const metricLabelAndMaxValueMap = new Map<string, number>();
|
||||
const metricLabelAndMinValueMap = new Map<string, number>();
|
||||
const columnsLabelMap = new Map<string, string[]>();
|
||||
const transformedData: RadarSeriesDataItemOption[] = [];
|
||||
data.forEach(datum => {
|
||||
@@ -155,6 +156,21 @@ export default function transformProps(
|
||||
} else {
|
||||
metricLabelAndMaxValueMap.set(metricLabel, value as number);
|
||||
}
|
||||
|
||||
if (metricLabelAndMinValueMap.has(metricLabel)) {
|
||||
metricLabelAndMinValueMap.set(
|
||||
metricLabel,
|
||||
Math.min(
|
||||
value as number,
|
||||
ensureIsInt(
|
||||
metricLabelAndMinValueMap.get(metricLabel),
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
metricLabelAndMinValueMap.set(metricLabel, value as number);
|
||||
}
|
||||
}
|
||||
|
||||
const isFiltered =
|
||||
@@ -199,6 +215,8 @@ export default function transformProps(
|
||||
|
||||
const indicator = metricLabels.map(metricLabel => {
|
||||
const maxValueInControl = columnConfig?.[metricLabel]?.radarMetricMaxValue;
|
||||
const minValueInControl = columnConfig?.[metricLabel]?.radarMetricMinValue;
|
||||
|
||||
// Ensure that 0 is at the center of the polar coordinates
|
||||
const metricValueAsMax =
|
||||
metricLabelAndMaxValueMap.get(metricLabel) === 0
|
||||
@@ -206,9 +224,23 @@ export default function transformProps(
|
||||
: metricLabelAndMaxValueMap.get(metricLabel);
|
||||
const max =
|
||||
maxValueInControl === null ? metricValueAsMax : maxValueInControl;
|
||||
|
||||
let min: number;
|
||||
// If the min value doesn't exist, set it to 0 (default),
|
||||
// if it is null, set it to the min value of the data,
|
||||
// otherwise, use the value from the control
|
||||
if (minValueInControl === undefined) {
|
||||
min = 0;
|
||||
} else if (minValueInControl === null) {
|
||||
min = metricLabelAndMinValueMap.get(metricLabel) || 0;
|
||||
} else {
|
||||
min = minValueInControl;
|
||||
}
|
||||
|
||||
return {
|
||||
name: metricLabel,
|
||||
max,
|
||||
min,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,10 @@ import {
|
||||
} from '../types';
|
||||
import { DEFAULT_LEGEND_FORM_DATA } from '../constants';
|
||||
|
||||
type RadarColumnConfig = Record<string, { radarMetricMaxValue?: number }>;
|
||||
type RadarColumnConfig = Record<
|
||||
string,
|
||||
{ radarMetricMaxValue?: number; radarMetricMinValue?: number }
|
||||
>;
|
||||
|
||||
export type EchartsRadarFormData = QueryFormData &
|
||||
LegendFormData & {
|
||||
|
||||
@@ -569,6 +569,7 @@ export default function transformProps(
|
||||
const showTotal = Boolean(isMultiSeries) && richTooltip && !isForecast;
|
||||
const showPercentage = showTotal && !forcePercentFormatter;
|
||||
const keys = Object.keys(forecastValues);
|
||||
let focusedRow;
|
||||
keys.forEach(key => {
|
||||
const value = forecastValues[key];
|
||||
if (value.observation === 0 && stack) {
|
||||
@@ -583,10 +584,15 @@ export default function transformProps(
|
||||
row.push(percentFormatter.format(value.observation / (total || 1)));
|
||||
}
|
||||
rows.push(row);
|
||||
if (key === focusedSeries) {
|
||||
focusedRow = rows.length - 1;
|
||||
}
|
||||
});
|
||||
if (stack) {
|
||||
keys.reverse();
|
||||
rows.reverse();
|
||||
if (focusedRow !== undefined) {
|
||||
focusedRow = rows.length - focusedRow - 1;
|
||||
}
|
||||
}
|
||||
if (showTotal) {
|
||||
const totalRow = ['Total', formatter.format(total)];
|
||||
@@ -595,11 +601,7 @@ export default function transformProps(
|
||||
}
|
||||
rows.push(totalRow);
|
||||
}
|
||||
return tooltipHtml(
|
||||
rows,
|
||||
tooltipFormatter(xValue),
|
||||
keys.findIndex(key => key === focusedSeries),
|
||||
);
|
||||
return tooltipHtml(rows, tooltipFormatter(xValue), focusedRow);
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
|
||||
@@ -55,6 +55,8 @@ import {
|
||||
ToolboxComponent,
|
||||
GraphicComponent,
|
||||
AriaComponent,
|
||||
MarkAreaComponent,
|
||||
MarkLineComponent,
|
||||
} from 'echarts/components';
|
||||
import { LabelLayout } from 'echarts/features';
|
||||
import { EchartsHandler, EchartsProps, EchartsStylesProps } from '../types';
|
||||
@@ -84,6 +86,8 @@ use([
|
||||
DataZoomComponent,
|
||||
GraphicComponent,
|
||||
GridComponent,
|
||||
MarkAreaComponent,
|
||||
MarkLineComponent,
|
||||
LegendComponent,
|
||||
ToolboxComponent,
|
||||
TooltipComponent,
|
||||
|
||||
@@ -53,8 +53,8 @@ export const LABEL_POSITION: [LabelPositionEnum, string][] = [
|
||||
[LabelPositionEnum.Right, 'Right'],
|
||||
[LabelPositionEnum.Bottom, 'Bottom'],
|
||||
[LabelPositionEnum.Inside, 'Inside'],
|
||||
[LabelPositionEnum.InsideBottomLeft, 'Inside left'],
|
||||
[LabelPositionEnum.InsideBottomRight, 'Inside right'],
|
||||
[LabelPositionEnum.InsideLeft, 'Inside left'],
|
||||
[LabelPositionEnum.InsideRight, 'Inside right'],
|
||||
[LabelPositionEnum.InsideTop, 'Inside top'],
|
||||
[LabelPositionEnum.InsideBottom, 'Inside bottom'],
|
||||
[LabelPositionEnum.InsideTopLeft, 'Inside top left'],
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"handlebars": "^4.7.7",
|
||||
"handlebars": "^4.7.8",
|
||||
"just-handlebars-helpers": "^1.0.19"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -40,10 +40,10 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/dom": "^8.20.1",
|
||||
"@testing-library/jest-dom": "^5.11.6",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^5.0.3",
|
||||
"@testing-library/user-event": "^12.7.0",
|
||||
"@testing-library/react-hooks": "*",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/classnames": "*",
|
||||
"@types/react": "*",
|
||||
"match-sorter": "^6.3.3",
|
||||
|
||||
@@ -26,6 +26,7 @@ export default {
|
||||
{
|
||||
id: 'DefaultsID',
|
||||
filterType: 'filter_select',
|
||||
chartsInScope: [],
|
||||
targets: [{}],
|
||||
cascadeParentIds: [],
|
||||
},
|
||||
|
||||
@@ -133,6 +133,7 @@ export const singleNativeFiltersState = {
|
||||
id: [NATIVE_FILTER_ID],
|
||||
name: 'eth',
|
||||
type: 'text',
|
||||
filterType: 'filter_select',
|
||||
targets: [{ datasetId: 13, column: { name: 'ethnic_minority' } }],
|
||||
defaultDataMask: {
|
||||
filterState: {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import '@testing-library/jest-dom';
|
||||
import { ReactNode, ReactElement } from 'react';
|
||||
import {
|
||||
render,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { isValidElement } from 'react';
|
||||
import { styledMount as mount } from 'spec/helpers/theming';
|
||||
import { render } from 'spec/helpers/testing-library';
|
||||
import ColumnElement from 'src/SqlLab/components/ColumnElement';
|
||||
import { mockedActions, table } from 'src/SqlLab/fixtures';
|
||||
|
||||
@@ -30,19 +30,25 @@ describe('ColumnElement', () => {
|
||||
expect(isValidElement(<ColumnElement {...mockedProps} />)).toBe(true);
|
||||
});
|
||||
it('renders a proper primary key', () => {
|
||||
const wrapper = mount(<ColumnElement column={table.columns[0]} />);
|
||||
expect(wrapper.find('i.fa-key')).toExist();
|
||||
expect(wrapper.find('.col-name').first().text()).toBe('id');
|
||||
const { container } = render(<ColumnElement column={table.columns[0]} />);
|
||||
expect(container.querySelector('i.fa-key')).toBeInTheDocument();
|
||||
expect(container.querySelector('.col-name')?.firstChild).toHaveTextContent(
|
||||
'id',
|
||||
);
|
||||
});
|
||||
it('renders a multi-key column', () => {
|
||||
const wrapper = mount(<ColumnElement column={table.columns[1]} />);
|
||||
expect(wrapper.find('i.fa-link')).toExist();
|
||||
expect(wrapper.find('i.fa-bookmark')).toExist();
|
||||
expect(wrapper.find('.col-name').first().text()).toBe('first_name');
|
||||
const { container } = render(<ColumnElement column={table.columns[1]} />);
|
||||
expect(container.querySelector('i.fa-link')).toBeInTheDocument();
|
||||
expect(container.querySelector('i.fa-bookmark')).toBeInTheDocument();
|
||||
expect(container.querySelector('.col-name')?.firstChild).toHaveTextContent(
|
||||
'first_name',
|
||||
);
|
||||
});
|
||||
it('renders a column with no keys', () => {
|
||||
const wrapper = mount(<ColumnElement column={table.columns[2]} />);
|
||||
expect(wrapper.find('i')).not.toExist();
|
||||
expect(wrapper.find('.col-name').first().text()).toBe('last_name');
|
||||
const { container } = render(<ColumnElement column={table.columns[2]} />);
|
||||
expect(container.querySelector('i')).not.toBeInTheDocument();
|
||||
expect(container.querySelector('.col-name')?.firstChild).toHaveTextContent(
|
||||
'last_name',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
css,
|
||||
getNumberFormatter,
|
||||
getExtensionsRegistry,
|
||||
ErrorLevel,
|
||||
ErrorTypeEnum,
|
||||
} from '@superset-ui/core';
|
||||
import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace';
|
||||
@@ -540,18 +541,33 @@ const ResultSet = ({
|
||||
}
|
||||
|
||||
if (query.state === QueryState.Failed) {
|
||||
const errors = [];
|
||||
if (query.errorMessage) {
|
||||
errors.push({
|
||||
error_type: ErrorTypeEnum.GENERIC_DB_ENGINE_ERROR,
|
||||
extra: {},
|
||||
level: 'error' as ErrorLevel,
|
||||
message: query.errorMessage,
|
||||
});
|
||||
}
|
||||
errors.push(...(query.extra?.errors || []), ...(query.errors || []));
|
||||
|
||||
return (
|
||||
<ResultlessStyles>
|
||||
<ErrorMessageWithStackTrace
|
||||
title={t('Database error')}
|
||||
error={query?.extra?.errors?.[0] || query?.errors?.[0]}
|
||||
subtitle={<MonospaceDiv>{query.errorMessage}</MonospaceDiv>}
|
||||
copyText={query.errorMessage || undefined}
|
||||
link={query.link}
|
||||
source="sqllab"
|
||||
/>
|
||||
{(query?.extra?.errors?.[0] || query?.errors?.[0])?.error_type ===
|
||||
ErrorTypeEnum.FRONTEND_TIMEOUT_ERROR ? (
|
||||
{errors.map((error, index) => (
|
||||
<ErrorMessageWithStackTrace
|
||||
key={index}
|
||||
title={t('Database error')}
|
||||
error={error}
|
||||
subtitle={<MonospaceDiv>{error.message}</MonospaceDiv>}
|
||||
copyText={error.message || undefined}
|
||||
link={query.link}
|
||||
source="sqllab"
|
||||
/>
|
||||
))}
|
||||
{errors.some(
|
||||
error => error?.error_type === ErrorTypeEnum.FRONTEND_TIMEOUT_ERROR,
|
||||
) ? (
|
||||
<Button
|
||||
className="sql-result-track-job"
|
||||
buttonSize="small"
|
||||
@@ -630,7 +646,7 @@ const ResultSet = ({
|
||||
: [];
|
||||
const allowHTML = getItem(
|
||||
LocalStorageKeys.SqllabIsRenderHtmlEnabled,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
return (
|
||||
<ResultContainer>
|
||||
|
||||
@@ -24,7 +24,7 @@ import * as uiCore from '@superset-ui/core';
|
||||
import { Provider } from 'react-redux';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import '@testing-library/jest-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import * as utils from 'src/utils/common';
|
||||
import ShareSqlLabQuery from 'src/SqlLab/components/ShareSqlLabQuery';
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
import { render } from 'spec/helpers/testing-library';
|
||||
import SouthPane from 'src/SqlLab/components/SouthPane';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import '@testing-library/jest-dom';
|
||||
import { STATUS_OPTIONS } from 'src/SqlLab/constants';
|
||||
import { initialState, table, defaultQueryEditor } from 'src/SqlLab/fixtures';
|
||||
import { denormalizeTimestamp } from '@superset-ui/core';
|
||||
|
||||
@@ -308,7 +308,7 @@ const SqlEditor: FC<Props> = ({
|
||||
getItem(LocalStorageKeys.SqllabIsAutocompleteEnabled, true),
|
||||
);
|
||||
const [renderHTMLEnabled, setRenderHTMLEnabled] = useState(
|
||||
getItem(LocalStorageKeys.SqllabIsRenderHtmlEnabled, false),
|
||||
getItem(LocalStorageKeys.SqllabIsRenderHtmlEnabled, true),
|
||||
);
|
||||
const [showCreateAsModal, setShowCreateAsModal] = useState(false);
|
||||
const [createAs, setCreateAs] = useState('');
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import AlteredSliceTag, {
|
||||
|
||||
+94
-62
@@ -16,15 +16,18 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
import {
|
||||
ensureIsArray,
|
||||
FeatureFlag,
|
||||
isFeatureEnabled,
|
||||
logging,
|
||||
QueryFormData,
|
||||
styled,
|
||||
t,
|
||||
SqlaFormData,
|
||||
ClientErrorObject,
|
||||
ChartDataResponse,
|
||||
} from '@superset-ui/core';
|
||||
import { PLACEHOLDER_DATASOURCE } from 'src/dashboard/constants';
|
||||
import Loading from 'src/components/Loading';
|
||||
@@ -36,73 +39,98 @@ import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import { isCurrentUserBot } from 'src/utils/isBot';
|
||||
import { ChartSource } from 'src/types/ChartSource';
|
||||
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
|
||||
import { Dispatch } from 'redux';
|
||||
import { Annotation } from 'src/explore/components/controls/AnnotationLayerControl';
|
||||
import ChartRenderer from './ChartRenderer';
|
||||
import { ChartErrorMessage } from './ChartErrorMessage';
|
||||
import { getChartRequiredFieldsMissingMessage } from '../../utils/getChartRequiredFieldsMissingMessage';
|
||||
|
||||
const propTypes = {
|
||||
annotationData: PropTypes.object,
|
||||
actions: PropTypes.object,
|
||||
chartId: PropTypes.number.isRequired,
|
||||
datasource: PropTypes.object,
|
||||
// current chart is included by dashboard
|
||||
dashboardId: PropTypes.number,
|
||||
// original selected values for FilterBox viz
|
||||
// so that FilterBox can pre-populate selected values
|
||||
// only affect UI control
|
||||
initialValues: PropTypes.object,
|
||||
// formData contains chart's own filter parameter
|
||||
// and merged with extra filter that current dashboard applying
|
||||
formData: PropTypes.object.isRequired,
|
||||
labelsColor: PropTypes.object,
|
||||
labelsColorMap: PropTypes.object,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
setControlValue: PropTypes.func,
|
||||
timeout: PropTypes.number,
|
||||
vizType: PropTypes.string.isRequired,
|
||||
triggerRender: PropTypes.bool,
|
||||
force: PropTypes.bool,
|
||||
isFiltersInitialized: PropTypes.bool,
|
||||
// state
|
||||
chartAlert: PropTypes.string,
|
||||
chartStatus: PropTypes.string,
|
||||
chartStackTrace: PropTypes.string,
|
||||
queriesResponse: PropTypes.arrayOf(PropTypes.object),
|
||||
triggerQuery: PropTypes.bool,
|
||||
chartIsStale: PropTypes.bool,
|
||||
errorMessage: PropTypes.node,
|
||||
// dashboard callbacks
|
||||
addFilter: PropTypes.func,
|
||||
onQuery: PropTypes.func,
|
||||
onFilterMenuOpen: PropTypes.func,
|
||||
onFilterMenuClose: PropTypes.func,
|
||||
ownState: PropTypes.object,
|
||||
postTransformProps: PropTypes.func,
|
||||
datasetsStatus: PropTypes.oneOf(['loading', 'error', 'complete']),
|
||||
isInView: PropTypes.bool,
|
||||
emitCrossFilters: PropTypes.bool,
|
||||
};
|
||||
export type ChartErrorType = Partial<ClientErrorObject>;
|
||||
export interface ChartProps {
|
||||
annotationData?: Annotation;
|
||||
actions: Actions;
|
||||
chartId: string;
|
||||
datasource?: {
|
||||
database?: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
dashboardId?: number;
|
||||
initialValues?: object;
|
||||
formData: QueryFormData;
|
||||
labelColors?: string;
|
||||
sharedLabelColors?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
setControlValue: Function;
|
||||
timeout?: number;
|
||||
vizType: string;
|
||||
triggerRender?: boolean;
|
||||
force?: boolean;
|
||||
isFiltersInitialized?: boolean;
|
||||
chartAlert?: string;
|
||||
chartStatus?: string;
|
||||
chartStackTrace?: string;
|
||||
queriesResponse: ChartDataResponse[];
|
||||
triggerQuery?: boolean;
|
||||
chartIsStale?: boolean;
|
||||
errorMessage?: React.ReactNode;
|
||||
addFilter?: (type: string) => void;
|
||||
onQuery?: () => void;
|
||||
onFilterMenuOpen?: (chartId: string, column: string) => void;
|
||||
onFilterMenuClose?: (chartId: string, column: string) => void;
|
||||
ownState: boolean;
|
||||
postTransformProps?: Function;
|
||||
datasetsStatus?: 'loading' | 'error' | 'complete';
|
||||
isInView?: boolean;
|
||||
emitCrossFilters?: boolean;
|
||||
}
|
||||
|
||||
export type Actions = {
|
||||
logEvent(
|
||||
LOG_ACTIONS_RENDER_CHART: string,
|
||||
arg1: {
|
||||
slice_id: string;
|
||||
has_err: boolean;
|
||||
error_details: string;
|
||||
start_offset: number;
|
||||
ts: number;
|
||||
duration: number;
|
||||
},
|
||||
): Dispatch;
|
||||
chartRenderingFailed(
|
||||
arg0: string,
|
||||
chartId: string,
|
||||
arg2: string | null,
|
||||
): Dispatch;
|
||||
postChartFormData(
|
||||
formData: SqlaFormData,
|
||||
arg1: boolean,
|
||||
timeout: number | undefined,
|
||||
chartId: string,
|
||||
dashboardId: number | undefined,
|
||||
ownState: boolean,
|
||||
): Dispatch;
|
||||
};
|
||||
const BLANK = {};
|
||||
const NONEXISTENT_DATASET = t(
|
||||
'The dataset associated with this chart no longer exists',
|
||||
);
|
||||
|
||||
const defaultProps = {
|
||||
const defaultProps: Partial<ChartProps> = {
|
||||
addFilter: () => BLANK,
|
||||
onFilterMenuOpen: () => BLANK,
|
||||
onFilterMenuClose: () => BLANK,
|
||||
initialValues: BLANK,
|
||||
setControlValue() {},
|
||||
setControlValue: () => BLANK,
|
||||
triggerRender: false,
|
||||
dashboardId: null,
|
||||
chartStackTrace: null,
|
||||
dashboardId: undefined,
|
||||
chartStackTrace: undefined,
|
||||
force: false,
|
||||
isInView: true,
|
||||
};
|
||||
|
||||
const Styles = styled.div`
|
||||
const Styles = styled.div<{ height: number; width?: number }>`
|
||||
min-height: ${p => p.height}px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
@@ -150,9 +178,12 @@ const MonospaceDiv = styled.div`
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
`;
|
||||
class Chart extends PureComponent<ChartProps, {}> {
|
||||
static defaultProps = defaultProps;
|
||||
|
||||
class Chart extends PureComponent {
|
||||
constructor(props) {
|
||||
renderStartTime: any;
|
||||
|
||||
constructor(props: ChartProps) {
|
||||
super(props);
|
||||
this.handleRenderContainerFailure =
|
||||
this.handleRenderContainerFailure.bind(this);
|
||||
@@ -182,7 +213,10 @@ class Chart extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
handleRenderContainerFailure(error, info) {
|
||||
handleRenderContainerFailure(
|
||||
error: Error,
|
||||
info: { componentStack: string } | null,
|
||||
) {
|
||||
const { actions, chartId } = this.props;
|
||||
logging.warn(error);
|
||||
actions.chartRenderingFailed(
|
||||
@@ -201,7 +235,7 @@ class Chart extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
renderErrorMessage(queryResponse) {
|
||||
renderErrorMessage(queryResponse: ChartErrorType) {
|
||||
const {
|
||||
chartId,
|
||||
chartAlert,
|
||||
@@ -241,14 +275,14 @@ class Chart extends PureComponent {
|
||||
error={error}
|
||||
subtitle={<MonospaceDiv>{message}</MonospaceDiv>}
|
||||
copyText={message}
|
||||
link={queryResponse ? queryResponse.link : null}
|
||||
link={queryResponse ? queryResponse.link : undefined}
|
||||
source={dashboardId ? ChartSource.Dashboard : ChartSource.Explore}
|
||||
stackTrace={chartStackTrace}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderSpinner(databaseName) {
|
||||
renderSpinner(databaseName: string | undefined) {
|
||||
const message = databaseName
|
||||
? t('Waiting on %s', databaseName)
|
||||
: t('Waiting on database...');
|
||||
@@ -290,12 +324,15 @@ class Chart extends PureComponent {
|
||||
queriesResponse = [],
|
||||
width,
|
||||
} = this.props;
|
||||
|
||||
const databaseName = datasource?.database?.name;
|
||||
|
||||
const isLoading = chartStatus === 'loading';
|
||||
this.renderContainerStartTime = Logger.getTimestamp();
|
||||
|
||||
if (chartStatus === 'failed') {
|
||||
return queriesResponse.map(item => this.renderErrorMessage(item));
|
||||
return queriesResponse.map(item =>
|
||||
this.renderErrorMessage(item as ChartErrorType),
|
||||
);
|
||||
}
|
||||
|
||||
if (errorMessage && ensureIsArray(queriesResponse).length === 0) {
|
||||
@@ -307,7 +344,6 @@ class Chart extends PureComponent {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!isLoading &&
|
||||
!chartAlert &&
|
||||
@@ -354,8 +390,4 @@ class Chart extends PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Chart.propTypes = propTypes;
|
||||
Chart.defaultProps = defaultProps;
|
||||
|
||||
export default Chart;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user