mirror of
https://github.com/apache/superset.git
synced 2026-05-04 07:24:18 +00:00
Compare commits
271 Commits
fix-query-
...
1.5.2rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79cb268279 | ||
|
|
cca31299d3 | ||
|
|
37bdf434bf | ||
|
|
8e7fb96f06 | ||
|
|
85fb2433ab | ||
|
|
9861c2b9e7 | ||
|
|
7ad097371e | ||
|
|
44e3d60c35 | ||
|
|
52e1349acc | ||
|
|
5a3bb82b8d | ||
|
|
a50b0a91fc | ||
|
|
fb0d6d57d6 | ||
|
|
5f3301aadb | ||
|
|
e5de0909c3 | ||
|
|
6bcb9674da | ||
|
|
1e8259a410 | ||
|
|
094b17e8cc | ||
|
|
991a453ff2 | ||
|
|
8e1b2a1354 | ||
|
|
6dee53682e | ||
|
|
4073b58aa2 | ||
|
|
d512e89aa9 | ||
|
|
f8a369d233 | ||
|
|
af5ded3fcb | ||
|
|
48f3eb4273 | ||
|
|
c3604dc4de | ||
|
|
17e10b77ca | ||
|
|
8089afce1e | ||
|
|
d652e0cab2 | ||
|
|
562e3a769b | ||
|
|
c5e2809c2c | ||
|
|
97c6af196e | ||
|
|
bda9044bfd | ||
|
|
4e44f4cc11 | ||
|
|
8becd3e080 | ||
|
|
9ca53b8905 | ||
|
|
2bd89d1705 | ||
|
|
94d4179045 | ||
|
|
054e6b2334 | ||
|
|
397a182c2f | ||
|
|
1050fcbd3c | ||
|
|
387138eb9e | ||
|
|
4fa96d3a3b | ||
|
|
5567828ee8 | ||
|
|
8c8bbfb89f | ||
|
|
2b6e35e039 | ||
|
|
1d141be463 | ||
|
|
87b51c26f4 | ||
|
|
c9e3ca11e2 | ||
|
|
5df14ed8f9 | ||
|
|
2d7d2dd373 | ||
|
|
28aa69628f | ||
|
|
f8b202f670 | ||
|
|
ccf296b786 | ||
|
|
90b08fa095 | ||
|
|
cdc136c70a | ||
|
|
98c4d943da | ||
|
|
3c8e65960f | ||
|
|
d657d813e3 | ||
|
|
95e5b59c38 | ||
|
|
419316e84a | ||
|
|
13e81c683e | ||
|
|
77d6207bed | ||
|
|
ecaecf0b6c | ||
|
|
280ecab0e6 | ||
|
|
5ca126698a | ||
|
|
44eb81e35f | ||
|
|
1e567999d9 | ||
|
|
d99a9a4134 | ||
|
|
c4d24a09a8 | ||
|
|
34e3119c40 | ||
|
|
031fadbed8 | ||
|
|
cc51a84c26 | ||
|
|
2001fb5037 | ||
|
|
96901d6c46 | ||
|
|
dba4610f9b | ||
|
|
f44ed063e8 | ||
|
|
90f49e261e | ||
|
|
3ff9cdeb65 | ||
|
|
3c627bdc61 | ||
|
|
3eece91378 | ||
|
|
af0dc75207 | ||
|
|
840be9972f | ||
|
|
e4cbbdc653 | ||
|
|
a96ff005fd | ||
|
|
70d800dc27 | ||
|
|
99c8f9bd13 | ||
|
|
91bf9bd68b | ||
|
|
e93d64d58e | ||
|
|
3c09690ed2 | ||
|
|
795ed3c719 | ||
|
|
a7ee677154 | ||
|
|
0c78522bfe | ||
|
|
bfa203aee0 | ||
|
|
7aba89c486 | ||
|
|
625555ac7e | ||
|
|
038d114b07 | ||
|
|
ba22905610 | ||
|
|
fb929ab649 | ||
|
|
a70f4dc52f | ||
|
|
1edd5f1343 | ||
|
|
cccec50454 | ||
|
|
495b29a4eb | ||
|
|
5f2ffb3ba4 | ||
|
|
56e78b9ef7 | ||
|
|
ae2763af53 | ||
|
|
fb0ae24c6b | ||
|
|
0c75d9ede0 | ||
|
|
029cf73b75 | ||
|
|
dead7c253c | ||
|
|
25c5e2b4c7 | ||
|
|
2f497190ed | ||
|
|
7af92ed337 | ||
|
|
da4a79f276 | ||
|
|
d65954994c | ||
|
|
0439d8db40 | ||
|
|
549b475a94 | ||
|
|
a6a2def6d3 | ||
|
|
18f82411c9 | ||
|
|
d9377559f6 | ||
|
|
a591eccfc2 | ||
|
|
069d42ed14 | ||
|
|
89b7b3784a | ||
|
|
3b0b60c57f | ||
|
|
a81b1ab374 | ||
|
|
6ccc1c86fa | ||
|
|
99f36f73d2 | ||
|
|
b21d4f841c | ||
|
|
158d442f0c | ||
|
|
baeb36f8c6 | ||
|
|
52f9b718f2 | ||
|
|
bf92b2067e | ||
|
|
d429bbd3b2 | ||
|
|
b51eadd657 | ||
|
|
0664a05afb | ||
|
|
3a9435df72 | ||
|
|
9bc76337cf | ||
|
|
f8a92de75c | ||
|
|
12759ec264 | ||
|
|
f0630a6ea7 | ||
|
|
d4223d7dc4 | ||
|
|
46dbf6c50c | ||
|
|
19ee561092 | ||
|
|
c6a4d75954 | ||
|
|
55aac5a3cb | ||
|
|
712212be6d | ||
|
|
792473f6db | ||
|
|
2a2105c8c8 | ||
|
|
e1964a8dfe | ||
|
|
f3172010d5 | ||
|
|
81a1abfab4 | ||
|
|
dc0153151c | ||
|
|
910679aad5 | ||
|
|
70facad1b2 | ||
|
|
565d83f9ec | ||
|
|
7d239b8958 | ||
|
|
408573d4d6 | ||
|
|
f6346d627b | ||
|
|
fed1c24252 | ||
|
|
c793b7bfe9 | ||
|
|
1953233b68 | ||
|
|
1834a268a1 | ||
|
|
1c53a76bcc | ||
|
|
33beba96e3 | ||
|
|
f1ea0ad56c | ||
|
|
f2541429cd | ||
|
|
3de8370ead | ||
|
|
2651c1d925 | ||
|
|
0522296607 | ||
|
|
823b9b2ff2 | ||
|
|
f91f9f5aae | ||
|
|
a1b9b2946d | ||
|
|
71338cab57 | ||
|
|
28be2311a0 | ||
|
|
ff277e0517 | ||
|
|
7bc6e14151 | ||
|
|
8c102174b8 | ||
|
|
90f4d77422 | ||
|
|
2979ff26cc | ||
|
|
a68b1fd686 | ||
|
|
ca0c12f2b3 | ||
|
|
3f1074e6a3 | ||
|
|
337ea4f59d | ||
|
|
db8b6af7c7 | ||
|
|
a0138af7f8 | ||
|
|
ba57cb410e | ||
|
|
37162a923b | ||
|
|
0598e95b1b | ||
|
|
2e6bd1590d | ||
|
|
20fd7a7cd5 | ||
|
|
fcca027b88 | ||
|
|
2b8b9b366a | ||
|
|
877ee42a1c | ||
|
|
a2fc96a9c3 | ||
|
|
891798b871 | ||
|
|
cb5d814fd7 | ||
|
|
7d0ee284fa | ||
|
|
89edf8a7a9 | ||
|
|
088f6f75ff | ||
|
|
d0c9df4d7b | ||
|
|
36561b7943 | ||
|
|
2193e17940 | ||
|
|
dae9e7c020 | ||
|
|
2c151f352e | ||
|
|
0aba34a807 | ||
|
|
67d7a7b115 | ||
|
|
cd2a958ce3 | ||
|
|
4462cfeb0f | ||
|
|
4c9785f3e3 | ||
|
|
005949f6b9 | ||
|
|
4bff8fe95e | ||
|
|
3729df2653 | ||
|
|
56b1144abd | ||
|
|
253f80ab6d | ||
|
|
e97b123961 | ||
|
|
bc65cf4509 | ||
|
|
3ce4f051d5 | ||
|
|
8bef059624 | ||
|
|
4b8fc06e5f | ||
|
|
29b46d00f9 | ||
|
|
032f560c2c | ||
|
|
219fa570b0 | ||
|
|
05f25d350c | ||
|
|
1658a9f6b4 | ||
|
|
379676e1f6 | ||
|
|
02d9825b11 | ||
|
|
45dc7b5984 | ||
|
|
4e78efcf07 | ||
|
|
1eac8712a4 | ||
|
|
9777e6d148 | ||
|
|
585b032a1a | ||
|
|
fb5d77e404 | ||
|
|
6178f0515a | ||
|
|
8640814a92 | ||
|
|
00a53dae73 | ||
|
|
ea534e2386 | ||
|
|
073be5d74f | ||
|
|
8951e23ac6 | ||
|
|
3500b31551 | ||
|
|
fcf98ec889 | ||
|
|
78e85aed02 | ||
|
|
75afb3a7b6 | ||
|
|
3b2b6674f6 | ||
|
|
5a8cd3477b | ||
|
|
6d2b583fee | ||
|
|
3c90220cf6 | ||
|
|
e8cf9446a6 | ||
|
|
4a91f76798 | ||
|
|
603f43bd95 | ||
|
|
8bfb2f854e | ||
|
|
178736ad27 | ||
|
|
fe0f1de64b | ||
|
|
f7b72d9486 | ||
|
|
1f4ff51322 | ||
|
|
928309841c | ||
|
|
10d382de56 | ||
|
|
46015e2c66 | ||
|
|
da64e7966f | ||
|
|
335a90a5ac | ||
|
|
ee3b3aea5d | ||
|
|
268ce0ae8b | ||
|
|
9375ebfbff | ||
|
|
5b465c2ff1 | ||
|
|
a76ae2edb6 | ||
|
|
419ca75412 | ||
|
|
2fc2e275d4 | ||
|
|
ce61dbc2bc | ||
|
|
77c8987bbe | ||
|
|
b78bced141 | ||
|
|
4167cfa30e | ||
|
|
e20788c0e1 |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -11,4 +11,4 @@
|
||||
/superset-frontend/src/components/Select/ @michael-s-molina @geido
|
||||
|
||||
# Notify Helm Chart maintainers about changes in it
|
||||
/helm/superset/ @craig-rueda
|
||||
/helm/superset/ @craig-rueda @dpgaspar @villebro
|
||||
|
||||
1
.github/workflows/embedded-sdk-test.yml
vendored
1
.github/workflows/embedded-sdk-test.yml
vendored
@@ -20,4 +20,5 @@ jobs:
|
||||
node-version: "16"
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
- run: npm run build
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -108,3 +108,5 @@ release.json
|
||||
messages.mo
|
||||
|
||||
docker/requirements-local.txt
|
||||
|
||||
cache/
|
||||
|
||||
@@ -20,7 +20,7 @@ repos:
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.910
|
||||
rev: v0.941
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [types-all]
|
||||
@@ -41,7 +41,7 @@ repos:
|
||||
- id: trailing-whitespace
|
||||
args: ["--markdown-linebreak-ext=md"]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 19.10b0
|
||||
rev: 22.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
|
||||
1252
CHANGELOG.md
1252
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -106,7 +106,7 @@ This statement thanks the following, on which it draws for content and inspirati
|
||||
|
||||
# Slack Community Guidelines
|
||||
|
||||
If you decide to join the [Community Slack](https://join.slack.com/t/apache-superset/shared_invite/zt-uxbh5g36-AISUtHbzOXcu0BIj7kgUaw), please adhere to the following rules:
|
||||
If you decide to join the [Community Slack](https://join.slack.com/t/apache-superset/shared_invite/zt-16jvzmoi8-sI7jKWp~xc2zYRe~NqiY9Q), please adhere to the following rules:
|
||||
|
||||
**1. Treat everyone in the community with respect.**
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ Finally, never submit a PR that will put master branch in broken state. If the P
|
||||
#### Authoring
|
||||
|
||||
- Fill in all sections of the PR template.
|
||||
- Title the PR with one of the following semantic prefixes (inspired by [Karma](http://karma-runner.github.io/0.10/dev/git-commit-msg.html])):
|
||||
- Title the PR with one of the following semantic prefixes (inspired by [Karma](http://karma-runner.github.io/0.10/dev/git-commit-msg.html)):
|
||||
|
||||
- `feat` (new feature)
|
||||
- `fix` (bug fix)
|
||||
@@ -412,7 +412,7 @@ You also need to install MySQL or [MariaDB](https://mariadb.com/downloads).
|
||||
|
||||
Ensure that you are using Python version 3.7 or 3.8, then proceed with:
|
||||
|
||||
````bash
|
||||
```bash
|
||||
# Create a virtual environment and activate it (recommended)
|
||||
python3 -m venv venv # setup a python3 virtualenv
|
||||
source venv/bin/activate
|
||||
@@ -457,7 +457,7 @@ $ make superset
|
||||
|
||||
# Setup pre-commit only
|
||||
$ make pre-commit
|
||||
````
|
||||
```
|
||||
|
||||
**Note: the FLASK_APP env var should not need to be set, as it's currently controlled
|
||||
via `.flaskenv`, however if needed, it should be set to `superset.app:create_app()`**
|
||||
@@ -663,8 +663,8 @@ tox -e pylint
|
||||
|
||||
In terms of best practices please advoid blanket disablement of Pylint messages globally (via `.pylintrc`) or top-level within the file header, albeit there being a few exceptions. Disablement should occur inline as it prevents masking issues and provides context as to why said message is disabled.
|
||||
|
||||
Additionally the Python code is auto-formatted using [Black](https://github.com/python/black) which
|
||||
is configured as a pre-commit hook. There are also numerous [editor integrations](https://black.readthedocs.io/en/stable/editor_integration.html)
|
||||
Additionally, the Python code is auto-formatted using [Black](https://github.com/python/black) which
|
||||
is configured as a pre-commit hook. There are also numerous [editor integrations](https://black.readthedocs.io/en/stable/integrations/editors.html)
|
||||
|
||||
### TypeScript
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
######################################################################
|
||||
# PY stage that simply does a pip install on our requirements
|
||||
######################################################################
|
||||
ARG PY_VER=3.8.12
|
||||
ARG PY_VER=3.8.13
|
||||
FROM python:${PY_VER} AS superset-py
|
||||
|
||||
RUN mkdir /app \
|
||||
@@ -71,7 +71,7 @@ RUN cd /app/superset-frontend \
|
||||
######################################################################
|
||||
# Final lean image...
|
||||
######################################################################
|
||||
ARG PY_VER=3.8.12
|
||||
ARG PY_VER=3.8.13
|
||||
FROM python:${PY_VER} AS lean
|
||||
|
||||
ENV LANG=C.UTF-8 \
|
||||
|
||||
@@ -25,7 +25,7 @@ under the License.
|
||||
[](https://badge.fury.io/py/apache-superset)
|
||||
[](https://codecov.io/github/apache/superset)
|
||||
[](https://pypi.python.org/pypi/apache-superset)
|
||||
[](https://join.slack.com/t/apache-superset/shared_invite/zt-uxbh5g36-AISUtHbzOXcu0BIj7kgUaw)
|
||||
[](https://join.slack.com/t/apache-superset/shared_invite/zt-16jvzmoi8-sI7jKWp~xc2zYRe~NqiY9Q)
|
||||
[](https://superset.apache.org)
|
||||
|
||||
<img
|
||||
@@ -47,7 +47,7 @@ A modern, enterprise-ready business intelligence web application.
|
||||
|
||||
## Why Superset?
|
||||
|
||||
Superset is a modern data exploration and data visualization platform. Superset can replace or augment proprietary business intelligence tools for many teams.
|
||||
Superset is a modern data exploration and data visualization platform. Superset can replace or augment proprietary business intelligence tools for many teams. Superset integrates well with a variety of data sources.
|
||||
|
||||
Superset provides:
|
||||
|
||||
@@ -129,7 +129,7 @@ Want to add support for your datastore or data engine? Read more [here](https://
|
||||
## Get Involved
|
||||
|
||||
- Ask and answer questions on [StackOverflow](https://stackoverflow.com/questions/tagged/apache-superset) using the **apache-superset** tag
|
||||
- [Join our community's Slack](https://join.slack.com/t/apache-superset/shared_invite/zt-uxbh5g36-AISUtHbzOXcu0BIj7kgUaw)
|
||||
- [Join our community's Slack](https://join.slack.com/t/apache-superset/shared_invite/zt-16jvzmoi8-sI7jKWp~xc2zYRe~NqiY9Q)
|
||||
and please read our [Slack Community Guidelines](https://github.com/apache/superset/blob/master/CODE_OF_CONDUCT.md#slack-community-guidelines)
|
||||
- [Join our dev@superset.apache.org Mailing list](https://lists.apache.org/list.html?dev@superset.apache.org)
|
||||
|
||||
@@ -144,7 +144,7 @@ how to set up a development environment.
|
||||
|
||||
- Getting Started with Superset
|
||||
- [Superset in 2 Minutes using Docker Compose](https://superset.apache.org/docs/installation/installing-superset-using-docker-compose#installing-superset-locally-using-docker-compose)
|
||||
- [Installing Database Drivers](https://superset.apache.org/docs/databases/dockeradddrivers)
|
||||
- [Installing Database Drivers](https://superset.apache.org/docs/databases/docker-add-drivers/)
|
||||
- [Building New Database Connectors](https://preset.io/blog/building-database-connector/)
|
||||
- [Create Your First Dashboard](https://superset.apache.org/docs/creating-charts-dashboards/first-dashboard)
|
||||
- [Comprehensive Tutorial for Contributing Code to Apache Superset
|
||||
|
||||
@@ -34,7 +34,7 @@ RUN apt-get install -y build-essential libssl-dev \
|
||||
|
||||
# Install nodejs for custom build
|
||||
# https://nodejs.org/en/download/package-manager/
|
||||
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - \
|
||||
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
RUN mkdir -p /home/superset
|
||||
|
||||
@@ -34,7 +34,7 @@ RUN apt-get install -y build-essential libssl-dev \
|
||||
|
||||
# Install nodejs for custom build
|
||||
# https://nodejs.org/en/download/package-manager/
|
||||
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - \
|
||||
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
RUN mkdir -p /home/superset
|
||||
|
||||
@@ -30,6 +30,7 @@ partaking in the process should join the channel.
|
||||
|
||||
## Release notes for recent releases
|
||||
|
||||
- [1.5](release-notes-1-5/README.md)
|
||||
- [1.4](release-notes-1-4/README.md)
|
||||
- [1.3](release-notes-1-3/README.md)
|
||||
- [1.2](release-notes-1-2/README.md)
|
||||
@@ -287,6 +288,8 @@ cd ~/src/superset/
|
||||
git branch
|
||||
# Create the release tag
|
||||
git tag -f ${SUPERSET_VERSION}
|
||||
# push the tag to the remote
|
||||
git push upstream ${SUPERSET_VERSION}
|
||||
```
|
||||
|
||||
### Update CHANGELOG and UPDATING on superset
|
||||
|
||||
@@ -167,7 +167,10 @@ class GitChangeLog:
|
||||
return f"### {self._version} ({self._logs[0].time})"
|
||||
|
||||
def _parse_change_log(
|
||||
self, changelog: Dict[str, str], pr_info: Dict[str, str], github_login: str,
|
||||
self,
|
||||
changelog: Dict[str, str],
|
||||
pr_info: Dict[str, str],
|
||||
github_login: str,
|
||||
) -> None:
|
||||
formatted_pr = (
|
||||
f"- [#{pr_info.get('id')}]"
|
||||
@@ -355,7 +358,8 @@ def compare(base_parameters: BaseParameters) -> None:
|
||||
|
||||
@cli.command("changelog")
|
||||
@click.option(
|
||||
"--csv", help="The csv filename to export the changelog to",
|
||||
"--csv",
|
||||
help="The csv filename to export the changelog to",
|
||||
)
|
||||
@click.option(
|
||||
"--access_token",
|
||||
@@ -381,12 +385,12 @@ def change_log(
|
||||
with open(csv, "w") as csv_file:
|
||||
log_items = list(logs)
|
||||
field_names = log_items[0].keys()
|
||||
writer = lib_csv.DictWriter( # type: ignore
|
||||
writer = lib_csv.DictWriter(
|
||||
csv_file,
|
||||
delimiter=",",
|
||||
quotechar='"',
|
||||
quoting=lib_csv.QUOTE_ALL,
|
||||
fieldnames=field_names, # type: ignore
|
||||
fieldnames=field_names,
|
||||
)
|
||||
writer.writeheader()
|
||||
for log in logs:
|
||||
|
||||
142
RELEASING/release-notes-1-5/README.md
Normal file
142
RELEASING/release-notes-1-5/README.md
Normal file
@@ -0,0 +1,142 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# Release Notes for Superset 1.5
|
||||
|
||||
Superset 1.5 focuses on polishing the dashboard native filters experience, while
|
||||
improving performance and stability. Superset 1.5 is likely the last minor release of
|
||||
version 1 of Superset, and will be succeeded by Superset 2.0. The 1.5 branch
|
||||
introduces the notion of a Long Term Support (LTS) version of Superset, and will
|
||||
receive security and other critical fixes even after Superset 2.x is released.
|
||||
Therefore, users will have the choice of staying on the 1.5 branch or upgrading to 2.x
|
||||
when available.
|
||||
|
||||
- [**User Experience**](#user-facing-features)
|
||||
- [**Feature flags**](#feature-flags)
|
||||
- [**Database Experience**](#database-experience)
|
||||
- [**Breaking Changes and Full Changelog**](#breaking-changes-and-full-changelog)
|
||||
|
||||
## User Facing Features
|
||||
|
||||
- Complex dashboards with lots of native filters and charts will render considerably
|
||||
faster. See the videos that shows the rendering time of a complex dashboard go from
|
||||
11 to 3 seconds: [#19064](https://github.com/apache/superset/pull/19064). In
|
||||
addition, applying filters and switching tabs is also much smoother.
|
||||
- The Native Filter Bar has been redesigned, along with moving the "Apply" and
|
||||
"Clear all" buttons to the bottom:
|
||||
|
||||

|
||||
|
||||
- Native filters can now be made dependent on multiple filters. This makes it possible
|
||||
to restrict the available values in a filter based on the selection of other filters.
|
||||
|
||||

|
||||
|
||||
- In addition to being able to write Custom SQL for adhoc metrics and filters, the
|
||||
column control now also features a Custom SQL tab. This makes it possible to write
|
||||
custom expressions directly in charts without adding them to the dataset as saved
|
||||
expressions.
|
||||
|
||||

|
||||
|
||||
- A new `SupersetMetastoreCache` has been added which makes it possible to cache data
|
||||
in the Superset Metastore without the need for running a dedicated cache like Redis
|
||||
or Memcached. The new cache will be used by default for required caches, but can also
|
||||
be used for caching chart or other data. See the
|
||||
[documentation](https://superset.apache.org/docs/installation/cache#caching) for
|
||||
details on using the new cache.
|
||||
- Previously it was possible for Dashboards with lots of filters to cause an error.
|
||||
A similar issue existed on Explore. Now Superset stores Dashboard and Explore state
|
||||
in the cache (as opposed to the URL), eliminating the infamous
|
||||
[Long URL Problem](https://github.com/apache/superset/issues/17086).
|
||||
- Previously permanent links to Dashboard and Explore pages were in fact shortened URLS
|
||||
that relied on state being stored in the URL (see Long URL Problem above). In
|
||||
addition, the links used numerical ids and didn't check user permissions making it
|
||||
easy to iterate through links that were stored in the metastore. Now permanent links
|
||||
state is stored as JSON objects in the metastore, making it possible to store
|
||||
arbitrarily large Dashboard and Explore state in permalinks. In addition, the ids
|
||||
are encoded using [`hashids`](https://hashids.org/) and check permissions, making
|
||||
permalink state more secure.
|
||||
|
||||

|
||||
|
||||
## Feature flags
|
||||
|
||||
- A new feature flag `GENERIC_CHART_AXES` has been added that makes it possible to
|
||||
use a non-temporal x-axis on the ECharts Timeseries chart
|
||||
([#17917](https://github.com/apache/superset/pull/17917)). When enabled, a new
|
||||
control "X Axis" is added to the control panel of ECharts line, area, bar, step and
|
||||
scatter charts, which makes it possible to use categorical or numerical x-axes on
|
||||
those charts.
|
||||
|
||||

|
||||
|
||||
## Database Experience
|
||||
|
||||
- DuckDB: Add support for database:
|
||||
[#19317](https://github.com/apache/superset/pull/19317)
|
||||
|
||||
- Kusto: Add support for Azure Data Explorer (Kusto):
|
||||
[#17898](https://github.com/apache/superset/pull/17898)
|
||||
|
||||
- Trino: Add server cert support and new auth methods:
|
||||
[#17593](https://github.com/apache/superset/pull/17593) and
|
||||
[#16346](https://github.com/apache/superset/pull/16346)
|
||||
|
||||
- Microsoft SQL Server (MSSQL): support using CTEs in virtual tables:
|
||||
[#18567](https://github.com/apache/superset/pull/18567)
|
||||
|
||||
- Teradata and MSSQL: add support for TOP limit syntax:
|
||||
[#18746](https://github.com/apache/superset/pull/18746) and
|
||||
[#18240](https://github.com/apache/superset/pull/18240)
|
||||
|
||||
- Apache Drill: User impersonation using `drill+sadrill`:
|
||||
[#19252](https://github.com/apache/superset/pull/19252)
|
||||
|
||||
## Developer Experience
|
||||
|
||||
- `superset-ui` has now been integrated into the Superset codebase as per
|
||||
[SIP-58](https://github.com/apache/superset/issues/13013) dubbed "Monorepo". This
|
||||
makes development of plugins that ship with Superset considerably simpler. In
|
||||
addition, it makes it possible to align `superset-ui` releases with official Superset
|
||||
releases.
|
||||
|
||||
## Breaking Changes and Full Changelog
|
||||
|
||||
**Breaking Changes**
|
||||
|
||||
- Bump `mysqlclient` from v1 to v2:
|
||||
[#17556](https://github.com/apache/superset/pull/17556)
|
||||
- Single and double quotes will no longer be removed from filter values:
|
||||
[#17881](https://github.com/apache/superset/pull/17881)
|
||||
- Previously `QUERY_COST_FORMATTERS_BY_ENGINE`, `SQL_VALIDATORS_BY_ENGINE` and
|
||||
`SCHEDULED_QUERIES` were expected to be defined in the feature flag dictionary in
|
||||
the `config.py` file. These should now be defined as a top-level config, with the
|
||||
feature flag dictionary being reserved for boolean only values:
|
||||
[#15254](https://github.com/apache/superset/pull/15254)
|
||||
- All Superset CLI commands (init, load_examples and etc) require setting the
|
||||
`FLASK_APP` environment variable (which is set by default when `.flaskenv` is loaded):
|
||||
[#17539](https://github.com/apache/superset/pull/17539)
|
||||
|
||||
**Changelog**
|
||||
|
||||
To see the complete changelog in this release, head to
|
||||
[CHANGELOG.MD](https://github.com/apache/superset/blob/1.5/CHANGELOG.md).
|
||||
As mentioned earlier, this release has a MASSIVE amount of bug fixes. The full
|
||||
changelog lists all of them!
|
||||
BIN
RELEASING/release-notes-1-5/media/adhoc_columns.png
Normal file
BIN
RELEASING/release-notes-1-5/media/adhoc_columns.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 202 KiB |
BIN
RELEASING/release-notes-1-5/media/categorical_line.png
Normal file
BIN
RELEASING/release-notes-1-5/media/categorical_line.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 301 KiB |
BIN
RELEASING/release-notes-1-5/media/dependent_filters.png
Normal file
BIN
RELEASING/release-notes-1-5/media/dependent_filters.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 428 KiB |
BIN
RELEASING/release-notes-1-5/media/filter_bar.png
Normal file
BIN
RELEASING/release-notes-1-5/media/filter_bar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 359 KiB |
BIN
RELEASING/release-notes-1-5/media/permalink.png
Normal file
BIN
RELEASING/release-notes-1-5/media/permalink.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 184 KiB |
@@ -106,7 +106,12 @@ def inter_send_email(
|
||||
|
||||
class BaseParameters(object):
|
||||
def __init__(
|
||||
self, email: str, username: str, password: str, version: str, version_rc: str,
|
||||
self,
|
||||
email: str,
|
||||
username: str,
|
||||
password: str,
|
||||
version: str,
|
||||
version_rc: str,
|
||||
) -> None:
|
||||
self.email = email
|
||||
self.username = username
|
||||
|
||||
35
UPDATING.md
35
UPDATING.md
@@ -22,14 +22,21 @@ under the License.
|
||||
This file documents any backwards-incompatible changes in Superset and
|
||||
assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
## 1.5.2
|
||||
|
||||
### Other
|
||||
|
||||
- [19570](https://github.com/apache/superset/pull/19570): makes [sqloxide](https://pypi.org/project/sqloxide/) optional so the SIP-68 migration can be run on aarch64. If the migration is taking too long installing sqloxide manually should improve the performance.
|
||||
|
||||
## 1.5.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- [18976](https://github.com/apache/superset/pull/18976): When running the app in debug mode, the app will default to use `SimpleCache` for `FILTER_STATE_CACHE_CONFIG` and `EXPLORE_FORM_DATA_CACHE_CONFIG`. When running in non-debug mode, a cache backend will need to be defined, otherwise the application will fail to start. For installations using Redis or other caching backends, it is recommended to use the same backend for both cache configs.
|
||||
- [17881](https://github.com/apache/superset/pull/17881): Previously simple adhoc filter values on string columns were stripped of enclosing single and double quotes. To fully support literal quotes in filters, both single and double quotes will no longer be removed from filter values.
|
||||
- [17984](https://github.com/apache/superset/pull/17984): Default Flask SECRET_KEY has changed for security reasons. You should always override with your own secret. Set `PREVIOUS_SECRET_KEY` (ex: PREVIOUS_SECRET_KEY = "\2\1thisismyscretkey\1\2\\e\\y\\y\\h") with your previous key and use `superset re-encrypt-secrets` to rotate you current secrets
|
||||
- [15254](https://github.com/apache/superset/pull/15254): Previously `QUERY_COST_FORMATTERS_BY_ENGINE`, `SQL_VALIDATORS_BY_ENGINE` and `SCHEDULED_QUERIES` were expected to be defined in the feature flag dictionary in the `config.py` file. These should now be defined as a top-level config, with the feature flag dictionary being reserved for boolean only values.
|
||||
- [17539](https://github.com/apache/superset/pull/17539): all Superset CLI commands (init, load_examples and etc) require setting the FLASK_APP environment variable (which is set by default when `.flaskenv` is loaded)
|
||||
- [17556](https://github.com/apache/superset/pull/17556): Bumps `mysqlclient` from v1 to v2.
|
||||
- [17539](https://github.com/apache/superset/pull/17539): All Superset CLI commands, e.g. `init`, `load_examples`, etc. require setting the `FLASK_APP` environment variable (which is set by default when `.flaskenv` is loaded).
|
||||
- [15254](https://github.com/apache/superset/pull/15254): The `QUERY_COST_FORMATTERS_BY_ENGINE`, `SQL_VALIDATORS_BY_ENGINE` and `SCHEDULED_QUERIES` feature flags are now defined as config keys given that feature flags are reserved for boolean only values.
|
||||
|
||||
### Potential Downtime
|
||||
|
||||
@@ -42,11 +49,25 @@ assists people when migrating to a new version.
|
||||
|
||||
### Deprecations
|
||||
|
||||
- [18960](https://github.com/apache/superset/pull/18960): Persisting URL params in chart metadata is no longer supported. To set a default value for URL params in Jinja code, use the optional second argument: `url_param("my-param", "my-default-value")`.
|
||||
|
||||
### Other
|
||||
|
||||
- [17589](https://github.com/apache/incubator-superset/pull/17589): It is now possible to limit access to users' recent activity data by setting the `ENABLE_BROAD_ACTIVITY_ACCESS` config flag to false, or customizing the `raise_for_user_activity_access` method in the security manager.
|
||||
- [17536](https://github.com/apache/superset/pull/17536): introduced a key-value endpoint to store dashboard filter state. This endpoint is backed by Flask-Caching and the default configuration assumes that the values will be stored in the file system. If you are already using another cache backend like Redis or Memchached, you'll probably want to change this setting in `superset_config.py`. The key is `FILTER_STATE_CACHE_CONFIG` and the available settings can be found in Flask-Caching [docs](https://flask-caching.readthedocs.io/en/latest/).
|
||||
- [17882](https://github.com/apache/superset/pull/17882): introduced a key-value endpoint to store Explore form data. This endpoint is backed by Flask-Caching and the default configuration assumes that the values will be stored in the file system. If you are already using another cache backend like Redis or Memchached, you'll probably want to change this setting in `superset_config.py`. The key is `EXPLORE_FORM_DATA_CACHE_CONFIG` and the available settings can be found in Flask-Caching [docs](https://flask-caching.readthedocs.io/en/latest/).
|
||||
- [17589](https://github.com/apache/superset/pull/17589): It is now possible to limit access to users' recent activity data by setting the `ENABLE_BROAD_ACTIVITY_ACCESS` config flag to false, or customizing the `raise_for_user_activity_access` method in the security manager.
|
||||
- [17536](https://github.com/apache/superset/pull/17536): introduced a key-value endpoint to store dashboard filter state. This endpoint is backed by Flask-Caching and the default configuration assumes that the values will be stored in the file system. If you are already using another cache backend like Redis or Memcached, you'll probably want to change this setting in `superset_config.py`. The key is `FILTER_STATE_CACHE_CONFIG` and the available settings can be found in Flask-Caching [docs](https://flask-caching.readthedocs.io/en/latest/).
|
||||
- [17882](https://github.com/apache/superset/pull/17882): introduced a key-value endpoint to store Explore form data. This endpoint is backed by Flask-Caching and the default configuration assumes that the values will be stored in the file system. If you are already using another cache backend like Redis or Memcached, you'll probably want to change this setting in `superset_config.py`. The key is `EXPLORE_FORM_DATA_CACHE_CONFIG` and the available settings can be found in Flask-Caching [docs](https://flask-caching.readthedocs.io/en/latest/).
|
||||
|
||||
## 1.4.1
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- [17984](https://github.com/apache/superset/pull/17984): Default Flask SECRET_KEY has changed for security reasons. You should always override with your own secret. Set `PREVIOUS_SECRET_KEY` (ex: PREVIOUS_SECRET_KEY = "\2\1thisismyscretkey\1\2\\e\\y\\y\\h") with your previous key and use `superset re-encrypt-secrets` to rotate you current secrets
|
||||
|
||||
### Potential Downtime
|
||||
|
||||
### Deprecations
|
||||
|
||||
### Other
|
||||
|
||||
## 1.4.0
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ ADMIN_PASSWORD="admin"
|
||||
# If Cypress run – overwrite the password for admin and export env variables
|
||||
if [ "$CYPRESS_CONFIG" == "true" ]; then
|
||||
ADMIN_PASSWORD="general"
|
||||
export SUPERSET_CONFIG=tests.superset_test_config
|
||||
export SUPERSET_CONFIG=tests.integration_tests.superset_test_config
|
||||
export SUPERSET_TESTENV=true
|
||||
export ENABLE_REACT_CRUD_VIEWS=true
|
||||
export SUPERSET__SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset
|
||||
|
||||
@@ -69,6 +69,16 @@ REDIS_RESULTS_DB = get_env_variable("REDIS_RESULTS_DB", "1")
|
||||
|
||||
RESULTS_BACKEND = FileSystemCache("/app/superset_home/sqllab")
|
||||
|
||||
CACHE_CONFIG = {
|
||||
"CACHE_TYPE": "redis",
|
||||
"CACHE_DEFAULT_TIMEOUT": 300,
|
||||
"CACHE_KEY_PREFIX": "superset_",
|
||||
"CACHE_REDIS_HOST": REDIS_HOST,
|
||||
"CACHE_REDIS_PORT": REDIS_PORT,
|
||||
"CACHE_REDIS_DB": REDIS_RESULTS_DB,
|
||||
}
|
||||
DATA_CACHE_CONFIG = CACHE_CONFIG
|
||||
|
||||
|
||||
class CeleryConfig(object):
|
||||
BROKER_URL = f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_CELERY_DB}"
|
||||
|
||||
@@ -27,6 +27,7 @@ gunicorn \
|
||||
--worker-class ${SERVER_WORKER_CLASS:-gthread} \
|
||||
--threads ${SERVER_THREADS_AMOUNT:-20} \
|
||||
--timeout ${GUNICORN_TIMEOUT:-60} \
|
||||
--keep-alive ${GUNICORN_KEEPALIVE:-2} \
|
||||
--limit-request-line ${SERVER_LIMIT_REQUEST_LINE:-0} \
|
||||
--limit-request-field_size ${SERVER_LIMIT_REQUEST_FIELD_SIZE:-0} \
|
||||
"${FLASK_APP}"
|
||||
|
||||
@@ -24,7 +24,7 @@ This website is built using [Docusaurus 2](https://docusaurus.io/), a modern sta
|
||||
### Installation
|
||||
|
||||
```
|
||||
$ yarn
|
||||
$ yarn install
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
@@ -4,12 +4,12 @@ hide_title: true
|
||||
sidebar_position: 9
|
||||
---
|
||||
|
||||
import { Buffer } from "buffer";
|
||||
import { Buffer } from 'buffer';
|
||||
global.Buffer = Buffer;
|
||||
import SwaggerUI from "swagger-ui-react";
|
||||
import openapi from "/resources/openapi.json";
|
||||
import "swagger-ui-react/swagger-ui.css";
|
||||
// import { Alert } from "antd";
|
||||
import SwaggerUI from 'swagger-ui-react';
|
||||
import openapi from '/resources/openapi.json';
|
||||
import 'swagger-ui-react/swagger-ui.css';
|
||||
import { Alert } from 'antd';
|
||||
|
||||
## API
|
||||
|
||||
@@ -18,28 +18,16 @@ Superset's public **REST API** follows the
|
||||
documented here. The docs bellow are generated using
|
||||
[Swagger React UI](https://www.npmjs.com/package/swagger-ui-react).
|
||||
|
||||
<!--
|
||||
TODO: (corbinrobb) Uncomment Alert if/when antd gets added and remove Infima alert. Fix SwaggerUI readability in dark mode.
|
||||
-->
|
||||
|
||||
<!-- <Alert
|
||||
<Alert
|
||||
type="info"
|
||||
message={
|
||||
|
||||
<div>
|
||||
<strong>NOTE! </strong>
|
||||
You can find an interactive version of this documentation on your local Superset
|
||||
instance at <strong>/swagger/v1</strong> (if enabled)
|
||||
instance at <strong>/swagger/v1</strong> (unless disabled)
|
||||
</div>
|
||||
|
||||
}
|
||||
/> -->
|
||||
|
||||
<div class="alert alert--info" role="alert">
|
||||
<strong>NOTE! </strong>
|
||||
You can find an interactive version of this documentation on your local Superset
|
||||
instance at <strong>/swagger/v1</strong> (if enabled)
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
@@ -8,11 +8,11 @@ version: 1
|
||||
## Contributing to Superset
|
||||
|
||||
Superset is an [Apache Software foundation](https://www.apache.org/theapacheway/index.html) project.
|
||||
The core contributors (or committers) to Superset communicate primarily in the following channels (all of
|
||||
which you can join):
|
||||
The core contributors (or committers) to Superset communicate primarily in the following channels (
|
||||
which can be joined by anyone):
|
||||
|
||||
- [Mailing list](https://lists.apache.org/list.html?dev@superset.apache.org)
|
||||
- [Apache Superset Slack community](https://join.slack.com/t/apache-superset/shared_invite/zt-uxbh5g36-AISUtHbzOXcu0BIj7kgUaw)
|
||||
- [Apache Superset Slack community](https://join.slack.com/t/apache-superset/shared_invite/zt-16jvzmoi8-sI7jKWp~xc2zYRe~NqiY9Q)
|
||||
- [Github issues and PR's](https://github.com/apache/superset/issues)
|
||||
|
||||
More references:
|
||||
|
||||
@@ -39,10 +39,10 @@ We use [Pylint](https://pylint.org/) for linting which can be invoked via:
|
||||
tox -e pylint
|
||||
```
|
||||
|
||||
In terms of best practices please advoid blanket disablement of Pylint messages globally (via `.pylintrc`) or top-level within the file header, albeit there being a few exceptions. Disablement should occur inline as it prevents masking issues and provides context as to why said message is disabled.
|
||||
In terms of best practices please avoid blanket disablement of Pylint messages globally (via `.pylintrc`) or top-level within the file header, albeit there being a few exceptions. Disablement should occur inline as it prevents masking issues and provides context as to why said message is disabled.
|
||||
|
||||
Additionally the Python code is auto-formatted using [Black](https://github.com/python/black) which
|
||||
is configured as a pre-commit hook. There are also numerous [editor integrations](https://black.readthedocs.io/en/stable/editor_integration.html)
|
||||
Additionally, the Python code is auto-formatted using [Black](https://github.com/python/black) which
|
||||
is configured as a pre-commit hook. There are also numerous [editor integrations](https://black.readthedocs.io/en/stable/integrations/editors.html)
|
||||
|
||||
### TypeScript
|
||||
|
||||
|
||||
@@ -103,4 +103,4 @@ app.logger.info(form_data)
|
||||
```
|
||||
|
||||
### Frontend Assets
|
||||
See [Running Frontend Assets Locally](https://superset.apache.org/docs/installation/installing-superset-from-scratch#os-dependencies)
|
||||
See [Building Frontend Assets Locally](https://github.com/apache/superset/blob/master/CONTRIBUTING.md#frontend)
|
||||
|
||||
@@ -189,3 +189,23 @@ all charts will load their data even if feature flag is turned on and no roles a
|
||||
to roles the access will fallback to **Dataset permissions**
|
||||
|
||||
<img src={useBaseUrl("/img/tutorial/tutorial_dashboard_access.png" )} />
|
||||
|
||||
### Customizing dashboard
|
||||
|
||||
The following URL parameters can be used to modify how the dashboard is rendered:
|
||||
- `standalone`:
|
||||
- `0` (default): dashboard is displayed normally
|
||||
- `1`: Top Navigation is hidden
|
||||
- `2`: Top Navigation + title is hidden
|
||||
- `3`: Top Navigation + title + top level tabs are hidden
|
||||
- `show_filters`:
|
||||
- `0`: render dashboard without Filter Bar
|
||||
- `1` (default): render dashboard with Filter Bar if native filters are enabled
|
||||
- `expand_filters`:
|
||||
- (default): render dashboard with Filter Bar expanded if there are native filters
|
||||
- `0`: render dashboard with Filter Bar collapsed
|
||||
- `1`: render dashboard with Filter Bar expanded
|
||||
|
||||
For example, when running the local development build, the following will disable the
|
||||
Top Nav and remove the Filter Bar:
|
||||
`http://localhost:8088/superset/dashboard/my-dashboard/?standalone=1&show_filters=0`
|
||||
|
||||
@@ -7,7 +7,7 @@ version: 1
|
||||
|
||||
## Apache Impala
|
||||
|
||||
The recommended connector library to Apache Hive is [impyla](https://github.com/cloudera/impyla).
|
||||
The recommended connector library to Apache Impala is [impyla](https://github.com/cloudera/impyla).
|
||||
|
||||
The expected connection string is formatted as follows:
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ A list of some of the recommended packages.
|
||||
| [SQL Server](/docs/databases/sql-server) | `pip install pymssql` | `mssql://` |
|
||||
| [Teradata](/docs/databases/teradata) | `pip install teradatasqlalchemy ` | `teradata://{user}:{password}@{host}` |
|
||||
| [Vertica](/docs/databases/vertica) | `pip install sqlalchemy-vertica-python` | `vertica+vertica_python://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
|
||||
| [Yugabyte](/docs/databases/yugabyte) | `pip install psycopg2` | `postgresql://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
|
||||
| [YugabyteDB](/docs/databases/yugabytedb) | `pip install psycopg2` | `postgresql://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ version: 1
|
||||
|
||||
## MySQL
|
||||
|
||||
The recommended connector library for MySQL is `[mysqlclient](https://pypi.org/project/mysqlclient/)`.
|
||||
The recommended connector library for MySQL is [mysqlclient](https://pypi.org/project/mysqlclient/).
|
||||
|
||||
Here's the connection string:
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Postgres
|
||||
title: YugabyteDB
|
||||
hide_title: true
|
||||
sidebar_position: 38
|
||||
version: 1
|
||||
@@ -33,6 +33,9 @@ Alerts and reports are disabled by default. To turn them on, you need to do some
|
||||
- You must install a headless browser, for taking screenshots of the charts and dashboards. Only Firefox and Chrome are currently supported.
|
||||
> If you choose Chrome, you must also change the value of `WEBDRIVER_TYPE` to `"chrome"` in your `superset_config.py`.
|
||||
|
||||
Note : All the components required (headless browser, redis, postgres db, celery worker and celery beat) are present in the docker image if you are following [Installing Superset Locally](https://superset.apache.org/docs/installation/installing-superset-using-docker-compose/).
|
||||
All you need to do is add the required config (See `Detailed Config`). Set `ALERT_REPORTS_NOTIFICATION_DRY_RUN` to `False` in [superset config](https://github.com/apache/superset/blob/master/docker/pythonpath_dev/superset_config.py) to disable dry-run mode and start receiving email/slack notifications.
|
||||
|
||||
#### Slack integration
|
||||
|
||||
To send alerts and reports to Slack channels, you need to create a new Slack Application on your workspace.
|
||||
@@ -123,6 +126,7 @@ SLACK_API_TOKEN = "xoxb-"
|
||||
# Email configuration
|
||||
SMTP_HOST = "smtp.sendgrid.net" #change to your host
|
||||
SMTP_STARTTLS = True
|
||||
SMTP_SSL_SERVER_AUTH = True # If your using an SMTP server with a valid certificate
|
||||
SMTP_SSL = False
|
||||
SMTP_USER = "your_user"
|
||||
SMTP_PORT = 2525 # your port eg. 587
|
||||
|
||||
@@ -7,20 +7,32 @@ version: 1
|
||||
|
||||
## Caching
|
||||
|
||||
Superset uses [Flask-Caching](https://flask-caching.readthedocs.io/) for caching purpose. For security reasons,
|
||||
there are two separate cache configs for Superset's own metadata (`CACHE_CONFIG`) and charting data queried from
|
||||
connected datasources (`DATA_CACHE_CONFIG`). However, Query results from SQL Lab are stored in another backend
|
||||
called `RESULTS_BACKEND`, See [Async Queries via Celery](/docs/installation/async-queries-celery) for details.
|
||||
|
||||
Configuring caching is as easy as providing `CACHE_CONFIG` and `DATA_CACHE_CONFIG` in your
|
||||
Superset uses [Flask-Caching](https://flask-caching.readthedocs.io/) for caching purposes. Configuring caching is as easy as providing a custom cache config in your
|
||||
`superset_config.py` that complies with [the Flask-Caching specifications](https://flask-caching.readthedocs.io/en/latest/#configuring-flask-caching).
|
||||
|
||||
Flask-Caching supports various caching backends, including Redis, Memcached, SimpleCache (in-memory), or the
|
||||
local filesystem.
|
||||
local filesystem. Custom cache backends are also supported. See [here](https://flask-caching.readthedocs.io/en/latest/#custom-cache-backends) for specifics.
|
||||
The following cache configurations can be customized:
|
||||
- Metadata cache (optional): `CACHE_CONFIG`
|
||||
- Charting data queried from datasets (optional): `DATA_CACHE_CONFIG`
|
||||
- SQL Lab query results (optional): `RESULTS_BACKEND`. See [Async Queries via Celery](/docs/installation/async-queries-celery) for details
|
||||
- Dashboard filter state (required): `FILTER_STATE_CACHE_CONFIG`.
|
||||
- Explore chart form data (required): `EXPLORE_FORM_DATA_CACHE_CONFIG`
|
||||
|
||||
Please note, that Dashboard and Explore caching is required. If these caches are undefined, Superset falls back to using a built-in cache that stores data
|
||||
in the metadata database. While it is recommended to use a dedicated cache, the built-in cache can also be used to cache other data.
|
||||
For example, to use the built-in cache to store chart data, use the following config:
|
||||
|
||||
```python
|
||||
DATA_CACHE_CONFIG = {
|
||||
"CACHE_TYPE": "SupersetMetastoreCache",
|
||||
"CACHE_KEY_PREFIX": "superset_results", # make sure this string is unique to avoid collisions
|
||||
"CACHE_DEFAULT_TIMEOUT": 86400, # 60 seconds * 60 minutes * 24 hours
|
||||
}
|
||||
```
|
||||
|
||||
- Redis (recommended): we recommend the [redis](https://pypi.python.org/pypi/redis) Python package
|
||||
- Memcached: we recommend using [pylibmc](https://pypi.org/project/pylibmc/) client library as
|
||||
`python-memcached` does not handle storing binary data correctly.
|
||||
- Redis: we recommend the [redis](https://pypi.python.org/pypi/redis) Python package
|
||||
|
||||
Both of these libraries can be installed using pip.
|
||||
|
||||
@@ -28,16 +40,7 @@ For chart data, Superset goes up a “timeout search path”, from a slice's con
|
||||
to the datasource’s, the database’s, then ultimately falls back to the global default
|
||||
defined in `DATA_CACHE_CONFIG`.
|
||||
|
||||
```
|
||||
DATA_CACHE_CONFIG = {
|
||||
'CACHE_TYPE': 'redis',
|
||||
'CACHE_DEFAULT_TIMEOUT': 60 * 60 * 24, # 1 day default (in secs)
|
||||
'CACHE_KEY_PREFIX': 'superset_results',
|
||||
'CACHE_REDIS_URL': 'redis://localhost:6379/0',
|
||||
}
|
||||
```
|
||||
|
||||
Custom cache backends are also supported. See [here](https://flask-caching.readthedocs.io/en/latest/#custom-cache-backends) for specifics.
|
||||
## Celery beat
|
||||
|
||||
Superset has a Celery task that will periodically warm up the cache based on different strategies.
|
||||
To use it, add the following to the `CELERYBEAT_SCHEDULE` section in `config.py`:
|
||||
|
||||
@@ -10,7 +10,7 @@ version: 1
|
||||
### Configuration
|
||||
|
||||
To configure your application, you need to create a file `superset_config.py` and add it to your
|
||||
`PYTHONPATH`. If your applcation was installed using docker-compose an alternative configuration is required. See [https://github.com/apache/superset/tree/master/docker#readme](https://github.com/apache/superset/tree/master/docker#readme) for details.
|
||||
`PYTHONPATH`. If your application was installed using docker-compose an alternative configuration is required. See [https://github.com/apache/superset/tree/master/docker#readme](https://github.com/apache/superset/tree/master/docker#readme) for details.
|
||||
|
||||
Here are some of the parameters you can set in that file:
|
||||
```
|
||||
@@ -20,8 +20,12 @@ ROW_LIMIT = 5000
|
||||
SUPERSET_WEBSERVER_PORT = 8088
|
||||
|
||||
# Flask App Builder configuration
|
||||
# Your App secret key
|
||||
SECRET_KEY = '\2\1thisismyscretkey\1\2\e\y\y\h'
|
||||
# Your App secret key will be used for securely signing the session cookie
|
||||
# and encrypting sensitive information on the database
|
||||
# Make sure you are changing this key for your deployment with a strong key.
|
||||
# You can generate a strong key using `openssl rand -base64 42`
|
||||
|
||||
SECRET_KEY = 'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY'
|
||||
|
||||
# The SQLAlchemy connection string to your database backend
|
||||
# This connection defines the path to the database that stores your
|
||||
@@ -303,3 +307,15 @@ defaults on a per database level via the `extra` parameter.
|
||||
|
||||
Note in a future release the interim SIP-15 logic will be removed (including the
|
||||
`time_grain_endpoints` form-data field) via a code change and Alembic migration.
|
||||
|
||||
### SECRET_KEY Rotation
|
||||
|
||||
If you want to rotate the SECRET_KEY(change the existing secret key), follow the below steps.
|
||||
|
||||
Add the new SECRET_KEY and PREVIOUS_SECRET_KEY to `superset_config.py`:
|
||||
|
||||
```python
|
||||
PREVIOUS_SECRET_KEY = 'CURRENT_SECRET_KEY' # The default SECRET_KEY for deployment is '21thisismyscretkey12eyyh'
|
||||
SECRET_KEY = 'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY'
|
||||
```
|
||||
Then run `superset re-encrypt-secrets`
|
||||
|
||||
@@ -92,6 +92,35 @@ postgresql:
|
||||
postgresqlPassword: superset
|
||||
```
|
||||
|
||||
Make sure, you set a unique strong complex alphanumeric string for your SECRET_KEY and use a tool to help you generate
|
||||
a sufficiently random sequence.
|
||||
|
||||
- To generate a good key you can run, `openssl rand -base64 42`
|
||||
|
||||
```yaml
|
||||
configOverrides:
|
||||
secret: |
|
||||
SECRET_KEY = 'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY'
|
||||
```
|
||||
|
||||
If you want to change the previous secret key then you should rotate the keys.
|
||||
Default secret key for kubernetes deployment is `thisISaSECRET_1234`
|
||||
|
||||
```yaml
|
||||
configOverrides:
|
||||
my_override: |
|
||||
PREVIOUS_SECRET_KEY = 'YOUR_PREVIOUS_SECRET_KEY'
|
||||
SECRET_KEY = 'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY'
|
||||
init:
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
. {{ .Values.configMountPath }}/superset_bootstrap.sh
|
||||
superset re-encrypt-secrets
|
||||
. {{ .Values.configMountPath }}/superset_init.sh
|
||||
```
|
||||
|
||||
#### Dependencies
|
||||
|
||||
Install additional packages and do any other bootstrap configuration in this script. For production clusters it's
|
||||
@@ -101,7 +130,7 @@ database drivers so that you can connect to those datasources in your Superset i
|
||||
```yaml
|
||||
bootstrapScript: |
|
||||
#!/bin/bash
|
||||
pip install psycopg2==2.8.5 \
|
||||
pip install psycopg2==2.9.1 \
|
||||
redis==3.2.1 \
|
||||
pybigquery==2.26.0 \
|
||||
elasticsearch-dbapi==0.2.5 &&\
|
||||
|
||||
@@ -119,8 +119,8 @@ In this section, we'll walkthrough the pre-defined Jinja macros in Superset.
|
||||
|
||||
The `{{ current_username() }}` macro returns the username of the currently logged in user.
|
||||
|
||||
If you have caching enabled in your Superset configuration, then by defaul the the `username` value will be used
|
||||
by Superset when calculating the cache key. A cache key is a unique identifer that determines if there's a
|
||||
If you have caching enabled in your Superset configuration, then by default the the `username` value will be used
|
||||
by Superset when calculating the cache key. A cache key is a unique identifier that determines if there's a
|
||||
cache hit in the future and Superset can retrieve cached data.
|
||||
|
||||
You can disable the inclusion of the `username` value in the calculation of the
|
||||
@@ -132,10 +132,10 @@ cache key by adding the following parameter to your Jinja code:
|
||||
|
||||
**Current User ID**
|
||||
|
||||
The `{{ current_user_id()}}` macro returns the user_id of the currently logged in user.
|
||||
The `{{ current_user_id() }}` macro returns the user_id of the currently logged in user.
|
||||
|
||||
If you have caching enabled in your Superset configuration, then by defaul the the `user_id` value will be used
|
||||
by Superset when calculating the cache key. A cache key is a unique identifer that determines if there's a
|
||||
If you have caching enabled in your Superset configuration, then by default the the `user_id` value will be used
|
||||
by Superset when calculating the cache key. A cache key is a unique identifier that determines if there's a
|
||||
cache hit in the future and Superset can retrieve cached data.
|
||||
|
||||
You can disable the inclusion of the `user_id` value in the calculation of the
|
||||
@@ -197,8 +197,8 @@ You can retrieve the value for a specific filter as a list using `{{ filter_valu
|
||||
|
||||
This is useful if:
|
||||
|
||||
- you want to use a filter component to filter a query where the name of filter component column doesn't match the one in the select statement
|
||||
- you want to have the ability for filter inside the main query for performance purposes
|
||||
- You want to use a filter component to filter a query where the name of filter component column doesn't match the one in the select statement
|
||||
- You want to have the ability for filter inside the main query for performance purposes
|
||||
|
||||
Here's a concrete example:
|
||||
|
||||
@@ -218,9 +218,9 @@ returns the operator specified in the Explore UI.
|
||||
|
||||
This is useful if:
|
||||
|
||||
- you want to handle more than the IN operator in your SQL clause
|
||||
- you want to handle generating custom SQL conditions for a filter
|
||||
- you want to have the ability to filter inside the main query for speed purposes
|
||||
- You want to handle more than the IN operator in your SQL clause
|
||||
- You want to handle generating custom SQL conditions for a filter
|
||||
- You want to have the ability to filter inside the main query for speed purposes
|
||||
|
||||
Here's a concrete example:
|
||||
|
||||
|
||||
@@ -35,10 +35,8 @@ const config = {
|
||||
favicon: 'img/favicon.ico',
|
||||
organizationName: 'apache', // Usually your GitHub org/user name.
|
||||
projectName: 'superset', // Usually your repo name.
|
||||
themes: [
|
||||
'@saucelabs/theme-github-codeblock'
|
||||
],
|
||||
plugins: [
|
||||
themes: ['@saucelabs/theme-github-codeblock'],
|
||||
plugins: [
|
||||
[
|
||||
'@docusaurus/plugin-client-redirects',
|
||||
{
|
||||
@@ -117,6 +115,10 @@ const config = {
|
||||
to: '/docs/contributing/contributing-page',
|
||||
from: '/docs/contributing/contribution-guidelines',
|
||||
},
|
||||
{
|
||||
to: '/docs/databases/yugabytedb',
|
||||
from: '/docs/databases/yugabyte/',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -155,6 +157,11 @@ const config = {
|
||||
defaultMode: 'light',
|
||||
disableSwitch: true,
|
||||
},
|
||||
algolia: {
|
||||
appId: 'WR5FASX5ED',
|
||||
apiKey: '299e4601d2fc5d0031bf9a0223c7f0c5',
|
||||
indexName: 'superset-apache',
|
||||
},
|
||||
navbar: {
|
||||
logo: {
|
||||
alt: 'Superset Logo',
|
||||
@@ -197,7 +204,7 @@ const config = {
|
||||
},
|
||||
{
|
||||
label: 'Slack',
|
||||
href: 'https://join.slack.com/t/apache-superset/shared_invite/zt-uxbh5g36-AISUtHbzOXcu0BIj7kgUaw',
|
||||
href: 'https://join.slack.com/t/apache-superset/shared_invite/zt-16jvzmoi8-sI7jKWp~xc2zYRe~NqiY9Q',
|
||||
},
|
||||
{
|
||||
label: 'Mailing List',
|
||||
@@ -233,8 +240,6 @@ const config = {
|
||||
darkTheme: darkCodeTheme,
|
||||
},
|
||||
}),
|
||||
|
||||
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
11303
docs/package-lock.json
generated
11303
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,17 +16,19 @@
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@algolia/client-search": "^4.13.0",
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
"@docusaurus/core": "^2.0.0-beta.15",
|
||||
"@docusaurus/plugin-client-redirects": "^2.0.0-beta.15",
|
||||
"@docusaurus/preset-classic": "^2.0.0-beta.15",
|
||||
"@docsearch/react": "^3.0.0",
|
||||
"@docusaurus/core": "^2.0.0-beta.17",
|
||||
"@docusaurus/plugin-client-redirects": "^2.0.0-beta.17",
|
||||
"@docusaurus/preset-classic": "^2.0.0-beta.17",
|
||||
"@emotion/core": "^10.1.1",
|
||||
"@emotion/styled": "^10.0.27",
|
||||
"@mdx-js/react": "^1.6.21",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@saucelabs/theme-github-codeblock": "^0.1.1",
|
||||
"@superset-ui/style": "^0.14.23",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"antd": "^4.8.0",
|
||||
"antd": "^4.19.3",
|
||||
"buffer": "^6.0.3",
|
||||
"clsx": "^1.1.1",
|
||||
"file-loader": "^6.2.0",
|
||||
@@ -36,13 +38,14 @@
|
||||
"react-github-btn": "^1.2.0",
|
||||
"stream": "^0.0.2",
|
||||
"swagger-ui-react": "^4.1.2",
|
||||
"theme-ui": "^0.3.1",
|
||||
"url-loader": "^4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^2.0.0-beta.15",
|
||||
"@docusaurus/module-type-aliases": "^2.0.0-beta.17",
|
||||
"@tsconfig/docusaurus": "^1.0.4",
|
||||
"typescript": "^4.3.5"
|
||||
"@types/react": "^17.0.42",
|
||||
"typescript": "^4.3.5",
|
||||
"webpack": "^5.61.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -23,7 +23,7 @@ import Layout from '@theme/Layout';
|
||||
|
||||
const links = [
|
||||
[
|
||||
'https://join.slack.com/t/apache-superset/shared_invite/zt-uxbh5g36-AISUtHbzOXcu0BIj7kgUaw',
|
||||
'https://join.slack.com/t/apache-superset/shared_invite/zt-16jvzmoi8-sI7jKWp~xc2zYRe~NqiY9Q',
|
||||
'Slack',
|
||||
'interact with other Superset users and community members',
|
||||
],
|
||||
|
||||
6708
docs/static/resources/openapi.json
vendored
6708
docs/static/resources/openapi.json
vendored
File diff suppressed because it is too large
Load Diff
2172
docs/yarn.lock
2172
docs/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@ maintainers:
|
||||
- name: craig-rueda
|
||||
email: craig@craigrueda.com
|
||||
url: https://github.com/craig-rueda
|
||||
version: 0.5.10
|
||||
version: 0.5.11
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 10.2.0
|
||||
|
||||
@@ -148,6 +148,9 @@ configOverrides: {}
|
||||
# AUTH_USER_REGISTRATION = True
|
||||
# # The default user self registration role
|
||||
# AUTH_USER_REGISTRATION_ROLE = "Admin"
|
||||
# secret: |
|
||||
# # Generate your own secret key for encryption. Use openssl rand -base64 42 to generate a good key
|
||||
# SECRET_KEY = 'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY'
|
||||
# Same as above but the values are files
|
||||
configOverridesFiles: {}
|
||||
# extend_timeout: extend_timeout.py
|
||||
@@ -302,6 +305,8 @@ init:
|
||||
# Configure resources
|
||||
# Warning: fab command consumes a lot of ram and can
|
||||
# cause the process to be killed due to OOM if it exceeds limit
|
||||
# Make sure you are giving a strong password for the admin user creation( else make sure you are changing after setup)
|
||||
# Also change the admin email to your own custom email.
|
||||
resources: {}
|
||||
# limits:
|
||||
# cpu:
|
||||
|
||||
@@ -77,7 +77,7 @@ flask==1.1.4
|
||||
# flask-openid
|
||||
# flask-sqlalchemy
|
||||
# flask-wtf
|
||||
flask-appbuilder==3.4.3
|
||||
flask-appbuilder==3.4.5
|
||||
# via apache-superset
|
||||
flask-babel==1.0.0
|
||||
# via flask-appbuilder
|
||||
@@ -113,6 +113,8 @@ graphlib-backport==1.0.3
|
||||
# via apache-superset
|
||||
gunicorn==20.1.0
|
||||
# via apache-superset
|
||||
hashids==1.3.1
|
||||
# via apache-superset
|
||||
holidays==0.10.3
|
||||
# via apache-superset
|
||||
humanize==3.11.0
|
||||
|
||||
@@ -36,6 +36,7 @@ pytest
|
||||
pytest-cov
|
||||
statsd
|
||||
pytest-mock
|
||||
sqloxide
|
||||
# DB dependencies
|
||||
-e file:.[bigquery]
|
||||
-e file:.[trino]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# SHA1:7a8e256097b4758bdeda2529d3d4d31e421e1a3c
|
||||
# SHA1:e273e8da6bfd5f6f8563fe067e243297cc7c588c
|
||||
#
|
||||
# This file is autogenerated by pip-compile-multi
|
||||
# To update, run:
|
||||
@@ -52,7 +52,6 @@ google-auth-oauthlib==0.4.6
|
||||
google-cloud-bigquery[bqstorage,pandas]==2.29.0
|
||||
# via
|
||||
# -r requirements/testing.in
|
||||
# apache-superset
|
||||
# pandas-gbq
|
||||
# pybigquery
|
||||
google-cloud-bigquery-storage==2.9.1
|
||||
@@ -105,9 +104,7 @@ openapi-schema-validator==0.1.5
|
||||
openapi-spec-validator==0.3.1
|
||||
# via -r requirements/testing.in
|
||||
pandas-gbq==0.15.0
|
||||
# via
|
||||
# -r requirements/testing.in
|
||||
# apache-superset
|
||||
# via -r requirements/testing.in
|
||||
parameterized==0.8.1
|
||||
# via -r requirements/testing.in
|
||||
parso==0.8.2
|
||||
@@ -138,9 +135,7 @@ pyasn1==0.4.8
|
||||
pyasn1-modules==0.2.8
|
||||
# via google-auth
|
||||
pybigquery==0.10.2
|
||||
# via
|
||||
# -r requirements/testing.in
|
||||
# apache-superset
|
||||
# via -r requirements/testing.in
|
||||
pydata-google-auth==1.2.0
|
||||
# via pandas-gbq
|
||||
pyfakefs==4.5.0
|
||||
@@ -168,6 +163,8 @@ rsa==4.7.2
|
||||
# via google-auth
|
||||
sqlalchemy-trino==0.4.1
|
||||
# via apache-superset
|
||||
sqloxide==0.1.15
|
||||
# via -r requirements/testing.in
|
||||
statsd==3.3.0
|
||||
# via -r requirements/testing.in
|
||||
traitlets==5.0.5
|
||||
|
||||
@@ -102,7 +102,10 @@ def find_models(module: ModuleType) -> List[Type[Model]]:
|
||||
while tables:
|
||||
table = tables.pop()
|
||||
seen.add(table)
|
||||
model = getattr(Base.classes, table)
|
||||
try:
|
||||
model = getattr(Base.classes, table)
|
||||
except AttributeError:
|
||||
continue
|
||||
model.__tablename__ = table
|
||||
models.append(model)
|
||||
|
||||
|
||||
@@ -60,7 +60,8 @@ def request(
|
||||
|
||||
|
||||
def list_runs(
|
||||
repo: str, params: Optional[Dict[str, str]] = None,
|
||||
repo: str,
|
||||
params: Optional[Dict[str, str]] = None,
|
||||
) -> Iterator[Dict[str, Any]]:
|
||||
"""List all github workflow runs.
|
||||
Returns:
|
||||
@@ -193,7 +194,11 @@ def cancel_github_workflows(
|
||||
if branch and ":" in branch:
|
||||
[user, branch] = branch.split(":", 2)
|
||||
runs = get_runs(
|
||||
repo, branch=branch, user=user, statuses=statuses, events=events,
|
||||
repo,
|
||||
branch=branch,
|
||||
user=user,
|
||||
statuses=statuses,
|
||||
events=events,
|
||||
)
|
||||
|
||||
# sort old jobs to the front, so to cancel older jobs first
|
||||
|
||||
5
setup.py
5
setup.py
@@ -23,8 +23,8 @@ import sys
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
PACKAGE_JSON = os.path.join(BASE_DIR, "superset-frontend", "package.json")
|
||||
|
||||
with open(PACKAGE_JSON, "r") as package_file:
|
||||
version_string = json.load(package_file)["version"]
|
||||
|
||||
@@ -78,7 +78,7 @@ setup(
|
||||
"cryptography>=3.3.2",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
"flask>=1.1.0, <2.0.0",
|
||||
"flask-appbuilder>=3.4.3, <4.0.0",
|
||||
"flask-appbuilder>=3.4.5, <4.0.0",
|
||||
"flask-caching>=1.10.0",
|
||||
"flask-compress",
|
||||
"flask-talisman",
|
||||
@@ -88,6 +88,7 @@ setup(
|
||||
"geopy",
|
||||
"graphlib-backport",
|
||||
"gunicorn>=20.1.0",
|
||||
"hashids>=1.3.1, <2",
|
||||
"holidays==0.10.3", # PINNED! https://github.com/dr-prodigy/python-holidays/issues/406
|
||||
"humanize",
|
||||
"itsdangerous>=1.0.0, <2.0.0", # https://github.com/apache/superset/pull/14627
|
||||
|
||||
@@ -40,6 +40,7 @@ embedDashboard({
|
||||
supersetDomain: "https://superset.example.com",
|
||||
mountPoint: document.getElementById("my-superset-container"), // any html element that can contain an iframe
|
||||
fetchGuestToken: () => fetchGuestTokenFromBackend(),
|
||||
dashboardUiConfig: { hideTitle: true }, // dashboard UI config: hideTitle, hideTab, hideChartControls (optional)
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
7419
superset-embedded-sdk/package-lock.json
generated
7419
superset-embedded-sdk/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@superset-ui/embedded-sdk",
|
||||
"version": "0.1.0-alpha.3",
|
||||
"version": "0.1.0-alpha.7",
|
||||
"description": "SDK for embedding resources from Superset into your own application",
|
||||
"access": "public",
|
||||
"keywords": [
|
||||
@@ -24,7 +24,7 @@
|
||||
"scripts": {
|
||||
"build": "tsc ; babel src --out-dir lib --extensions '.ts,.tsx' ; webpack --mode production",
|
||||
"ci:release": "node ./release-if-necessary.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "jest"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 3 chrome versions",
|
||||
@@ -40,8 +40,10 @@
|
||||
"@babel/core": "^7.16.12",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@types/jest": "^27.4.1",
|
||||
"axios": "^0.25.0",
|
||||
"babel-loader": "^8.2.3",
|
||||
"jest": "^27.5.1",
|
||||
"typescript": "^4.5.5",
|
||||
"webpack": "^5.67.0",
|
||||
"webpack-cli": "^4.9.2"
|
||||
|
||||
96
superset-embedded-sdk/src/guestTokenRefresh.test.ts
Normal file
96
superset-embedded-sdk/src/guestTokenRefresh.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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 {
|
||||
REFRESH_TIMING_BUFFER_MS,
|
||||
getGuestTokenRefreshTiming,
|
||||
MIN_REFRESH_WAIT_MS,
|
||||
DEFAULT_TOKEN_EXP_MS,
|
||||
} from "./guestTokenRefresh";
|
||||
|
||||
describe("guest token refresh", () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers("modern"); // "modern" allows us to fake the system time
|
||||
jest.setSystemTime(new Date("2022-03-03 01:00"));
|
||||
jest.spyOn(global, "setTimeout");
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
function makeFakeJWT(claims: any) {
|
||||
// not a valid jwt, but close enough for this code
|
||||
const tokenifiedClaims = Buffer.from(JSON.stringify(claims)).toString(
|
||||
"base64"
|
||||
);
|
||||
return `abc.${tokenifiedClaims}.xyz`;
|
||||
}
|
||||
|
||||
it("schedules refresh with an epoch exp", () => {
|
||||
// exp is in seconds
|
||||
const ttl = 1300;
|
||||
const exp = Date.now() / 1000 + ttl;
|
||||
const fakeToken = makeFakeJWT({ exp });
|
||||
|
||||
const timing = getGuestTokenRefreshTiming(fakeToken);
|
||||
|
||||
expect(timing).toBeGreaterThan(MIN_REFRESH_WAIT_MS);
|
||||
expect(timing).toBe(ttl * 1000 - REFRESH_TIMING_BUFFER_MS);
|
||||
});
|
||||
|
||||
it("schedules refresh with an epoch exp containing a decimal", () => {
|
||||
const ttl = 1300.123;
|
||||
const exp = Date.now() / 1000 + ttl;
|
||||
const fakeToken = makeFakeJWT({ exp });
|
||||
|
||||
const timing = getGuestTokenRefreshTiming(fakeToken);
|
||||
|
||||
expect(timing).toBeGreaterThan(MIN_REFRESH_WAIT_MS);
|
||||
expect(timing).toBe(ttl * 1000 - REFRESH_TIMING_BUFFER_MS);
|
||||
});
|
||||
|
||||
it("schedules refresh with iso exp", () => {
|
||||
const exp = new Date("2022-03-03 01:09").toISOString();
|
||||
const fakeToken = makeFakeJWT({ exp });
|
||||
|
||||
const timing = getGuestTokenRefreshTiming(fakeToken);
|
||||
const expectedTiming = 1000 * 60 * 9 - REFRESH_TIMING_BUFFER_MS;
|
||||
|
||||
expect(timing).toBeGreaterThan(MIN_REFRESH_WAIT_MS);
|
||||
expect(timing).toBe(expectedTiming);
|
||||
});
|
||||
|
||||
it("avoids refresh spam", () => {
|
||||
const fakeToken = makeFakeJWT({ exp: Date.now() / 1000 });
|
||||
|
||||
const timing = getGuestTokenRefreshTiming(fakeToken);
|
||||
|
||||
expect(timing).toBe(MIN_REFRESH_WAIT_MS - REFRESH_TIMING_BUFFER_MS);
|
||||
});
|
||||
|
||||
it("uses a default when it cannot parse the date", () => {
|
||||
const fakeToken = makeFakeJWT({ exp: "invalid date" });
|
||||
|
||||
const timing = getGuestTokenRefreshTiming(fakeToken);
|
||||
|
||||
expect(timing).toBeGreaterThan(MIN_REFRESH_WAIT_MS);
|
||||
expect(timing).toBe(DEFAULT_TOKEN_EXP_MS - REFRESH_TIMING_BUFFER_MS);
|
||||
});
|
||||
});
|
||||
32
superset-embedded-sdk/src/guestTokenRefresh.ts
Normal file
32
superset-embedded-sdk/src/guestTokenRefresh.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const REFRESH_TIMING_BUFFER_MS = 5000 // refresh guest token early to avoid failed superset requests
|
||||
export const MIN_REFRESH_WAIT_MS = 10000 // avoid blasting requests as fast as the cpu can handle
|
||||
export const DEFAULT_TOKEN_EXP_MS = 300000 // (5 min) used only when parsing guest token exp fails
|
||||
|
||||
// when do we refresh the guest token?
|
||||
export function getGuestTokenRefreshTiming(currentGuestToken: string) {
|
||||
const parsedJwt = JSON.parse(Buffer.from(currentGuestToken.split('.')[1], 'base64').toString());
|
||||
// if exp is int, it is in seconds, but Date() takes milliseconds
|
||||
const exp = new Date(/[^0-9\.]/g.test(parsedJwt.exp) ? parsedJwt.exp : parseFloat(parsedJwt.exp) * 1000);
|
||||
const isValidDate = exp.toString() !== 'Invalid Date';
|
||||
const ttl = isValidDate ? Math.max(MIN_REFRESH_WAIT_MS, exp.getTime() - Date.now()) : DEFAULT_TOKEN_EXP_MS;
|
||||
return ttl - REFRESH_TIMING_BUFFER_MS;
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import { IFRAME_COMMS_MESSAGE_TYPE } from './const';
|
||||
|
||||
// We can swap this out for the actual switchboard package once it gets published
|
||||
import { Switchboard } from '@superset-ui/switchboard';
|
||||
import { getGuestTokenRefreshTiming } from './guestTokenRefresh';
|
||||
|
||||
/**
|
||||
* The function to fetch a guest token from your Host App's backend server.
|
||||
@@ -29,6 +30,12 @@ import { Switchboard } from '@superset-ui/switchboard';
|
||||
*/
|
||||
export type GuestTokenFetchFn = () => Promise<string>;
|
||||
|
||||
export type UiConfigType = {
|
||||
hideTitle?: boolean
|
||||
hideTab?: boolean
|
||||
hideChartControls?: boolean
|
||||
}
|
||||
|
||||
export type EmbedDashboardParams = {
|
||||
/** The id provided by the embed configuration UI in Superset */
|
||||
id: string
|
||||
@@ -38,6 +45,8 @@ export type EmbedDashboardParams = {
|
||||
mountPoint: HTMLElement
|
||||
/** A function to fetch a guest token from the Host App's backend server */
|
||||
fetchGuestToken: GuestTokenFetchFn
|
||||
/** The dashboard UI config: hideTitle, hideTab, hideChartControls **/
|
||||
dashboardUiConfig?: UiConfigType
|
||||
/** Are we in debug mode? */
|
||||
debug?: boolean
|
||||
}
|
||||
@@ -59,6 +68,7 @@ export async function embedDashboard({
|
||||
supersetDomain,
|
||||
mountPoint,
|
||||
fetchGuestToken,
|
||||
dashboardUiConfig,
|
||||
debug = false
|
||||
}: EmbedDashboardParams): Promise<EmbeddedDashboard> {
|
||||
function log(...info: unknown[]) {
|
||||
@@ -69,14 +79,32 @@ export async function embedDashboard({
|
||||
|
||||
log('embedding');
|
||||
|
||||
function calculateConfig() {
|
||||
let configNumber = 0
|
||||
if(dashboardUiConfig) {
|
||||
if(dashboardUiConfig.hideTitle) {
|
||||
configNumber += 1
|
||||
}
|
||||
if(dashboardUiConfig.hideTab) {
|
||||
configNumber += 2
|
||||
}
|
||||
if(dashboardUiConfig.hideChartControls) {
|
||||
configNumber += 8
|
||||
}
|
||||
}
|
||||
return configNumber
|
||||
}
|
||||
|
||||
async function mountIframe(): Promise<Switchboard> {
|
||||
return new Promise(resolve => {
|
||||
const iframe = document.createElement('iframe');
|
||||
const dashboardConfig = dashboardUiConfig ? `?uiConfig=${calculateConfig()}` : ""
|
||||
|
||||
// setup the iframe's sandbox configuration
|
||||
iframe.sandbox.add("allow-same-origin"); // needed for postMessage to work
|
||||
iframe.sandbox.add("allow-scripts"); // obviously the iframe needs scripts
|
||||
iframe.sandbox.add("allow-presentation"); // for fullscreen charts
|
||||
iframe.sandbox.add("allow-downloads"); // for downloading charts as image
|
||||
// add these ones if it turns out we need them:
|
||||
// iframe.sandbox.add("allow-top-navigation");
|
||||
// iframe.sandbox.add("allow-forms");
|
||||
@@ -103,7 +131,7 @@ export async function embedDashboard({
|
||||
resolve(new Switchboard({ port: ourPort, name: 'superset-embedded-sdk', debug }));
|
||||
});
|
||||
|
||||
iframe.src = `${supersetDomain}/dashboard/${id}/embedded`;
|
||||
iframe.src = `${supersetDomain}/embedded/${id}${dashboardConfig}`;
|
||||
mountPoint.replaceChildren(iframe);
|
||||
log('placed the iframe')
|
||||
});
|
||||
@@ -117,6 +145,14 @@ export async function embedDashboard({
|
||||
ourPort.emit('guestToken', { guestToken });
|
||||
log('sent guest token');
|
||||
|
||||
async function refreshGuestToken() {
|
||||
const newGuestToken = await fetchGuestToken();
|
||||
ourPort.emit('guestToken', { guestToken: newGuestToken });
|
||||
setTimeout(refreshGuestToken, getGuestTokenRefreshTiming(newGuestToken));
|
||||
}
|
||||
|
||||
setTimeout(refreshGuestToken, getGuestTokenRefreshTiming(guestToken));
|
||||
|
||||
function unmount() {
|
||||
log('unmounting');
|
||||
mountPoint.replaceChildren();
|
||||
|
||||
@@ -67,7 +67,7 @@ module.exports = {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
plugins: ['prettier', 'react', 'file-progress'],
|
||||
plugins: ['prettier', 'react', 'file-progress', 'theme-colors'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
@@ -183,8 +183,27 @@ module.exports = {
|
||||
'max-classes-per-file': 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'*.test.ts',
|
||||
'*.test.tsx',
|
||||
'*.test.js',
|
||||
'*.test.jsx',
|
||||
'*.stories.tsx',
|
||||
'*.stories.jsx',
|
||||
'fixtures.*',
|
||||
'cypress-base/cypress/**/*',
|
||||
'Stories.tsx',
|
||||
'packages/superset-ui-core/src/style/index.tsx',
|
||||
],
|
||||
rules: {
|
||||
'theme-colors/no-literal-colors': 0,
|
||||
'no-restricted-imports': 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: {
|
||||
'theme-colors/no-literal-colors': 1,
|
||||
camelcase: [
|
||||
'error',
|
||||
{
|
||||
|
||||
@@ -33,6 +33,7 @@ module.exports = {
|
||||
'@storybook/addon-knobs',
|
||||
'storybook-addon-paddings',
|
||||
],
|
||||
staticDirs: ['../src/assets/images'],
|
||||
webpackFinal: config => ({
|
||||
...config,
|
||||
module: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:8088",
|
||||
"chromeWebSecurity": false,
|
||||
"defaultCommandTimeout": 5000,
|
||||
"defaultCommandTimeout": 8000,
|
||||
"numTestsKeptInMemory": 0,
|
||||
"experimentalFetchPolyfill": true,
|
||||
"requestTimeout": 10000,
|
||||
@@ -12,7 +12,7 @@
|
||||
"viewportHeight": 1024,
|
||||
"projectId": "ukwxzo",
|
||||
"retries": {
|
||||
"runMode": 1,
|
||||
"runMode": 2,
|
||||
"openMode": 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { getChartAlias, Slice } from 'cypress/utils/vizPlugins';
|
||||
import {
|
||||
dashboardView,
|
||||
editDashboardView,
|
||||
nativeFilters,
|
||||
} from 'cypress/support/directories';
|
||||
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
@@ -25,7 +30,13 @@ export const testItems = {
|
||||
dashboard: 'Cypress Sales Dashboard',
|
||||
dataset: 'Vehicle Sales',
|
||||
chart: 'Cypress chart',
|
||||
newChart: 'New Cypress Chart',
|
||||
createdDashboard: 'New Dashboard',
|
||||
defaultNameDashboard: '[ untitled dashboard ]',
|
||||
newDashboardTitle: `Test dashboard [NEW TEST]`,
|
||||
bulkFirstNameDashboard: 'First Dash',
|
||||
bulkSecondNameDashboard: 'Second Dash',
|
||||
worldBanksDataCopy: `World Bank's Data [copy]`,
|
||||
};
|
||||
|
||||
export const CHECK_DASHBOARD_FAVORITE_ENDPOINT =
|
||||
@@ -133,3 +144,112 @@ export function resize(selector: string) {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function cleanUp() {
|
||||
cy.deleteDashboardByName(testItems.dashboard);
|
||||
cy.deleteDashboardByName(testItems.defaultNameDashboard);
|
||||
cy.deleteDashboardByName('');
|
||||
cy.deleteDashboardByName(testItems.newDashboardTitle);
|
||||
cy.deleteDashboardByName(testItems.bulkFirstNameDashboard);
|
||||
cy.deleteDashboardByName(testItems.bulkSecondNameDashboard);
|
||||
cy.deleteDashboardByName(testItems.createdDashboard);
|
||||
cy.deleteDashboardByName(testItems.worldBanksDataCopy);
|
||||
cy.deleteChartByName(testItems.chart);
|
||||
cy.deleteChartByName(testItems.newChart);
|
||||
}
|
||||
|
||||
/** ************************************************************************
|
||||
* Clicks on new filter button
|
||||
* @returns {None}
|
||||
* @summary helper for adding new filter
|
||||
************************************************************************* */
|
||||
export function clickOnAddFilterInModal() {
|
||||
return cy
|
||||
.get(nativeFilters.addFilterButton.button)
|
||||
.first()
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get(nativeFilters.addFilterButton.dropdownItem)
|
||||
.contains('Filter')
|
||||
.click({ force: true });
|
||||
});
|
||||
}
|
||||
|
||||
/** ************************************************************************
|
||||
* Fills value native filter form with basic information
|
||||
* @param {string} name name for filter
|
||||
* @param {string} dataset which dataset should be used
|
||||
* @param {string} filterColumn which column should be used
|
||||
* @returns {None}
|
||||
* @summary helper for filling value native filter form
|
||||
************************************************************************* */
|
||||
export function fillValueNativeFilterForm(
|
||||
name: string,
|
||||
dataset: string,
|
||||
filterColumn: string,
|
||||
) {
|
||||
cy.get(nativeFilters.modal.container)
|
||||
.find(nativeFilters.filtersPanel.filterName)
|
||||
.last()
|
||||
.click({ scrollBehavior: false })
|
||||
.type(name, { scrollBehavior: false });
|
||||
cy.get(nativeFilters.modal.container)
|
||||
.find(nativeFilters.filtersPanel.datasetName)
|
||||
.last()
|
||||
.click({ scrollBehavior: false })
|
||||
.type(`${dataset}{enter}`, { scrollBehavior: false });
|
||||
cy.get(nativeFilters.silentLoading).should('not.exist');
|
||||
cy.get(nativeFilters.filtersPanel.filterInfoInput)
|
||||
.last()
|
||||
.should('be.visible')
|
||||
.click({ force: true });
|
||||
cy.get(nativeFilters.filtersPanel.filterInfoInput).last().type(filterColumn);
|
||||
cy.get(nativeFilters.filtersPanel.inputDropdown)
|
||||
.should('be.visible', { timeout: 20000 })
|
||||
.last()
|
||||
.click();
|
||||
}
|
||||
/** ************************************************************************
|
||||
* Get native filter placeholder e.g 9 options
|
||||
* @param {number} index which input it fills
|
||||
* @returns cy object for assertions
|
||||
* @summary helper for getting placeholder value
|
||||
************************************************************************* */
|
||||
export function getNativeFilterPlaceholderWithIndex(index: number) {
|
||||
return cy.get(nativeFilters.filtersPanel.columnEmptyInput).eq(index);
|
||||
}
|
||||
|
||||
/** ************************************************************************
|
||||
* Apply native filter value from dashboard view
|
||||
* @param {number} index which input it fills
|
||||
* @param {string} value what is filter value
|
||||
* @returns {null}
|
||||
* @summary put value to nth native filter input in view
|
||||
************************************************************************* */
|
||||
export function applyNativeFilterValueWithIndex(index: number, value: string) {
|
||||
cy.get(nativeFilters.filterFromDashboardView.filterValueInput)
|
||||
.eq(index)
|
||||
.parent()
|
||||
.should('be.visible', { timeout: 10000 })
|
||||
.type(`${value}{enter}`);
|
||||
// click the title to dismiss shown options
|
||||
cy.get(nativeFilters.filterFromDashboardView.filterName).eq(index).click();
|
||||
}
|
||||
|
||||
/** ************************************************************************
|
||||
* Fills parent filter input
|
||||
* @param {number} index which input it fills
|
||||
* @param {string} value on which filter it depends on
|
||||
* @returns {null}
|
||||
* @summary takes first or second input and modify the depends on filter value
|
||||
************************************************************************* */
|
||||
export function addParentFilterWithValue(index: number, value: string) {
|
||||
return cy
|
||||
.get(nativeFilters.filterConfigurationSections.displayedSection)
|
||||
.within(() => {
|
||||
cy.get('input[aria-label="Limit type"]')
|
||||
.eq(index)
|
||||
.click({ force: true })
|
||||
.type(`${value}{enter}`, { delay: 30, force: true });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,16 +27,19 @@ interface QueryString {
|
||||
native_filters_key: string;
|
||||
}
|
||||
|
||||
describe('nativefiler url param key', () => {
|
||||
xdescribe('nativefiler url param key', () => {
|
||||
// const urlParams = { param1: '123', param2: 'abc' };
|
||||
before(() => {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
let initialFilterKey: string;
|
||||
it('should have cachekey in nativefilter param', () => {
|
||||
// things in `before` will not retry and the `waitForChartLoad` check is
|
||||
// especically flaky and may need more retries
|
||||
cy.visit(WORLD_HEALTH_DASHBOARD);
|
||||
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
|
||||
cy.wait(1000); // wait for key to be published (debounced)
|
||||
});
|
||||
let initialFilterKey: string;
|
||||
it('should have cachekey in nativefilter param', () => {
|
||||
cy.location().then(loc => {
|
||||
const queryParams = qs.parse(loc.search) as QueryString;
|
||||
expect(typeof queryParams.native_filters_key).eq('string');
|
||||
@@ -44,6 +47,9 @@ describe('nativefiler url param key', () => {
|
||||
});
|
||||
|
||||
it('should have different key when page reloads', () => {
|
||||
cy.visit(WORLD_HEALTH_DASHBOARD);
|
||||
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
|
||||
cy.wait(1000); // wait for key to be published (debounced)
|
||||
cy.location().then(loc => {
|
||||
const queryParams = qs.parse(loc.search) as QueryString;
|
||||
expect(queryParams.native_filters_key).not.equal(initialFilterKey);
|
||||
|
||||
@@ -22,7 +22,17 @@ import {
|
||||
nativeFilters,
|
||||
exploreView,
|
||||
} from 'cypress/support/directories';
|
||||
import { testItems } from './dashboard.helper';
|
||||
import {
|
||||
cleanUp,
|
||||
testItems,
|
||||
WORLD_HEALTH_CHARTS,
|
||||
waitForChartLoad,
|
||||
clickOnAddFilterInModal,
|
||||
fillValueNativeFilterForm,
|
||||
getNativeFilterPlaceholderWithIndex,
|
||||
addParentFilterWithValue,
|
||||
applyNativeFilterValueWithIndex,
|
||||
} from './dashboard.helper';
|
||||
import { DASHBOARD_LIST } from '../dashboard_list/dashboard_list.helper';
|
||||
import { CHART_LIST } from '../chart_list/chart_list.helper';
|
||||
import { FORM_DATA_DEFAULTS } from '../explore/visualizations/shared.helper';
|
||||
@@ -39,21 +49,27 @@ const milliseconds = new Date().getTime();
|
||||
const dashboard = `Test Dashboard${milliseconds}`;
|
||||
|
||||
describe('Nativefilters Sanity test', () => {
|
||||
before(() => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cleanUp();
|
||||
cy.intercept('/api/v1/dashboard/?q=**').as('dashboardsList');
|
||||
cy.intercept('POST', '**/copy_dash/*').as('copy');
|
||||
cy.intercept('/api/v1/dashboard/*').as('dashboard');
|
||||
cy.request(
|
||||
'api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:100)',
|
||||
).then(xhr => {
|
||||
const dashboards = xhr.body.result;
|
||||
cy.intercept('GET', '**/api/v1/dataset/**').as('datasetLoad');
|
||||
cy.intercept('**/api/v1/dashboard/?q=**').as('dashboardsList');
|
||||
cy.visit('dashboard/list/');
|
||||
cy.contains('Actions');
|
||||
cy.wait('@dashboardsList').then(xhr => {
|
||||
const dashboards = xhr.response?.body.result;
|
||||
/* eslint-disable no-unused-expressions */
|
||||
expect(dashboards).not.to.be.undefined;
|
||||
const worldBankDashboard = dashboards.find(
|
||||
(d: { dashboard_title: string }) =>
|
||||
d.dashboard_title === "World Bank's Data",
|
||||
);
|
||||
cy.visit(worldBankDashboard.url);
|
||||
});
|
||||
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
|
||||
cy.get(dashboardView.threeDotsMenuIcon).should('be.visible').click();
|
||||
cy.get(dashboardView.saveAsMenuOption).should('be.visible').click();
|
||||
cy.get(dashboardView.saveModal.dashboardNameInput)
|
||||
@@ -65,19 +81,10 @@ describe('Nativefilters Sanity test', () => {
|
||||
.its('response.statusCode')
|
||||
.should('eq', 200);
|
||||
});
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.request(
|
||||
'api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:100)',
|
||||
).then(xhr => {
|
||||
const dashboards = xhr.body.result;
|
||||
const testDashboard = dashboards.find(
|
||||
(d: { dashboard_title: string }) =>
|
||||
d.dashboard_title === testItems.dashboard,
|
||||
);
|
||||
cy.visit(testDashboard.url);
|
||||
});
|
||||
afterEach(() => {
|
||||
cleanUp();
|
||||
});
|
||||
|
||||
it('User can expand / retract native filter sidebar on a dashboard', () => {
|
||||
cy.get(nativeFilters.createFilterButton).should('not.exist');
|
||||
cy.get(nativeFilters.filterFromDashboardView.expand)
|
||||
@@ -123,21 +130,10 @@ describe('Nativefilters Sanity test', () => {
|
||||
.within(() =>
|
||||
cy.get('input').type('wb_health_population{enter}', { force: true }),
|
||||
);
|
||||
// Add following step to avoid flaky enter value in line 177
|
||||
cy.get(nativeFilters.filtersPanel.inputDropdown)
|
||||
.should('be.visible', { timeout: 20000 })
|
||||
.last()
|
||||
.click();
|
||||
|
||||
cy.get('.loading inline-centered css-101mkpk').should('not.exist');
|
||||
// hack for unclickable country_name
|
||||
cy.wait(5000);
|
||||
cy.get(nativeFilters.filtersPanel.filterInfoInput)
|
||||
.last()
|
||||
.should('be.visible', { timeout: 30000 })
|
||||
.click({ force: true });
|
||||
cy.get(nativeFilters.filtersPanel.filterInfoInput)
|
||||
cy.get(`${nativeFilters.filtersPanel.filterInfoInput}:visible:last`)
|
||||
.last()
|
||||
.focus()
|
||||
.type('country_name');
|
||||
cy.get(nativeFilters.filtersPanel.inputDropdown)
|
||||
.should('be.visible', { timeout: 20000 })
|
||||
@@ -270,7 +266,6 @@ describe('Nativefilters Sanity test', () => {
|
||||
'Filter has default value',
|
||||
'Can select multiple values',
|
||||
'Filter value is required',
|
||||
'Filter is hierarchical',
|
||||
'Select first filter value by default',
|
||||
'Inverse selection',
|
||||
'Dynamically search all filter values',
|
||||
@@ -402,15 +397,6 @@ describe('Nativefilters Sanity test', () => {
|
||||
cy.get('.line').within(() => {
|
||||
cy.contains('United States').should('be.visible');
|
||||
});
|
||||
|
||||
// clean up the default setting
|
||||
cy.get(nativeFilters.filterFromDashboardView.expand).click({ force: true });
|
||||
cy.get(nativeFilters.filterFromDashboardView.createFilterButton).click();
|
||||
cy.contains('Filter has default value').click();
|
||||
cy.get(nativeFilters.modal.footer)
|
||||
.find(nativeFilters.modal.saveButton)
|
||||
.should('be.visible')
|
||||
.click({ force: true });
|
||||
});
|
||||
|
||||
it('User can create a time grain filter', () => {
|
||||
@@ -542,6 +528,87 @@ describe('Nativefilters Sanity test', () => {
|
||||
.contains('year')
|
||||
.should('be.visible');
|
||||
});
|
||||
it('User can create a value filter', () => {
|
||||
cy.get(nativeFilters.filterFromDashboardView.expand).click({ force: true });
|
||||
cy.get(nativeFilters.filterFromDashboardView.createFilterButton)
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.get(nativeFilters.modal.container).should('be.visible');
|
||||
cy.get('body').type('{home}');
|
||||
|
||||
cy.get(nativeFilters.filtersPanel.filterTypeInput)
|
||||
.click({ scrollBehavior: false })
|
||||
.type('{home}Value{enter}', { scrollBehavior: false });
|
||||
cy.get(nativeFilters.filtersPanel.filterTypeInput)
|
||||
.find(nativeFilters.filtersPanel.filterTypeItem)
|
||||
.should('have.text', 'Value');
|
||||
cy.get(nativeFilters.modal.container)
|
||||
.find(nativeFilters.filtersPanel.filterName)
|
||||
.click({ scrollBehavior: false })
|
||||
.clear()
|
||||
.type('country_name', { scrollBehavior: false });
|
||||
|
||||
cy.get(nativeFilters.silentLoading).should('not.exist');
|
||||
cy.get(nativeFilters.filtersPanel.filterInfoInput)
|
||||
.last()
|
||||
.should('be.visible')
|
||||
.click({ force: true });
|
||||
cy.get(nativeFilters.filtersPanel.filterInfoInput)
|
||||
.last()
|
||||
.type('country_name {enter}');
|
||||
cy.get(nativeFilters.modal.footer)
|
||||
.find(nativeFilters.modal.saveButton)
|
||||
.should('be.visible')
|
||||
.click({ force: true });
|
||||
cy.get(nativeFilters.filterFromDashboardView.filterName)
|
||||
.should('be.visible', { timeout: 40000 })
|
||||
.contains('country_name');
|
||||
});
|
||||
|
||||
it('User can create parent filters using "Values are dependent on other filters"', () => {
|
||||
cy.get(nativeFilters.filterFromDashboardView.expand)
|
||||
.should('be.visible')
|
||||
.click({ force: true });
|
||||
cy.get(nativeFilters.filterFromDashboardView.createFilterButton).click();
|
||||
// Create parent filter 'region'.
|
||||
fillValueNativeFilterForm('region', 'wb_health_population', 'region');
|
||||
// Create filter 'country_name' depend on region filter.
|
||||
clickOnAddFilterInModal();
|
||||
fillValueNativeFilterForm(
|
||||
'country_name',
|
||||
'wb_health_population',
|
||||
'country_name',
|
||||
);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Values are dependent on other filters')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
addParentFilterWithValue(0, 'region');
|
||||
cy.wait(1000);
|
||||
cy.get(nativeFilters.modal.footer)
|
||||
.contains('Save')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
// Validate both filter in dashboard view.
|
||||
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
|
||||
['region', 'country_name'].forEach(it => {
|
||||
cy.get(nativeFilters.filterFromDashboardView.filterName)
|
||||
.contains(it)
|
||||
.should('be.visible');
|
||||
});
|
||||
getNativeFilterPlaceholderWithIndex(1)
|
||||
.invoke('text')
|
||||
.should('equal', '214 options', { timeout: 20000 });
|
||||
// apply first filter value and validate 2nd filter is depden on 1st filter.
|
||||
applyNativeFilterValueWithIndex(0, 'East Asia & Pacific');
|
||||
|
||||
getNativeFilterPlaceholderWithIndex(0).should('have.text', '36 options', {
|
||||
timeout: 20000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
xdescribe('Nativefilters', () => {
|
||||
|
||||
@@ -129,7 +129,7 @@ describe('Test datatable', () => {
|
||||
it('Datapane loads view samples', () => {
|
||||
cy.get('[data-test="data-tab"]').click();
|
||||
cy.contains('View samples').click();
|
||||
cy.get('[data-test="row-count-label"]').contains('10k rows retrieved');
|
||||
cy.get('[data-test="row-count-label"]').contains('1k rows retrieved');
|
||||
cy.get('.ant-empty-description').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,8 +37,8 @@ describe('SqlLab query panel', () => {
|
||||
const sampleResponse = {
|
||||
status: 'success',
|
||||
data: [{ '?column?': 1 }],
|
||||
columns: [{ name: '?column?', type: 'INT', is_date: false }],
|
||||
selected_columns: [{ name: '?column?', type: 'INT', is_date: false }],
|
||||
columns: [{ name: '?column?', type: 'INT', is_dttm: false }],
|
||||
selected_columns: [{ name: '?column?', type: 'INT', is_dttm: false }],
|
||||
expanded_columns: [],
|
||||
};
|
||||
|
||||
|
||||
@@ -31,11 +31,11 @@ describe('SqlLab query tabs', () => {
|
||||
cy.get('[data-test="sql-editor-tabs"]')
|
||||
.children()
|
||||
.eq(0)
|
||||
.contains(`Untitled Query ${initialTabCount + 1}`);
|
||||
.contains(`Untitled Query ${initialTabCount}`);
|
||||
cy.get('[data-test="sql-editor-tabs"]')
|
||||
.children()
|
||||
.eq(0)
|
||||
.contains(`Untitled Query ${initialTabCount + 2}`);
|
||||
.contains(`Untitled Query ${initialTabCount + 1}`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -327,6 +327,10 @@ export const nativeFilters = {
|
||||
addFilter: dataTestLocator('add-filter-button'),
|
||||
defaultValueCheck: '.ant-checkbox-checked',
|
||||
},
|
||||
addFilterButton: {
|
||||
button: `.ant-modal-content [data-test="new-dropdown-icon"]`,
|
||||
dropdownItem: '.ant-dropdown-menu-item',
|
||||
},
|
||||
filtersPanel: {
|
||||
filterName: dataTestLocator('filters-config-modal__name-input'),
|
||||
datasetName: dataTestLocator('filters-config-modal__datasource-input'),
|
||||
@@ -350,6 +354,7 @@ export const nativeFilters = {
|
||||
removeFilter: '[aria-label="remove"]',
|
||||
silentLoading: '.loading inline-centered css-101mkpk',
|
||||
filterConfigurationSections: {
|
||||
displayedSection: 'div[style="height: 100%; overflow-y: auto;"]',
|
||||
collapseExpandButton: '.ant-collapse-arrow',
|
||||
checkedCheckbox: '.ant-checkbox-wrapper-checked',
|
||||
infoTooltip: '[aria-label="Show info tooltip"]',
|
||||
|
||||
@@ -47,6 +47,20 @@ declare namespace Cypress {
|
||||
querySubstring?: string | RegExp;
|
||||
chartSelector?: JQuery.Selector;
|
||||
}): cy;
|
||||
|
||||
/**
|
||||
* Get
|
||||
*/
|
||||
getDashboards(): cy;
|
||||
getCharts(): cy;
|
||||
|
||||
/**
|
||||
* Delete
|
||||
*/
|
||||
deleteDashboard(id: number): cy;
|
||||
deleteDashboardByName(name: string): cy;
|
||||
deleteChartByName(name: string): cy;
|
||||
deleteChart(id: number): cy;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import '@cypress/code-coverage/support';
|
||||
|
||||
const BASE_EXPLORE_URL = '/superset/explore/?form_data=';
|
||||
const TokenName = Cypress.env('TOKEN_NAME');
|
||||
|
||||
/* eslint-disable consistent-return */
|
||||
Cypress.on('uncaught:exception', err => {
|
||||
@@ -102,3 +103,88 @@ Cypress.Commands.add(
|
||||
return cy;
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add('deleteDashboardByName', (name: string) =>
|
||||
cy.getDashboards().then((dashboards: any) => {
|
||||
dashboards?.forEach((element: any) => {
|
||||
if (element.dashboard_title === name) {
|
||||
const elementId = element.id;
|
||||
cy.deleteDashboard(elementId);
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
Cypress.Commands.add('deleteDashboard', (id: number) =>
|
||||
cy
|
||||
.request({
|
||||
method: 'DELETE',
|
||||
url: `api/v1/dashboard/${id}`,
|
||||
headers: {
|
||||
Cookie: `csrf_access_token=${window.localStorage.getItem(
|
||||
'access_token',
|
||||
)}`,
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${TokenName}`,
|
||||
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
|
||||
Referer: `${Cypress.config().baseUrl}/`,
|
||||
},
|
||||
})
|
||||
.then(resp => resp),
|
||||
);
|
||||
|
||||
Cypress.Commands.add('getDashboards', () =>
|
||||
cy
|
||||
.request({
|
||||
method: 'GET',
|
||||
url: `api/v1/dashboard/`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${TokenName}`,
|
||||
},
|
||||
})
|
||||
.then(resp => resp.body.result),
|
||||
);
|
||||
|
||||
Cypress.Commands.add('deleteChart', (id: number) =>
|
||||
cy
|
||||
.request({
|
||||
method: 'DELETE',
|
||||
url: `api/v1/chart/${id}`,
|
||||
headers: {
|
||||
Cookie: `csrf_access_token=${window.localStorage.getItem(
|
||||
'access_token',
|
||||
)}`,
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${TokenName}`,
|
||||
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
|
||||
Referer: `${Cypress.config().baseUrl}/`,
|
||||
},
|
||||
failOnStatusCode: false,
|
||||
})
|
||||
.then(resp => resp),
|
||||
);
|
||||
|
||||
Cypress.Commands.add('getCharts', () =>
|
||||
cy
|
||||
.request({
|
||||
method: 'GET',
|
||||
url: `api/v1/chart/`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${TokenName}`,
|
||||
},
|
||||
})
|
||||
.then(resp => resp.body.result),
|
||||
);
|
||||
|
||||
Cypress.Commands.add('deleteChartByName', (name: string) =>
|
||||
cy.getCharts().then((slices: any) => {
|
||||
slices?.forEach((element: any) => {
|
||||
if (element.slice_name === name) {
|
||||
const elementId = element.id;
|
||||
cy.deleteChart(elementId);
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
19953
superset-frontend/package-lock.json
generated
19953
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superset",
|
||||
"version": "0.0.0dev",
|
||||
"version": "1.5.2",
|
||||
"description": "Superset is a data exploration platform designed to be visual, intuitive, and interactive.",
|
||||
"keywords": [
|
||||
"big",
|
||||
@@ -34,7 +34,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"_lint": "eslint --ignore-path=.eslintignore --ext .js,.jsx,.ts,tsx .",
|
||||
"_prettier": "prettier './({src,spec,cypress-base,plugins,packages}/**/*{.js,.jsx,.ts,.tsx,.css,.less,.scss,.sass}|package.json)'",
|
||||
"_prettier": "prettier './({src,spec,cypress-base,plugins,packages,.storybook}/**/*{.js,.jsx,.ts,.tsx,.css,.less,.scss,.sass}|package.json)'",
|
||||
"build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=production BABEL_ENV=\"${BABEL_ENV:=production}\" webpack --mode=production --color",
|
||||
"build-dev": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=development webpack --mode=development --color",
|
||||
"build-instrumented": "cross-env NODE_ENV=production BABEL_ENV=instrumented webpack --mode=production --color",
|
||||
@@ -61,7 +61,7 @@
|
||||
"prettier-check": "npm run _prettier -- --check",
|
||||
"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 start-storybook -s ./src/assets/images -p 6006",
|
||||
"storybook": "cross-env NODE_ENV=development BABEL_ENV=development start-storybook -p 6006",
|
||||
"tdd": "cross-env NODE_ENV=test jest --watch",
|
||||
"test": "cross-env NODE_ENV=test jest",
|
||||
"type": "tsc --noEmit"
|
||||
@@ -127,8 +127,8 @@
|
||||
"dom-to-image": "^2.6.0",
|
||||
"emotion-rgba": "0.0.9",
|
||||
"fast-glob": "^3.2.7",
|
||||
"fontsource-fira-code": "^3.0.5",
|
||||
"fontsource-inter": "^3.0.5",
|
||||
"fontsource-fira-code": "^4.0.0",
|
||||
"fontsource-inter": "^4.0.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"fuse.js": "^6.4.6",
|
||||
"geolib": "^2.0.24",
|
||||
@@ -197,7 +197,8 @@
|
||||
"rison": "^0.1.1",
|
||||
"scroll-into-view-if-needed": "^2.2.28",
|
||||
"shortid": "^2.2.6",
|
||||
"urijs": "^1.19.6",
|
||||
"tinycolor2": "^1.4.2",
|
||||
"urijs": "^1.19.8",
|
||||
"use-immer": "^0.6.0",
|
||||
"use-query-params": "^1.1.9",
|
||||
"yargs": "^15.4.1"
|
||||
@@ -219,15 +220,15 @@
|
||||
"@emotion/jest": "^11.3.0",
|
||||
"@hot-loader/react-dom": "^16.13.0",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@storybook/addon-actions": "^6.3.12",
|
||||
"@storybook/addon-essentials": "^6.3.12",
|
||||
"@storybook/addon-actions": "^6.4.19",
|
||||
"@storybook/addon-essentials": "^6.4.19",
|
||||
"@storybook/addon-knobs": "^6.3.1",
|
||||
"@storybook/addon-links": "^6.3.12",
|
||||
"@storybook/addons": "^6.3.12",
|
||||
"@storybook/builder-webpack5": "^6.3.12",
|
||||
"@storybook/client-api": "^6.3.12",
|
||||
"@storybook/manager-webpack5": "^6.3.12",
|
||||
"@storybook/react": "^6.3.12",
|
||||
"@storybook/addon-links": "^6.4.19",
|
||||
"@storybook/addons": "^6.4.19",
|
||||
"@storybook/builder-webpack5": "^6.4.19",
|
||||
"@storybook/client-api": "^6.4.19",
|
||||
"@storybook/manager-webpack5": "^6.4.19",
|
||||
"@storybook/react": "^6.4.19",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"@testing-library/dom": "^7.29.4",
|
||||
"@testing-library/jest-dom": "^5.11.6",
|
||||
@@ -262,6 +263,7 @@
|
||||
"@types/rison": "0.0.6",
|
||||
"@types/shortid": "^0.0.29",
|
||||
"@types/sinon": "^9.0.5",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/yargs": "12 - 15",
|
||||
"@typescript-eslint/eslint-plugin": "^5.3.0",
|
||||
"@typescript-eslint/parser": "^5.3.0",
|
||||
@@ -291,6 +293,7 @@
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"eslint-plugin-testing-library": "^3.10.1",
|
||||
"eslint-plugin-theme-colors": "file:tools/eslint-plugin-theme-colors",
|
||||
"exports-loader": "^0.7.0",
|
||||
"fetch-mock": "^7.7.3",
|
||||
"fork-ts-checker-webpack-plugin": "^6.3.3",
|
||||
@@ -303,7 +306,7 @@
|
||||
"jsdom": "^16.4.0",
|
||||
"lerna": "^4.0.0",
|
||||
"less": "^3.12.2",
|
||||
"less-loader": "^5.0.0",
|
||||
"less-loader": "^10.2.0",
|
||||
"mini-css-extract-plugin": "^2.3.0",
|
||||
"mock-socket": "^9.0.3",
|
||||
"node-fetch": "^2.6.1",
|
||||
@@ -323,6 +326,7 @@
|
||||
"transform-loader": "^0.2.4",
|
||||
"ts-loader": "^9.2.5",
|
||||
"typescript": "^4.5.4",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"webpack": "^5.52.1",
|
||||
"webpack-bundle-analyzer": "^4.4.2",
|
||||
"webpack-cli": "^4.8.0",
|
||||
@@ -332,6 +336,12 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.9.1",
|
||||
"npm": "^7.5.4"
|
||||
"npm": "^7.5.4 || ^8.1.2"
|
||||
},
|
||||
"overrides": {
|
||||
"omnibar": {
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,40 +2,41 @@
|
||||
"name": "@superset-ui/generator-superset",
|
||||
"version": "0.18.25",
|
||||
"description": "Scaffolder for Superset",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache-superset/superset-ui/issues"
|
||||
},
|
||||
"homepage": "https://github.com/apache-superset/superset-ui#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/apache-superset/superset-ui.git"
|
||||
},
|
||||
"author": "Superset",
|
||||
"files": [
|
||||
"generators"
|
||||
],
|
||||
"main": "generators/index.js",
|
||||
"keywords": [
|
||||
"yeoman",
|
||||
"generator",
|
||||
"superset",
|
||||
"yeoman-generator"
|
||||
],
|
||||
"devDependencies": {
|
||||
"yeoman-assert": "^3.1.0",
|
||||
"yeoman-test": "^6.2.0",
|
||||
"fs-extra": "^10.0.0"
|
||||
"homepage": "https://github.com/apache-superset/superset-ui#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache-superset/superset-ui/issues"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">= 4.0.0"
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/apache-superset/superset-ui.git"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "Superset",
|
||||
"main": "generators/index.js",
|
||||
"files": [
|
||||
"generators"
|
||||
],
|
||||
"dependencies": {
|
||||
"chalk": "^4.0.0",
|
||||
"lodash": "^4.17.11",
|
||||
"yeoman-generator": "^4.0.0",
|
||||
"yosay": "^2.0.2"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"fs-extra": "^10.0.0",
|
||||
"yeoman-assert": "^3.1.0",
|
||||
"yeoman-environment": "^3.3.0",
|
||||
"yeoman-test": "^6.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">= 4.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
"name": "@superset-ui/chart-controls",
|
||||
"version": "0.18.25",
|
||||
"description": "Superset UI control-utils",
|
||||
"keywords": [
|
||||
"superset"
|
||||
],
|
||||
"homepage": "https://github.com/apache-superset/superset-ui#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache-superset/superset-ui/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/apache-superset/superset-ui.git"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "Superset",
|
||||
"sideEffects": false,
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
@@ -9,22 +22,6 @@
|
||||
"esm",
|
||||
"lib"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/apache-superset/superset-ui.git"
|
||||
},
|
||||
"keywords": [
|
||||
"superset"
|
||||
],
|
||||
"author": "Superset",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache-superset/superset-ui/issues"
|
||||
},
|
||||
"homepage": "https://github.com/apache-superset/superset-ui#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"lodash": "^4.17.15",
|
||||
@@ -33,10 +30,18 @@
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/dom": "^7.29.4",
|
||||
"@testing-library/jest-dom": "^5.11.6",
|
||||
"@testing-library/react": "^11.2.0",
|
||||
"@testing-library/react-hooks": "^5.0.3",
|
||||
"@testing-library/user-event": "^12.7.0",
|
||||
"@types/enzyme": "^3.10.5",
|
||||
"@types/react": "*",
|
||||
"antd": "^4.9.4",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"@types/enzyme": "^3.10.5"
|
||||
"react-dom": "^16.13.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useEffect, useState, ReactNode } from 'react';
|
||||
import React, { useState, ReactNode, useLayoutEffect } from 'react';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { Tooltip } from './Tooltip';
|
||||
import { ColumnTypeLabel } from './ColumnTypeLabel';
|
||||
@@ -47,7 +47,7 @@ export function ColumnOption({
|
||||
const type = hasExpression ? 'expression' : type_generic;
|
||||
const [tooltipText, setTooltipText] = useState<ReactNode>(column.column_name);
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
setTooltipText(getColumnTooltipNode(column, labelRef));
|
||||
}, [labelRef, column]);
|
||||
|
||||
@@ -61,26 +61,12 @@ export function ColumnOption({
|
||||
details={column.certification_details}
|
||||
/>
|
||||
)}
|
||||
<Tooltip
|
||||
id="metric-name-tooltip"
|
||||
title={tooltipText}
|
||||
trigger={['hover']}
|
||||
placement="top"
|
||||
>
|
||||
<Tooltip id="metric-name-tooltip" title={tooltipText}>
|
||||
<span className="m-r-5 option-label column-option-label" ref={labelRef}>
|
||||
{getColumnLabelText(column)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
{column.description && (
|
||||
<InfoTooltipWithTrigger
|
||||
className="m-r-5 text-muted"
|
||||
icon="info"
|
||||
tooltip={column.description}
|
||||
label={`descr-${column.column_name}`}
|
||||
placement="top"
|
||||
/>
|
||||
)}
|
||||
{hasExpression && (
|
||||
<InfoTooltipWithTrigger
|
||||
className="m-r-5 text-muted"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useEffect, useState, ReactNode } from 'react';
|
||||
import React, { useState, ReactNode, useLayoutEffect } from 'react';
|
||||
import { styled, Metric, SafeMarkdown } from '@superset-ui/core';
|
||||
import InfoTooltipWithTrigger from './InfoTooltipWithTrigger';
|
||||
import { ColumnTypeLabel } from './ColumnTypeLabel';
|
||||
@@ -63,7 +63,7 @@ export function MetricOption({
|
||||
|
||||
const [tooltipText, setTooltipText] = useState<ReactNode>(metric.metric_name);
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
setTooltipText(getMetricTooltipNode(metric, labelRef));
|
||||
}, [labelRef, metric]);
|
||||
|
||||
@@ -77,24 +77,11 @@ export function MetricOption({
|
||||
details={metric.certification_details}
|
||||
/>
|
||||
)}
|
||||
<Tooltip
|
||||
id="metric-name-tooltip"
|
||||
title={tooltipText}
|
||||
trigger={['hover']}
|
||||
placement="top"
|
||||
>
|
||||
<Tooltip id="metric-name-tooltip" title={tooltipText}>
|
||||
<span className="option-label metric-option-label" ref={labelRef}>
|
||||
{link}
|
||||
</span>
|
||||
</Tooltip>
|
||||
{metric.description && (
|
||||
<InfoTooltipWithTrigger
|
||||
className="text-muted"
|
||||
icon="info"
|
||||
tooltip={metric.description}
|
||||
label={`descr-${metric.metric_name}`}
|
||||
/>
|
||||
)}
|
||||
{showFormula && (
|
||||
<InfoTooltipWithTrigger
|
||||
className="text-muted"
|
||||
|
||||
@@ -46,9 +46,17 @@ export const Tooltip = ({ overlayStyle, color, ...props }: TooltipProps) => {
|
||||
overlayStyle={{
|
||||
fontSize: theme.typography.sizes.s,
|
||||
lineHeight: '1.6',
|
||||
maxWidth: theme.gridUnit * 62,
|
||||
minWidth: theme.gridUnit * 30,
|
||||
...overlayStyle,
|
||||
}}
|
||||
// make the tooltip display closer to the label
|
||||
align={{ offset: [0, 1] }}
|
||||
color={defaultColor || color}
|
||||
trigger="hover"
|
||||
placement="bottom"
|
||||
// don't allow hovering over the tooltip
|
||||
mouseLeaveDelay={0}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -18,9 +18,41 @@
|
||||
*/
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { t } from '@superset-ui/core';
|
||||
import { css, styled, t } from '@superset-ui/core';
|
||||
import { ColumnMeta, Metric } from '@superset-ui/chart-controls';
|
||||
|
||||
const TooltipSectionWrapper = styled.div`
|
||||
${({ theme }) => css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
line-height: 1.2;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
margin-bottom: ${theme.gridUnit * 2}px;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const TooltipSectionLabel = styled.span`
|
||||
${({ theme }) => css`
|
||||
font-weight: ${theme.typography.weights.bold};
|
||||
`}
|
||||
`;
|
||||
|
||||
const TooltipSection = ({
|
||||
label,
|
||||
text,
|
||||
}: {
|
||||
label: ReactNode;
|
||||
text: ReactNode;
|
||||
}) => (
|
||||
<TooltipSectionWrapper>
|
||||
<TooltipSectionLabel>{label}</TooltipSectionLabel>
|
||||
<span>{text}</span>
|
||||
</TooltipSectionWrapper>
|
||||
);
|
||||
|
||||
export const isLabelTruncated = (labelRef?: React.RefObject<any>): boolean =>
|
||||
!!(
|
||||
labelRef &&
|
||||
@@ -35,22 +67,25 @@ export const getColumnTooltipNode = (
|
||||
column: ColumnMeta,
|
||||
labelRef?: React.RefObject<any>,
|
||||
): ReactNode => {
|
||||
// don't show tooltip if it hasn't verbose_name and hasn't truncated
|
||||
if (!column.verbose_name && !isLabelTruncated(labelRef)) {
|
||||
if (
|
||||
!column.verbose_name &&
|
||||
!column.description &&
|
||||
!isLabelTruncated(labelRef)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (column.verbose_name) {
|
||||
return (
|
||||
<>
|
||||
<div>{t('column name: %s', column.column_name)}</div>
|
||||
<div>{t('verbose name: %s', column.verbose_name)}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// show column name in tooltip when column truncated
|
||||
return t('column name: %s', column.column_name);
|
||||
return (
|
||||
<>
|
||||
<TooltipSection label={t('Column name')} text={column.column_name} />
|
||||
{column.verbose_name && (
|
||||
<TooltipSection label={t('Label')} text={column.verbose_name} />
|
||||
)}
|
||||
{column.description && (
|
||||
<TooltipSection label={t('Description')} text={column.description} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type MetricType = Omit<Metric, 'id'> & { label?: string };
|
||||
@@ -59,23 +94,27 @@ export const getMetricTooltipNode = (
|
||||
metric: MetricType,
|
||||
labelRef?: React.RefObject<any>,
|
||||
): ReactNode => {
|
||||
// don't show tooltip if it hasn't verbose_name, label and hasn't truncated
|
||||
if (!metric.verbose_name && !metric.label && !isLabelTruncated(labelRef)) {
|
||||
if (
|
||||
!metric.verbose_name &&
|
||||
!metric.description &&
|
||||
!metric.label &&
|
||||
!isLabelTruncated(labelRef)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (metric.verbose_name) {
|
||||
return (
|
||||
<>
|
||||
<div>{t('metric name: %s', metric.metric_name)}</div>
|
||||
<div>{t('verbose name: %s', metric.verbose_name)}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLabelTruncated(labelRef) && metric.label) {
|
||||
return t('label name: %s', metric.label);
|
||||
}
|
||||
|
||||
return t('metric name: %s', metric.metric_name);
|
||||
return (
|
||||
<>
|
||||
<TooltipSection label={t('Metric name')} text={metric.metric_name} />
|
||||
{(metric.label || metric.verbose_name) && (
|
||||
<TooltipSection
|
||||
label={t('Label')}
|
||||
text={metric.label || metric.verbose_name}
|
||||
/>
|
||||
)}
|
||||
{metric.description && (
|
||||
<TooltipSection label={t('Description')} text={metric.description} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,16 +21,16 @@ import {
|
||||
getColumnLabel,
|
||||
getMetricLabel,
|
||||
PostProcessingBoxplot,
|
||||
BoxPlotQueryObjectWhiskerType,
|
||||
} from '@superset-ui/core';
|
||||
import { PostProcessingFactory } from './types';
|
||||
|
||||
type BoxPlotQueryObjectWhiskerType =
|
||||
PostProcessingBoxplot['options']['whisker_type'];
|
||||
const PERCENTILE_REGEX = /(\d+)\/(\d+) percentiles/;
|
||||
|
||||
export const boxplotOperator: PostProcessingFactory<
|
||||
PostProcessingBoxplot | undefined
|
||||
> = (formData, queryObject) => {
|
||||
export const boxplotOperator: PostProcessingFactory<PostProcessingBoxplot> = (
|
||||
formData,
|
||||
queryObject,
|
||||
) => {
|
||||
const { groupby, whiskerOptions } = formData;
|
||||
|
||||
if (whiskerOptions) {
|
||||
|
||||
@@ -19,16 +19,15 @@
|
||||
import { PostProcessingContribution } from '@superset-ui/core';
|
||||
import { PostProcessingFactory } from './types';
|
||||
|
||||
export const contributionOperator: PostProcessingFactory<
|
||||
PostProcessingContribution | undefined
|
||||
> = (formData, queryObject) => {
|
||||
if (formData.contributionMode) {
|
||||
return {
|
||||
operation: 'contribution',
|
||||
options: {
|
||||
orientation: formData.contributionMode,
|
||||
},
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
export const contributionOperator: PostProcessingFactory<PostProcessingContribution> =
|
||||
(formData, queryObject) => {
|
||||
if (formData.contributionMode) {
|
||||
return {
|
||||
operation: 'contribution',
|
||||
options: {
|
||||
orientation: formData.contributionMode,
|
||||
},
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/* eslint-disable camelcase */
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitationsxw
|
||||
* under the License.
|
||||
*/
|
||||
import { PostProcessingFlatten } from '@superset-ui/core';
|
||||
import { PostProcessingFactory } from './types';
|
||||
|
||||
export const flattenOperator: PostProcessingFactory<PostProcessingFlatten> = (
|
||||
formData,
|
||||
queryObject,
|
||||
) => ({
|
||||
operation: 'flatten',
|
||||
});
|
||||
@@ -23,7 +23,9 @@ export { timeComparePivotOperator } from './timeComparePivotOperator';
|
||||
export { sortOperator } from './sortOperator';
|
||||
export { pivotOperator } from './pivotOperator';
|
||||
export { resampleOperator } from './resampleOperator';
|
||||
export { renameOperator } from './renameOperator';
|
||||
export { contributionOperator } from './contributionOperator';
|
||||
export { prophetOperator } from './prophetOperator';
|
||||
export { boxplotOperator } from './boxplotOperator';
|
||||
export { flattenOperator } from './flattenOperator';
|
||||
export * from './utils';
|
||||
|
||||
@@ -24,23 +24,20 @@ import {
|
||||
PostProcessingPivot,
|
||||
} from '@superset-ui/core';
|
||||
import { PostProcessingFactory } from './types';
|
||||
import { isValidTimeCompare } from './utils';
|
||||
import { timeComparePivotOperator } from './timeComparePivotOperator';
|
||||
|
||||
export const pivotOperator: PostProcessingFactory<
|
||||
PostProcessingPivot | undefined
|
||||
> = (formData, queryObject) => {
|
||||
export const pivotOperator: PostProcessingFactory<PostProcessingPivot> = (
|
||||
formData,
|
||||
queryObject,
|
||||
) => {
|
||||
const metricLabels = ensureIsArray(queryObject.metrics).map(getMetricLabel);
|
||||
const { x_axis: xAxis } = formData;
|
||||
if ((xAxis || queryObject.is_timeseries) && metricLabels.length) {
|
||||
if (isValidTimeCompare(formData, queryObject)) {
|
||||
return timeComparePivotOperator(formData, queryObject);
|
||||
}
|
||||
|
||||
if ((xAxis || queryObject.is_timeseries) && metricLabels.length) {
|
||||
const index = [getColumnLabel(xAxis || DTTM_ALIAS)];
|
||||
return {
|
||||
operation: 'pivot',
|
||||
options: {
|
||||
index: [xAxis || DTTM_ALIAS],
|
||||
index,
|
||||
columns: ensureIsArray(queryObject.columns).map(getColumnLabel),
|
||||
// Create 'dummy' mean aggregates to assign cell values in pivot table
|
||||
// use the 'mean' aggregates to avoid drop NaN. PR: https://github.com/apache-superset/superset-ui/pull/1231
|
||||
@@ -48,6 +45,8 @@ export const pivotOperator: PostProcessingFactory<
|
||||
metricLabels.map(metric => [metric, { operator: 'mean' }]),
|
||||
),
|
||||
drop_missing_columns: false,
|
||||
flatten_columns: false,
|
||||
reset_index: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,12 +16,18 @@
|
||||
* specific language governing permissions and limitationsxw
|
||||
* under the License.
|
||||
*/
|
||||
import { DTTM_ALIAS, PostProcessingProphet } from '@superset-ui/core';
|
||||
import {
|
||||
DTTM_ALIAS,
|
||||
getColumnLabel,
|
||||
PostProcessingProphet,
|
||||
} from '@superset-ui/core';
|
||||
import { PostProcessingFactory } from './types';
|
||||
|
||||
export const prophetOperator: PostProcessingFactory<
|
||||
PostProcessingProphet | undefined
|
||||
> = (formData, queryObject) => {
|
||||
export const prophetOperator: PostProcessingFactory<PostProcessingProphet> = (
|
||||
formData,
|
||||
queryObject,
|
||||
) => {
|
||||
const index = getColumnLabel(formData.x_axis || DTTM_ALIAS);
|
||||
if (formData.forecastEnabled) {
|
||||
return {
|
||||
operation: 'prophet',
|
||||
@@ -32,7 +38,7 @@ export const prophetOperator: PostProcessingFactory<
|
||||
yearly_seasonality: formData.forecastSeasonalityYearly,
|
||||
weekly_seasonality: formData.forecastSeasonalityWeekly,
|
||||
daily_seasonality: formData.forecastSeasonalityDaily,
|
||||
index: formData.x_axis || DTTM_ALIAS,
|
||||
index,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/* eslint-disable camelcase */
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitationsxw
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
PostProcessingRename,
|
||||
ensureIsArray,
|
||||
getMetricLabel,
|
||||
ComparisionType,
|
||||
} from '@superset-ui/core';
|
||||
import { PostProcessingFactory } from './types';
|
||||
import { getMetricOffsetsMap, isValidTimeCompare } from './utils';
|
||||
|
||||
export const renameOperator: PostProcessingFactory<PostProcessingRename> = (
|
||||
formData,
|
||||
queryObject,
|
||||
) => {
|
||||
const metrics = ensureIsArray(queryObject.metrics);
|
||||
const columns = ensureIsArray(queryObject.columns);
|
||||
const { x_axis: xAxis } = formData;
|
||||
// remove or rename top level of column name(metric name) in the MultiIndex when
|
||||
// 1) only 1 metric
|
||||
// 2) exist dimentsion
|
||||
// 3) exist xAxis
|
||||
// 4) exist time comparison, and comparison type is "actual values"
|
||||
if (
|
||||
metrics.length === 1 &&
|
||||
columns.length > 0 &&
|
||||
(xAxis || queryObject.is_timeseries) &&
|
||||
!(
|
||||
// todo: we should provide an approach to handle derived metrics
|
||||
(
|
||||
isValidTimeCompare(formData, queryObject) &&
|
||||
[
|
||||
ComparisionType.Difference,
|
||||
ComparisionType.Ratio,
|
||||
ComparisionType.Percentage,
|
||||
].includes(formData.comparison_type)
|
||||
)
|
||||
)
|
||||
) {
|
||||
const renamePairs: [string, string | null][] = [];
|
||||
|
||||
if (
|
||||
// "actual values" will add derived metric.
|
||||
// we will rename the "metric" from the metricWithOffset label
|
||||
// for example: "count__1 year ago" => "1 year ago"
|
||||
isValidTimeCompare(formData, queryObject) &&
|
||||
formData.comparison_type === ComparisionType.Values
|
||||
) {
|
||||
const metricOffsetMap = getMetricOffsetsMap(formData, queryObject);
|
||||
const timeOffsets = ensureIsArray(formData.time_compare);
|
||||
[...metricOffsetMap.keys()].forEach(metricWithOffset => {
|
||||
const offsetLabel = timeOffsets.find(offset =>
|
||||
metricWithOffset.includes(offset),
|
||||
);
|
||||
renamePairs.push([metricWithOffset, offsetLabel]);
|
||||
});
|
||||
}
|
||||
|
||||
renamePairs.push([getMetricLabel(metrics[0]), null]);
|
||||
|
||||
return {
|
||||
operation: 'rename',
|
||||
options: {
|
||||
columns: Object.fromEntries(renamePairs),
|
||||
level: 0,
|
||||
inplace: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@@ -17,36 +17,23 @@
|
||||
* specific language governing permissions and limitationsxw
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
DTTM_ALIAS,
|
||||
ensureIsArray,
|
||||
isPhysicalColumn,
|
||||
PostProcessingResample,
|
||||
} from '@superset-ui/core';
|
||||
import { PostProcessingResample } from '@superset-ui/core';
|
||||
import { PostProcessingFactory } from './types';
|
||||
|
||||
export const resampleOperator: PostProcessingFactory<
|
||||
PostProcessingResample | undefined
|
||||
> = (formData, queryObject) => {
|
||||
export const resampleOperator: PostProcessingFactory<PostProcessingResample> = (
|
||||
formData,
|
||||
queryObject,
|
||||
) => {
|
||||
const resampleZeroFill = formData.resample_method === 'zerofill';
|
||||
const resampleMethod = resampleZeroFill ? 'asfreq' : formData.resample_method;
|
||||
const resampleRule = formData.resample_rule;
|
||||
if (resampleMethod && resampleRule) {
|
||||
const groupby_columns = ensureIsArray(queryObject.columns).map(column => {
|
||||
if (isPhysicalColumn(column)) {
|
||||
return column;
|
||||
}
|
||||
return column.label;
|
||||
});
|
||||
|
||||
return {
|
||||
operation: 'resample',
|
||||
options: {
|
||||
method: resampleMethod,
|
||||
rule: resampleRule,
|
||||
fill_value: resampleZeroFill ? 0 : null,
|
||||
time_column: formData.x_axis || DTTM_ALIAS,
|
||||
groupby_columns,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,39 +18,25 @@
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
ComparisionType,
|
||||
ensureIsArray,
|
||||
ensureIsInt,
|
||||
PostProcessingCum,
|
||||
PostProcessingRolling,
|
||||
RollingType,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
getMetricOffsetsMap,
|
||||
isValidTimeCompare,
|
||||
TIME_COMPARISON_SEPARATOR,
|
||||
} from './utils';
|
||||
import { getMetricOffsetsMap, isValidTimeCompare } from './utils';
|
||||
import { PostProcessingFactory } from './types';
|
||||
|
||||
export const rollingWindowOperator: PostProcessingFactory<
|
||||
PostProcessingRolling | PostProcessingCum | undefined
|
||||
PostProcessingRolling | PostProcessingCum
|
||||
> = (formData, queryObject) => {
|
||||
let columns: (string | undefined)[];
|
||||
if (isValidTimeCompare(formData, queryObject)) {
|
||||
const metricsMap = getMetricOffsetsMap(formData, queryObject);
|
||||
const comparisonType = formData.comparison_type;
|
||||
if (comparisonType === ComparisionType.Values) {
|
||||
// time compare type: actual values
|
||||
columns = [
|
||||
...Array.from(metricsMap.values()),
|
||||
...Array.from(metricsMap.keys()),
|
||||
];
|
||||
} else {
|
||||
// time compare type: difference / percentage / ratio
|
||||
columns = Array.from(metricsMap.entries()).map(([offset, metric]) =>
|
||||
[comparisonType, metric, offset].join(TIME_COMPARISON_SEPARATOR),
|
||||
);
|
||||
}
|
||||
columns = [
|
||||
...Array.from(metricsMap.values()),
|
||||
...Array.from(metricsMap.keys()),
|
||||
];
|
||||
} else {
|
||||
columns = ensureIsArray(queryObject.metrics).map(metric => {
|
||||
if (typeof metric === 'string') {
|
||||
@@ -67,7 +53,6 @@ export const rollingWindowOperator: PostProcessingFactory<
|
||||
options: {
|
||||
operator: 'sum',
|
||||
columns: columnsMap,
|
||||
is_pivot_df: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -84,7 +69,6 @@ export const rollingWindowOperator: PostProcessingFactory<
|
||||
window: ensureIsInt(formData.rolling_periods, 1),
|
||||
min_periods: ensureIsInt(formData.min_periods, 0),
|
||||
columns: columnsMap,
|
||||
is_pivot_df: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@
|
||||
import { DTTM_ALIAS, PostProcessingSort, RollingType } from '@superset-ui/core';
|
||||
import { PostProcessingFactory } from './types';
|
||||
|
||||
export const sortOperator: PostProcessingFactory<
|
||||
PostProcessingSort | undefined
|
||||
> = (formData, queryObject) => {
|
||||
export const sortOperator: PostProcessingFactory<PostProcessingSort> = (
|
||||
formData,
|
||||
queryObject,
|
||||
) => {
|
||||
const { x_axis: xAxis } = formData;
|
||||
if (
|
||||
(xAxis || queryObject.is_timeseries) &&
|
||||
|
||||
@@ -21,26 +21,25 @@ import { ComparisionType, PostProcessingCompare } from '@superset-ui/core';
|
||||
import { getMetricOffsetsMap, isValidTimeCompare } from './utils';
|
||||
import { PostProcessingFactory } from './types';
|
||||
|
||||
export const timeCompareOperator: PostProcessingFactory<
|
||||
PostProcessingCompare | undefined
|
||||
> = (formData, queryObject) => {
|
||||
const comparisonType = formData.comparison_type;
|
||||
const metricOffsetMap = getMetricOffsetsMap(formData, queryObject);
|
||||
export const timeCompareOperator: PostProcessingFactory<PostProcessingCompare> =
|
||||
(formData, queryObject) => {
|
||||
const comparisonType = formData.comparison_type;
|
||||
const metricOffsetMap = getMetricOffsetsMap(formData, queryObject);
|
||||
|
||||
if (
|
||||
isValidTimeCompare(formData, queryObject) &&
|
||||
comparisonType !== ComparisionType.Values
|
||||
) {
|
||||
return {
|
||||
operation: 'compare',
|
||||
options: {
|
||||
source_columns: Array.from(metricOffsetMap.values()),
|
||||
compare_columns: Array.from(metricOffsetMap.keys()),
|
||||
compare_type: comparisonType,
|
||||
drop_original_columns: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (
|
||||
isValidTimeCompare(formData, queryObject) &&
|
||||
comparisonType !== ComparisionType.Values
|
||||
) {
|
||||
return {
|
||||
operation: 'compare',
|
||||
options: {
|
||||
source_columns: Array.from(metricOffsetMap.values()),
|
||||
compare_columns: Array.from(metricOffsetMap.keys()),
|
||||
compare_type: comparisonType,
|
||||
drop_original_columns: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -18,54 +18,41 @@
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
ComparisionType,
|
||||
DTTM_ALIAS,
|
||||
ensureIsArray,
|
||||
getColumnLabel,
|
||||
NumpyFunction,
|
||||
PostProcessingPivot,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
getMetricOffsetsMap,
|
||||
isValidTimeCompare,
|
||||
TIME_COMPARISON_SEPARATOR,
|
||||
} from './utils';
|
||||
import { getMetricOffsetsMap, isValidTimeCompare } from './utils';
|
||||
import { PostProcessingFactory } from './types';
|
||||
|
||||
export const timeComparePivotOperator: PostProcessingFactory<
|
||||
PostProcessingPivot | undefined
|
||||
> = (formData, queryObject) => {
|
||||
const comparisonType = formData.comparison_type;
|
||||
const metricOffsetMap = getMetricOffsetsMap(formData, queryObject);
|
||||
export const timeComparePivotOperator: PostProcessingFactory<PostProcessingPivot> =
|
||||
(formData, queryObject) => {
|
||||
const metricOffsetMap = getMetricOffsetsMap(formData, queryObject);
|
||||
|
||||
if (isValidTimeCompare(formData, queryObject)) {
|
||||
const valuesAgg = Object.fromEntries(
|
||||
[...metricOffsetMap.values(), ...metricOffsetMap.keys()].map(metric => [
|
||||
metric,
|
||||
// use the 'mean' aggregates to avoid drop NaN
|
||||
{ operator: 'mean' as NumpyFunction },
|
||||
]),
|
||||
);
|
||||
const changeAgg = Object.fromEntries(
|
||||
[...metricOffsetMap.entries()]
|
||||
.map(([offset, metric]) =>
|
||||
[comparisonType, metric, offset].join(TIME_COMPARISON_SEPARATOR),
|
||||
)
|
||||
// use the 'mean' aggregates to avoid drop NaN
|
||||
.map(metric => [metric, { operator: 'mean' as NumpyFunction }]),
|
||||
);
|
||||
if (isValidTimeCompare(formData, queryObject)) {
|
||||
const aggregates = Object.fromEntries(
|
||||
[...metricOffsetMap.values(), ...metricOffsetMap.keys()].map(metric => [
|
||||
metric,
|
||||
// use the 'mean' aggregates to avoid drop NaN
|
||||
{ operator: 'mean' as NumpyFunction },
|
||||
]),
|
||||
);
|
||||
const index = [getColumnLabel(formData.x_axis || DTTM_ALIAS)];
|
||||
|
||||
return {
|
||||
operation: 'pivot',
|
||||
options: {
|
||||
index: [formData.x_axis || DTTM_ALIAS],
|
||||
columns: ensureIsArray(queryObject.columns).map(getColumnLabel),
|
||||
aggregates:
|
||||
comparisonType === ComparisionType.Values ? valuesAgg : changeAgg,
|
||||
drop_missing_columns: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
operation: 'pivot',
|
||||
options: {
|
||||
index,
|
||||
columns: ensureIsArray(queryObject.columns).map(getColumnLabel),
|
||||
drop_missing_columns: false,
|
||||
flatten_columns: false,
|
||||
reset_index: false,
|
||||
aggregates,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -170,6 +170,7 @@ export const advancedAnalyticsControls: ControlPanelSectionConfig = {
|
||||
choices: [
|
||||
['asfreq', 'Null imputation'],
|
||||
['zerofill', 'Zero imputation'],
|
||||
['linear', 'Linear interpolation'],
|
||||
['ffill', 'Forward values'],
|
||||
['bfill', 'Backward values'],
|
||||
['median', 'Median values'],
|
||||
|
||||
@@ -48,8 +48,10 @@ export default React.memo(function ColumnConfigItem({
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
padding: `${1.5 * gridUnit}px ${2 * gridUnit}px`,
|
||||
padding: `${gridUnit}px ${2 * gridUnit}px`,
|
||||
borderBottom: `1px solid ${colors.grayscale.light2}`,
|
||||
position: 'relative',
|
||||
paddingRight: caretWidth,
|
||||
|
||||
@@ -80,7 +80,7 @@ export const dndEntity: typeof dndGroupByControl = {
|
||||
export const dnd_adhoc_filters: SharedControlConfig<'DndFilterSelect'> = {
|
||||
type: 'DndFilterSelect',
|
||||
label: t('Filters'),
|
||||
default: null,
|
||||
default: [],
|
||||
description: '',
|
||||
mapStateToProps: ({ datasource, form_data }) => ({
|
||||
columns: datasource?.columns.filter(c => c.filterable) || [],
|
||||
|
||||
@@ -43,6 +43,8 @@ import {
|
||||
SequentialScheme,
|
||||
legacyValidateInteger,
|
||||
validateNonEmpty,
|
||||
JsonArray,
|
||||
ComparisionType,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
import {
|
||||
@@ -89,11 +91,21 @@ export const PRIMARY_COLOR = { r: 0, g: 122, b: 135, a: 1 };
|
||||
const ROW_LIMIT_OPTIONS = [10, 50, 100, 250, 500, 1000, 5000, 10000, 50000];
|
||||
const SERIES_LIMITS = [5, 10, 25, 50, 100, 500];
|
||||
|
||||
const appContainer = document.getElementById('app');
|
||||
const { user } = JSON.parse(
|
||||
appContainer?.getAttribute('data-bootstrap') || '{}',
|
||||
);
|
||||
|
||||
type Control = {
|
||||
savedMetrics?: Metric[] | null;
|
||||
default?: unknown;
|
||||
};
|
||||
|
||||
type SelectDefaultOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const groupByControl: SharedControlConfig<'SelectControl', ColumnMeta> = {
|
||||
type: 'SelectControl',
|
||||
label: t('Group by'),
|
||||
@@ -106,8 +118,6 @@ const groupByControl: SharedControlConfig<'SelectControl', ColumnMeta> = {
|
||||
'One or many columns to group by. High cardinality groupings should include a sort by metric ' +
|
||||
'and series limit to limit the number of fetched and rendered series.',
|
||||
),
|
||||
sortComparator: (a: { label: string }, b: { label: string }) =>
|
||||
a.label.localeCompare(b.label),
|
||||
optionRenderer: c => <ColumnOption showType column={c} />,
|
||||
valueRenderer: c => <ColumnOption column={c} />,
|
||||
valueKey: 'column_name',
|
||||
@@ -162,6 +172,7 @@ const datasourceControl: SharedControlConfig<'DatasourceControl'> = {
|
||||
mapStateToProps: ({ datasource, form_data }) => ({
|
||||
datasource,
|
||||
form_data,
|
||||
user,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -201,6 +212,9 @@ const linear_color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
|
||||
renderTrigger: true,
|
||||
schemes: () => sequentialSchemeRegistry.getMap(),
|
||||
isLinear: true,
|
||||
mapStateToProps: state => ({
|
||||
dashboardId: state?.form_data?.dashboardId,
|
||||
}),
|
||||
};
|
||||
|
||||
const secondary_metric: SharedControlConfig<'MetricsControl'> = {
|
||||
@@ -337,6 +351,18 @@ const row_limit: SharedControlConfig<'SelectControl'> = {
|
||||
description: t('Limits the number of rows that get displayed.'),
|
||||
};
|
||||
|
||||
const order_desc: SharedControlConfig<'CheckboxControl'> = {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Sort Descending'),
|
||||
default: true,
|
||||
description: t('Whether to sort descending or ascending'),
|
||||
visibility: ({ controls }) =>
|
||||
Boolean(
|
||||
controls?.timeseries_limit_metric.value &&
|
||||
(controls?.timeseries_limit_metric.value as JsonArray).length,
|
||||
),
|
||||
};
|
||||
|
||||
const limit: SharedControlConfig<'SelectControl'> = {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
@@ -424,29 +450,33 @@ const size: SharedControlConfig<'MetricsControl'> = {
|
||||
default: null,
|
||||
};
|
||||
|
||||
const y_axis_format: SharedControlConfig<'SelectControl'> = {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Y Axis Format'),
|
||||
renderTrigger: true,
|
||||
default: DEFAULT_NUMBER_FORMAT,
|
||||
choices: D3_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
mapStateToProps: state => {
|
||||
const showWarning = state.controls?.comparison_type?.value === 'percentage';
|
||||
return {
|
||||
warning: showWarning
|
||||
? t(
|
||||
'When `Calculation type` is set to "Percentage change", the Y ' +
|
||||
'Axis Format is forced to `.1%`',
|
||||
)
|
||||
: null,
|
||||
disabled: showWarning,
|
||||
};
|
||||
},
|
||||
};
|
||||
const y_axis_format: SharedControlConfig<'SelectControl', SelectDefaultOption> =
|
||||
{
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Y Axis Format'),
|
||||
renderTrigger: true,
|
||||
default: DEFAULT_NUMBER_FORMAT,
|
||||
choices: D3_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
tokenSeparators: ['\n', '\t', ';'],
|
||||
filterOption: ({ data: option }, search) =>
|
||||
option.label.includes(search) || option.value.includes(search),
|
||||
mapStateToProps: state => {
|
||||
const isPercentage =
|
||||
state.controls?.comparison_type?.value === ComparisionType.Percentage;
|
||||
return {
|
||||
choices: isPercentage
|
||||
? D3_FORMAT_OPTIONS.filter(option => option[0].includes('%'))
|
||||
: D3_FORMAT_OPTIONS,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const x_axis_time_format: SharedControlConfig<'SelectControl'> = {
|
||||
const x_axis_time_format: SharedControlConfig<
|
||||
'SelectControl',
|
||||
SelectDefaultOption
|
||||
> = {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Time format'),
|
||||
@@ -454,12 +484,14 @@ const x_axis_time_format: SharedControlConfig<'SelectControl'> = {
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
description: D3_TIME_FORMAT_DOCS,
|
||||
filterOption: ({ data: option }, search) =>
|
||||
option.label.includes(search) || option.value.includes(search),
|
||||
};
|
||||
|
||||
const adhoc_filters: SharedControlConfig<'AdhocFilterControl'> = {
|
||||
type: 'AdhocFilterControl',
|
||||
label: t('Filters'),
|
||||
default: null,
|
||||
default: [],
|
||||
description: '',
|
||||
mapStateToProps: ({ datasource, form_data }) => ({
|
||||
columns: datasource?.columns.filter(c => c.filterable) || [],
|
||||
@@ -508,6 +540,7 @@ const sharedControls = {
|
||||
limit,
|
||||
timeseries_limit_metric: enableExploreDnd ? dnd_sort_by : sort_by,
|
||||
orderby: enableExploreDnd ? dnd_sort_by : sort_by,
|
||||
order_desc,
|
||||
series: enableExploreDnd ? dndSeries : series,
|
||||
entity: enableExploreDnd ? dndEntity : entity,
|
||||
x: enableExploreDnd ? dnd_x : x,
|
||||
|
||||
@@ -171,6 +171,8 @@ export type TabOverride = 'data' | 'customize' | boolean;
|
||||
* bubbled up to the control header, section header and query panel header.
|
||||
* - warning: text shown as a tooltip on a warning icon in the control's header
|
||||
* - error: text shown as a tooltip on a error icon in the control's header
|
||||
* - shouldMapStateToProps: a function that receives the previous and current app state
|
||||
* and determines if the control needs to recalculate it's props based on the new state.
|
||||
* - mapStateToProps: a function that receives the App's state and return an object of k/v
|
||||
* to overwrite configuration at runtime. This is useful to alter a component based on
|
||||
* anything external to it, like another control's value. For instance it's possible to
|
||||
@@ -198,6 +200,13 @@ export interface BaseControlConfig<
|
||||
/**
|
||||
* Add additional props to chart control.
|
||||
*/
|
||||
shouldMapStateToProps?: (
|
||||
prevState: ControlPanelState,
|
||||
state: ControlPanelState,
|
||||
controlState: ControlState,
|
||||
// TODO: add strict `chartState` typing (see superset-frontend/src/explore/types)
|
||||
chartState?: AnyDict,
|
||||
) => boolean;
|
||||
mapStateToProps?: (
|
||||
state: ControlPanelState,
|
||||
controlState: ControlState,
|
||||
|
||||
@@ -25,7 +25,7 @@ export const D3_FORMAT_DOCS = t(
|
||||
|
||||
// input choices & options
|
||||
export const D3_FORMAT_OPTIONS: [string, string][] = [
|
||||
[NumberFormats.SMART_NUMBER, t('Adaptative formating')],
|
||||
[NumberFormats.SMART_NUMBER, t('Adaptive formatting')],
|
||||
['~g', t('Original value')],
|
||||
[',d', ',d (12345.432 => 12,345)'],
|
||||
['.1s', '.1s (12345.432 => 10k)'],
|
||||
@@ -34,6 +34,8 @@ export const D3_FORMAT_OPTIONS: [string, string][] = [
|
||||
['.2%', '.2% (12345.432 => 1234543.20%)'],
|
||||
['.3%', '.3% (12345.432 => 1234543.200%)'],
|
||||
['.4r', '.4r (12345.432 => 12350)'],
|
||||
[',.1f', ',.1f (12345.432 => 12,345.4)'],
|
||||
[',.2f', ',.2f (12345.432 => 12,345.43)'],
|
||||
[',.3f', ',.3f (12345.432 => 12,345.432)'],
|
||||
['+,', '+, (12345.432 => +12,345.432)'],
|
||||
['$,.2f', '$,.2f (12345.432 => $12,345.43)'],
|
||||
@@ -46,7 +48,7 @@ export const D3_TIME_FORMAT_DOCS = t(
|
||||
);
|
||||
|
||||
export const D3_TIME_FORMAT_OPTIONS: [string, string][] = [
|
||||
[smartDateFormatter.id, t('Adaptative formating')],
|
||||
[smartDateFormatter.id, t('Adaptive formatting')],
|
||||
['%d/%m/%Y', '%d/%m/%Y | 14/01/2019'],
|
||||
['%m/%d/%Y', '%m/%d/%Y | 01/14/2019'],
|
||||
['%Y-%m-%d', '%Y-%m-%d | 2019-01-14'],
|
||||
|
||||
@@ -53,12 +53,7 @@ describe('ColumnOption', () => {
|
||||
expect(lbl).toHaveLength(1);
|
||||
expect(lbl.first().text()).toBe('Foo');
|
||||
});
|
||||
it('shows 2 InfoTooltipWithTrigger', () => {
|
||||
expect(wrapper.find(InfoTooltipWithTrigger)).toHaveLength(2);
|
||||
});
|
||||
it('shows only 1 InfoTooltipWithTrigger when no descr', () => {
|
||||
delete props.column.description;
|
||||
wrapper = shallow(factory(props));
|
||||
it('shows 1 InfoTooltipWithTrigger', () => {
|
||||
expect(wrapper.find(InfoTooltipWithTrigger)).toHaveLength(1);
|
||||
});
|
||||
it('shows a label with column_name when no verbose_name', () => {
|
||||
|
||||
@@ -51,12 +51,7 @@ describe('MetricOption', () => {
|
||||
expect(lbl).toHaveLength(1);
|
||||
expect(lbl.first().text()).toBe('Foo');
|
||||
});
|
||||
it('shows 3 InfoTooltipWithTrigger', () => {
|
||||
expect(wrapper.find('InfoTooltipWithTrigger')).toHaveLength(3);
|
||||
});
|
||||
it('shows only 2 InfoTooltipWithTrigger when no descr', () => {
|
||||
props.metric.description = '';
|
||||
wrapper = shallow(factory(props));
|
||||
it('shows 2 InfoTooltipWithTrigger', () => {
|
||||
expect(wrapper.find('InfoTooltipWithTrigger')).toHaveLength(2);
|
||||
});
|
||||
it('shows a label with metric_name when no verbose_name', () => {
|
||||
@@ -64,7 +59,7 @@ describe('MetricOption', () => {
|
||||
wrapper = shallow(factory(props));
|
||||
expect(wrapper.find('.option-label').first().text()).toBe('foo');
|
||||
});
|
||||
it('shows only 1 InfoTooltipWithTrigger when no descr and no warning', () => {
|
||||
it('shows only 1 InfoTooltipWithTrigger when no warning', () => {
|
||||
props.metric.warning_text = '';
|
||||
wrapper = shallow(factory(props));
|
||||
expect(wrapper.find('InfoTooltipWithTrigger')).toHaveLength(1);
|
||||
|
||||
@@ -16,14 +16,19 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
import React, { ReactElement } from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { ThemeProvider, supersetTheme } from '@superset-ui/core';
|
||||
import {
|
||||
getColumnLabelText,
|
||||
getColumnTooltipNode,
|
||||
getMetricTooltipNode,
|
||||
} from '../../src/components/labelUtils';
|
||||
|
||||
const renderWithTheme = (ui: ReactElement) =>
|
||||
render(<ThemeProvider theme={supersetTheme}>{ui}</ThemeProvider>);
|
||||
|
||||
test("should get column name when column doesn't have verbose_name", () => {
|
||||
expect(
|
||||
getColumnLabelText({
|
||||
@@ -52,66 +57,80 @@ test('should get null as tooltip', () => {
|
||||
id: 123,
|
||||
column_name: 'column name',
|
||||
verbose_name: '',
|
||||
description: '',
|
||||
},
|
||||
ref,
|
||||
),
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
test('should get column name and verbose name when it has a verbose name', () => {
|
||||
const rvNode = (
|
||||
test('should get column name, verbose name and description when it has a verbose name', () => {
|
||||
const ref = { current: { scrollWidth: 100, clientWidth: 100 } };
|
||||
renderWithTheme(
|
||||
<>
|
||||
<div>column name: column name</div>
|
||||
<div>verbose name: verbose name</div>
|
||||
</>
|
||||
{getColumnTooltipNode(
|
||||
{
|
||||
id: 123,
|
||||
column_name: 'column name',
|
||||
verbose_name: 'verbose name',
|
||||
description: 'A very important column',
|
||||
},
|
||||
ref,
|
||||
)}
|
||||
</>,
|
||||
);
|
||||
|
||||
const ref = { current: { scrollWidth: 100, clientWidth: 100 } };
|
||||
expect(
|
||||
getColumnTooltipNode(
|
||||
{
|
||||
id: 123,
|
||||
column_name: 'column name',
|
||||
verbose_name: 'verbose name',
|
||||
},
|
||||
ref,
|
||||
),
|
||||
).toStrictEqual(rvNode);
|
||||
expect(screen.getByText('Column name')).toBeVisible();
|
||||
expect(screen.getByText('column name')).toBeVisible();
|
||||
expect(screen.getByText('Label')).toBeVisible();
|
||||
expect(screen.getByText('verbose name')).toBeVisible();
|
||||
expect(screen.getByText('Description')).toBeVisible();
|
||||
expect(screen.getByText('A very important column')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should get column name as tooltip if it overflowed', () => {
|
||||
const ref = { current: { scrollWidth: 200, clientWidth: 100 } };
|
||||
expect(
|
||||
getColumnTooltipNode(
|
||||
{
|
||||
id: 123,
|
||||
column_name: 'long long long long column name',
|
||||
verbose_name: '',
|
||||
},
|
||||
ref,
|
||||
),
|
||||
).toBe('column name: long long long long column name');
|
||||
renderWithTheme(
|
||||
<>
|
||||
{getColumnTooltipNode(
|
||||
{
|
||||
id: 123,
|
||||
column_name: 'long long long long column name',
|
||||
verbose_name: '',
|
||||
description: '',
|
||||
},
|
||||
ref,
|
||||
)}
|
||||
</>,
|
||||
);
|
||||
expect(screen.getByText('Column name')).toBeVisible();
|
||||
expect(screen.getByText('long long long long column name')).toBeVisible();
|
||||
expect(screen.queryByText('Label')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Description')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should get column name and verbose name as tooltip if it overflowed', () => {
|
||||
const rvNode = (
|
||||
test('should get column name, verbose name and description as tooltip if it overflowed', () => {
|
||||
const ref = { current: { scrollWidth: 200, clientWidth: 100 } };
|
||||
renderWithTheme(
|
||||
<>
|
||||
<div>column name: long long long long column name</div>
|
||||
<div>verbose name: long long long long verbose name</div>
|
||||
</>
|
||||
{getColumnTooltipNode(
|
||||
{
|
||||
id: 123,
|
||||
column_name: 'long long long long column name',
|
||||
verbose_name: 'long long long long verbose name',
|
||||
description: 'A very important column',
|
||||
},
|
||||
ref,
|
||||
)}
|
||||
</>,
|
||||
);
|
||||
|
||||
const ref = { current: { scrollWidth: 200, clientWidth: 100 } };
|
||||
expect(
|
||||
getColumnTooltipNode(
|
||||
{
|
||||
id: 123,
|
||||
column_name: 'long long long long column name',
|
||||
verbose_name: 'long long long long verbose name',
|
||||
},
|
||||
ref,
|
||||
),
|
||||
).toStrictEqual(rvNode);
|
||||
expect(screen.getByText('Column name')).toBeVisible();
|
||||
expect(screen.getByText('long long long long column name')).toBeVisible();
|
||||
expect(screen.getByText('Label')).toBeVisible();
|
||||
expect(screen.getByText('long long long long verbose name')).toBeVisible();
|
||||
expect(screen.getByText('Description')).toBeVisible();
|
||||
expect(screen.getByText('A very important column')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should get null as tooltip in metric', () => {
|
||||
@@ -122,64 +141,76 @@ test('should get null as tooltip in metric', () => {
|
||||
metric_name: 'count',
|
||||
label: '',
|
||||
verbose_name: '',
|
||||
description: '',
|
||||
},
|
||||
ref,
|
||||
),
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
test('should get metric name and verbose name as tooltip in metric', () => {
|
||||
const rvNode = (
|
||||
<>
|
||||
<div>metric name: count</div>
|
||||
<div>verbose name: count(*)</div>
|
||||
</>
|
||||
);
|
||||
|
||||
test('should get metric name, verbose name and description as tooltip in metric', () => {
|
||||
const ref = { current: { scrollWidth: 100, clientWidth: 100 } };
|
||||
expect(
|
||||
getMetricTooltipNode(
|
||||
{
|
||||
metric_name: 'count',
|
||||
label: 'count(*)',
|
||||
verbose_name: 'count(*)',
|
||||
},
|
||||
ref,
|
||||
),
|
||||
).toStrictEqual(rvNode);
|
||||
});
|
||||
|
||||
test('should get metric name and verbose name in tooltip if it overflowed', () => {
|
||||
const rvNode = (
|
||||
renderWithTheme(
|
||||
<>
|
||||
<div>metric name: count</div>
|
||||
<div>verbose name: longlonglonglonglong verbose metric</div>
|
||||
</>
|
||||
{getMetricTooltipNode(
|
||||
{
|
||||
metric_name: 'count',
|
||||
label: 'count(*)',
|
||||
verbose_name: 'count(*)',
|
||||
description: 'Count metric',
|
||||
},
|
||||
ref,
|
||||
)}
|
||||
</>,
|
||||
);
|
||||
|
||||
const ref = { current: { scrollWidth: 200, clientWidth: 100 } };
|
||||
expect(
|
||||
getMetricTooltipNode(
|
||||
{
|
||||
metric_name: 'count',
|
||||
label: '',
|
||||
verbose_name: 'longlonglonglonglong verbose metric',
|
||||
},
|
||||
ref,
|
||||
),
|
||||
).toStrictEqual(rvNode);
|
||||
expect(screen.getByText('Metric name')).toBeVisible();
|
||||
expect(screen.getByText('count')).toBeVisible();
|
||||
expect(screen.getByText('Label')).toBeVisible();
|
||||
expect(screen.getByText('count(*)')).toBeVisible();
|
||||
expect(screen.getByText('Description')).toBeVisible();
|
||||
expect(screen.getByText('Count metric')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should get label name as tooltip in metric if it overflowed', () => {
|
||||
test('should get metric name as tooltip if it overflowed', () => {
|
||||
const ref = { current: { scrollWidth: 200, clientWidth: 100 } };
|
||||
expect(
|
||||
getMetricTooltipNode(
|
||||
{
|
||||
metric_name: 'count',
|
||||
label: 'longlonglonglonglong metric label',
|
||||
verbose_name: '',
|
||||
},
|
||||
ref,
|
||||
),
|
||||
).toBe('label name: longlonglonglonglong metric label');
|
||||
renderWithTheme(
|
||||
<>
|
||||
{getMetricTooltipNode(
|
||||
{
|
||||
metric_name: 'long long long long metric name',
|
||||
label: '',
|
||||
verbose_name: '',
|
||||
description: '',
|
||||
},
|
||||
ref,
|
||||
)}
|
||||
</>,
|
||||
);
|
||||
expect(screen.getByText('Metric name')).toBeVisible();
|
||||
expect(screen.getByText('long long long long metric name')).toBeVisible();
|
||||
expect(screen.queryByText('Label')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Description')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should get metric name, verbose name and description in tooltip if it overflowed', () => {
|
||||
const ref = { current: { scrollWidth: 200, clientWidth: 100 } };
|
||||
renderWithTheme(
|
||||
<>
|
||||
{getMetricTooltipNode(
|
||||
{
|
||||
metric_name: 'count',
|
||||
label: '',
|
||||
verbose_name: 'longlonglonglonglong verbose metric',
|
||||
description: 'Count metric',
|
||||
},
|
||||
ref,
|
||||
)}
|
||||
</>,
|
||||
);
|
||||
expect(screen.getByText('Metric name')).toBeVisible();
|
||||
expect(screen.getByText('count')).toBeVisible();
|
||||
expect(screen.getByText('Label')).toBeVisible();
|
||||
expect(screen.getByText('longlonglonglonglong verbose metric')).toBeVisible();
|
||||
expect(screen.getByText('Description')).toBeVisible();
|
||||
expect(screen.getByText('Count metric')).toBeVisible();
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user