mirror of
https://github.com/apache/superset.git
synced 2026-08-01 19:42:29 +00:00
Compare commits
37 Commits
0.32.0rc2.
...
test_tag
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11bace3808 | ||
|
|
ba64ae6974 | ||
|
|
7846bae8e0 | ||
|
|
501340b5db | ||
|
|
19b3753d2c | ||
|
|
416534add5 | ||
|
|
713b0ae4f4 | ||
|
|
5669a82350 | ||
|
|
a09348d0ec | ||
|
|
adc9a6b495 | ||
|
|
2631558ac4 | ||
|
|
b70a9ae524 | ||
|
|
817783f466 | ||
|
|
36176f3e20 | ||
|
|
11a7ad00b7 | ||
|
|
c44ae612df | ||
|
|
c4fb7a0a87 | ||
|
|
5938ac30d6 | ||
|
|
7c02587924 | ||
|
|
744135c7fe | ||
|
|
83ee917832 | ||
|
|
a62a8d3d98 | ||
|
|
a93219f291 | ||
|
|
8100a8fa97 | ||
|
|
6b0ab2100d | ||
|
|
f38cea3ee3 | ||
|
|
01689c38ea | ||
|
|
879c553b0a | ||
|
|
97cb10dbc8 | ||
|
|
594cd70960 | ||
|
|
1ffee8b236 | ||
|
|
3ae7d32caa | ||
|
|
954e42bafe | ||
|
|
bab7ee7ecf | ||
|
|
bbd781b66e | ||
|
|
cf1a35b94b | ||
|
|
d65059b06a |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,6 +33,7 @@ _modules
|
||||
_static
|
||||
build
|
||||
app.db
|
||||
apache_superset.egg-info/
|
||||
changelog.sh
|
||||
dist
|
||||
dump.rdb
|
||||
|
||||
@@ -35,7 +35,7 @@ little bit helps, and credit will always be given.
|
||||
- [Pull Request Guidelines](#pull-request-guidelines)
|
||||
- [Protocol](#protocol)
|
||||
- [Managing Issues and PRs](#managing-issues-and-prs)
|
||||
- [Local development](#local-development)
|
||||
- [Setup Local Environment for Development](#setup-local-environment-for-development)
|
||||
- [Documentation](#documentation)
|
||||
- [Flask server](#flask-server)
|
||||
- [Frontend assets](#frontend-assets)
|
||||
@@ -410,6 +410,10 @@ export enum FeatureFlag {
|
||||
}
|
||||
```
|
||||
|
||||
`superset/config.py` contains `DEFAULT_FEATURE_FLAGS` which will be overwritten by
|
||||
those specified under FEATURE_FLAGS in `superset_config.py`. For example, `DEFAULT_FEATURE_FLAGS = { 'FOO': True, 'BAR': False }` in `superset/config.py` and `FEATURE_FLAGS = { 'BAR': True, 'BAZ': True }` in `superset_config.py` will result
|
||||
in combined feature flags of `{ 'FOO': True, 'BAR': True, 'BAZ': True }`.
|
||||
|
||||
## Linting
|
||||
|
||||
Lint the project with:
|
||||
|
||||
@@ -231,3 +231,5 @@ BSD 3-Clause licenses
|
||||
========================================================================
|
||||
Creative Commons Attribution 4.0
|
||||
========================================================================
|
||||
|
||||
(Creative Commons Attribution 4.0) diva-gis (http://www.diva-gis.org/Data)
|
||||
@@ -195,6 +195,7 @@ the world know they are using Superset. Join our growing community!
|
||||
1. [Lyft](https://www.lyft.com/)
|
||||
1. [Maieutical Labs](https://maieuticallabs.it)
|
||||
1. [Myra Labs](http://www.myralabs.com/)
|
||||
1. [Now](https://www.now.vn/)
|
||||
1. [PeopleDoc](https://www.people-doc.com)
|
||||
1. [Ona](https://ona.io)
|
||||
1. [Pronto Tools](http://www.prontotools.io)
|
||||
|
||||
120
RELEASING.md
120
RELEASING.md
@@ -46,80 +46,66 @@ git commit -a -m "New doc version"
|
||||
git push origin master
|
||||
```
|
||||
|
||||
# Apache Releases
|
||||
## Publishing a PyPI release
|
||||
|
||||
You'll probably want to run these commands manually and understand what
|
||||
they do prior to doing so.
|
||||
We create a branch that goes along each minor release `0.24`
|
||||
and micro releases get corresponding tags as in `0.24.0`. Git history should
|
||||
never be altered in release branches.
|
||||
Bug fixes and security-related patches get cherry-picked
|
||||
(usually from master) as in `git cherry-pick -x {SHA}`.
|
||||
|
||||
First you need to setup a few things. This is a one-off and doesn't
|
||||
need to be done at every release.
|
||||
Following a set of cherries being picked, a release can be pushed to
|
||||
PyPI as follows:
|
||||
|
||||
```bash
|
||||
# Create PGP Key
|
||||
gpg --gen-key
|
||||
|
||||
# Checkout ASF dist repo
|
||||
# branching off of master
|
||||
git checkout -b 0.25
|
||||
|
||||
svn checkout https://dist.apache.org/repos/dist/dev/incubator/superset/ ~/svn/superset_dev
|
||||
# cherry-picking a SHA
|
||||
git cherry-pick -x f9d85bd2e1fd9bc233d19c76bed09467522b968a
|
||||
# repeat with other SHAs, don't forget the -x
|
||||
|
||||
svn checkout https://dist.apache.org/repos/dist/incubator/superset/ ~/svn/superset
|
||||
cd ~/svn/superset
|
||||
|
||||
|
||||
# Add your GPG pub key to KEYS file. Replace "Maxime Beauchemin" with your name
|
||||
export FULLNAME="Maxime Beauchemin"
|
||||
(gpg --list-sigs $FULLNAME && gpg --armor --export $FULLNAME ) >> KEYS
|
||||
|
||||
|
||||
# Commit the changes
|
||||
svn commit -m "Add PGP keys of new Superset committer"
|
||||
# source of thruth for release numbers live in package.json
|
||||
vi superset/assets/package.json
|
||||
# hard code release in file, commit to the release branch
|
||||
git commit -a -m "0.25.0"
|
||||
|
||||
# create the release tag in the release branch
|
||||
git tag 0.25.0
|
||||
git push apache 0.25 --tags
|
||||
|
||||
# check travis to confirm the build succeeded as
|
||||
# you shouldn't assume that a clean cherry will be clean
|
||||
# when landing on a new sundae
|
||||
|
||||
# compile the JS, and push to pypi
|
||||
# to run this part you'll need a pypi account and rights on the
|
||||
# superset package. Committers that want to ship releases
|
||||
# should have this access.
|
||||
# You'll also need a `.pypirc` as specified here:
|
||||
# http://peterdowns.com/posts/first-time-with-pypi.html
|
||||
./pypi_push.sh
|
||||
|
||||
# publish an update to the CHANGELOG.md for the right version range
|
||||
# looking the latest CHANGELOG entry for the second argument
|
||||
./gen_changelog.sh 0.22.1 0.25.0
|
||||
# this will overwrite the CHANGELOG.md with only the version range
|
||||
# so you'll want to copy paste that on top of the previous CHANGELOG.md
|
||||
# open a PR against `master`
|
||||
```
|
||||
|
||||
Now let's craft a source release
|
||||
```bash
|
||||
# Assuming these commands are executed from the root of the repo
|
||||
# Setting a VERSION var will be useful
|
||||
export VERSION=0.31.0rc18
|
||||
In the future we'll start publishing release candidates for minor releases
|
||||
only, but typically not for micro release.
|
||||
The process will be similar to the process described above, expect the
|
||||
tags will be formatted `0.25.0rc1`, `0.25.0rc2`, ..., until consensus
|
||||
is reached.
|
||||
|
||||
# Let's create a git tag
|
||||
git tag -f ${VERSION}
|
||||
We should also have a Github PR label process to target the proper
|
||||
release, and tooling helping keeping track of all the cherries and
|
||||
target versions.
|
||||
|
||||
# [WARNING!] This command wipes everything in your repo that is
|
||||
# gitignored in preparation for the source release.
|
||||
# You may want to check that there's nothing your care about here first.
|
||||
# Alternatively you could clone the repo into another location as in
|
||||
# git clone git@github.com:apache/incubator-superset.git superset-releases
|
||||
git clean -fxd
|
||||
# Create the target folder
|
||||
mkdir -p ~/svn/superset_dev/${VERSION}/
|
||||
git archive \
|
||||
--format=tar.gz ${VERSION} \
|
||||
--prefix=apache-superset-${VERSION}/ \
|
||||
-o ~/svn/superset_dev/${VERSION}/apache-superset-${VERSION}-source.tar.gz
|
||||
|
||||
cd ~/svn/superset_dev/
|
||||
scripts/sign.sh apache-superset-${VERSION}-source.tar.gz
|
||||
```
|
||||
|
||||
Now let's ship this RC into svn's dev folder
|
||||
|
||||
```bash
|
||||
# cp or mv the files over to the svn repo
|
||||
cd ~/svn/superset_dev/
|
||||
svn add ${VERSION}
|
||||
svn commit
|
||||
```
|
||||
|
||||
Now you're ready to start the VOTE thread.
|
||||
|
||||
Upon a successful vote, you'll have to copy the folder into the non-"dev/"
|
||||
folder.
|
||||
```bash
|
||||
cp -r ~/svn/superset_dev/${VERSION}/ ~/svn/superset/${VERSION}/
|
||||
cd ~/svn/superset/
|
||||
svn add ${VERSION}
|
||||
svn commit
|
||||
```
|
||||
|
||||
Now you can announce the release on the mailing list, make sure to use the
|
||||
proper template
|
||||
For Apache releases, the process will be a bit heavier and should get
|
||||
documented here. There will be extra steps for signing the binaries,
|
||||
with a PGP key and providing MD5, Apache voting, as well as
|
||||
publishing to Apache's SVN repository. View the ASF docs for more
|
||||
information.
|
||||
|
||||
11
UPDATING.md
11
UPDATING.md
@@ -27,11 +27,6 @@ assists people when migrating to a new version.
|
||||
run `pip install superset[presto]` and/or `pip install superset[hive]` as
|
||||
required.
|
||||
|
||||
* [5445](https://github.com/apache/incubator-superset/pull/5445) : a change
|
||||
which prevents encoding of empty string from form data in the datanbase.
|
||||
This involves a non-schema changing migration which does potentially impact
|
||||
a large number of records. Scheduled downtime may be advised.
|
||||
|
||||
## Superset 0.31.0
|
||||
* boto3 / botocore was removed from the dependency list. If you use s3
|
||||
as a place to store your SQL Lab result set or Hive uploads, you may
|
||||
@@ -42,6 +37,12 @@ favor of good old `npm install`. While yarn should still work just fine,
|
||||
you should probably align to guarantee builds similar to the ones we
|
||||
use in testing and across the community in general.
|
||||
|
||||
## Superset 0.30.0
|
||||
* 0.30.0 includes a db_migration that removes allow_run_sync. This may
|
||||
require downtime because during the migration if the db is migrated first,
|
||||
superset will get 500 errors when the code can't find the field (until
|
||||
the deploy finishes).
|
||||
|
||||
## Superset 0.29.0
|
||||
* India was removed from the "Country Map" visualization as the geojson
|
||||
file included in the package was very large
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: apache-superset
|
||||
Version: 0.999.0.dev0
|
||||
Summary: A modern, enterprise-ready business intelligence web application
|
||||
Home-page: https://superset.apache.org/
|
||||
Author: Apache Software Foundation
|
||||
Author-email: dev@superset.incubator.apache.org
|
||||
License: Apache License, Version 2.0
|
||||
Download-URL: https://dist.apache.org/repos/dist/release/superset/0.999.0dev
|
||||
Description: <!--
|
||||
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.
|
||||
-->
|
||||
Superset
|
||||
=========
|
||||
|
||||
[](https://travis-ci.org/apache/incubator-superset)
|
||||
[](https://badge.fury.io/py/superset)
|
||||
[](https://codecov.io/github/apache/incubator-superset)
|
||||
[](https://pypi.python.org/pypi/superset)
|
||||
[](https://gitter.im/airbnb/superset?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://superset.incubator.apache.org)
|
||||
[](https://david-dm.org/apache/incubator-superset?path=superset/assets)
|
||||
|
||||
<img
|
||||
src="https://cloud.githubusercontent.com/assets/130878/20946612/49a8a25c-bbc0-11e6-8314-10bef902af51.png"
|
||||
alt="Superset"
|
||||
width="500"
|
||||
/>
|
||||
|
||||
**Apache Superset** (incubating) is a modern, enterprise-ready
|
||||
business intelligence web application
|
||||
|
||||
[this project used to be named **Caravel**, and **Panoramix** in the past]
|
||||
|
||||
|
||||
Screenshots & Gifs
|
||||
------------------
|
||||
|
||||
**View Dashboards**
|
||||
|
||||
<kbd><img title="View Dashboards" src="https://raw.githubusercontent.com/apache/incubator-superset/master/superset/assets/images/screenshots/bank_dash.png"></kbd><br/>
|
||||
|
||||
**Slice & dice your data**
|
||||
|
||||
<kbd><img title="Slice & dice your data" src="https://raw.githubusercontent.com/apache/incubator-superset/master/superset/assets/images/screenshots/explore.png"></kbd><br/>
|
||||
|
||||
**Query and visualize your data with SQL Lab**
|
||||
|
||||
<kbd><img title="SQL Lab" src="https://raw.githubusercontent.com/apache/incubator-superset/master/superset/assets/images/screenshots/sqllab.png"></kbd><br/>
|
||||
|
||||
**Visualize geospatial data with deck.gl**
|
||||
|
||||
<kbd><img title="Geospatial" src="https://raw.githubusercontent.com/apache/incubator-superset/master/superset/assets/images/screenshots/deckgl_dash.png"></kbd><br/>
|
||||
|
||||
**Choose from a wide array of visualizations**
|
||||
|
||||
<kbd><img title="Visualizations" src="https://raw.githubusercontent.com/apache/incubator-superset/master/superset/assets/images/screenshots/visualizations.png"></kbd><br/>
|
||||
|
||||
Apache Superset
|
||||
---------------
|
||||
Apache Superset is a data exploration and visualization web application.
|
||||
|
||||
Superset provides:
|
||||
* An intuitive interface to explore and visualize datasets, and
|
||||
create interactive dashboards.
|
||||
* A wide array of beautiful visualizations to showcase your data.
|
||||
* Easy, code-free, user flows to drill down and slice and dice the data
|
||||
underlying exposed dashboards. The dashboards and charts acts as a starting
|
||||
point for deeper analysis.
|
||||
* A state of the art SQL editor/IDE exposing a rich metadata browser, and
|
||||
an easy workflow to create visualizations out of any result set.
|
||||
* An extensible, high granularity security model allowing intricate rules
|
||||
on who can access which product features and datasets.
|
||||
Integration with major
|
||||
authentication backends (database, OpenID, LDAP, OAuth, REMOTE_USER, ...)
|
||||
* A lightweight semantic layer, allowing to control how data sources are
|
||||
exposed to the user by defining dimensions and metrics
|
||||
* Out of the box support for most SQL-speaking databases
|
||||
* Deep integration with Druid allows for Superset to stay blazing fast while
|
||||
slicing and dicing large, realtime datasets
|
||||
* Fast loading dashboards with configurable caching
|
||||
|
||||
|
||||
Database Support
|
||||
----------------
|
||||
|
||||
Superset speaks many SQL dialects through SQLAlchemy, a Python
|
||||
ORM that is compatible with
|
||||
[most common databases](https://docs.sqlalchemy.org/en/rel_1_2/core/engines.html).
|
||||
|
||||
Superset can be used to visualize data out of most databases:
|
||||
* MySQL
|
||||
* Postgres
|
||||
* Vertica
|
||||
* Oracle
|
||||
* Microsoft SQL Server
|
||||
* SQLite
|
||||
* Greenplum
|
||||
* Firebird
|
||||
* MariaDB
|
||||
* Sybase
|
||||
* IBM DB2
|
||||
* Exasol
|
||||
* MonetDB
|
||||
* Snowflake
|
||||
* Redshift
|
||||
* Clickhouse
|
||||
* Apache Kylin
|
||||
* Google BigQuery
|
||||
* **more!** look for the availability of a SQLAlchemy dialect for your database
|
||||
to find out whether it will work with Superset
|
||||
|
||||
|
||||
Apache Druid (Incubating)!
|
||||
------
|
||||
|
||||
On top of having the ability to query your relational databases,
|
||||
Superset ships with deep integration with Druid (a real time distributed
|
||||
column-store). When querying Druid,
|
||||
Superset can query humongous amounts of data on top of real time dataset.
|
||||
Note that Superset does not require Druid in any way to function, it's simply
|
||||
another database backend that it can query.
|
||||
|
||||
Here's a description of Druid from the http://druid.io website:
|
||||
|
||||
*Druid is an open-source analytics data store designed for
|
||||
business intelligence (OLAP) queries on event data. Druid provides low
|
||||
latency (real-time) data ingestion, flexible data exploration,
|
||||
and fast data aggregation. Existing Druid deployments have scaled to
|
||||
trillions of events and petabytes of data. Druid is best used to
|
||||
power analytic dashboards and applications.*
|
||||
|
||||
|
||||
Installation & Configuration
|
||||
----------------------------
|
||||
|
||||
[See in the documentation](https://superset.incubator.apache.org/installation.html)
|
||||
|
||||
|
||||
Resources
|
||||
-------------
|
||||
* [Mailing list](https://lists.apache.org/list.html?dev@superset.apache.org)
|
||||
* [Gitter (live chat) Channel](https://gitter.im/airbnb/superset)
|
||||
* [Docker image](https://hub.docker.com/r/amancevice/superset/) (community contributed)
|
||||
* [Slides from Strata (March 2016)](https://drive.google.com/open?id=0B5PVE0gzO81oOVJkdF9aNkJMSmM)
|
||||
* [Stackoverflow tag](https://stackoverflow.com/questions/tagged/apache-superset)
|
||||
* [Join our Slack](https://join.slack.com/t/apache-superset/shared_invite/enQtNDMxMDY5NjM4MDU0LTc2Y2QwYjE4NGYwNzQyZWUwYTExZTdiZDMzMWQwZjc2YmJmM2QyMDkwMGVjZTA4N2I2MzUxZTk2YmE5MWRhZWE)
|
||||
* [DEPRECATED Google Group](https://groups.google.com/forum/#!forum/airbnb_superset)
|
||||
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
Interested in contributing? Casual hacking? Check out
|
||||
[Contributing.MD](https://github.com/airbnb/superset/blob/master/CONTRIBUTING.md)
|
||||
|
||||
|
||||
Who uses Apache Superset (incubating)?
|
||||
--------------------------------------
|
||||
|
||||
Here's a list of organizations who have taken the time to send a PR to let
|
||||
the world know they are using Superset. Join our growing community!
|
||||
|
||||
1. [AiHello](https://www.aihello.com)
|
||||
1. [Airbnb](https://github.com/airbnb)
|
||||
1. [Airboxlab](https://foobot.io)
|
||||
1. [Aktia Bank plc](https://www.aktia.com)
|
||||
1. [Amino](https://amino.com)
|
||||
1. [Apollo GraphQL](https://www.apollographql.com/)
|
||||
1. [Ascendica Development](http://ascendicadevelopment.com)
|
||||
1. [Astronomer](https://www.astronomer.io)
|
||||
1. [Brilliant.org](https://brilliant.org/)
|
||||
1. [Capital Service S.A.](http://capitalservice.pl)
|
||||
1. [Clark.de](http://clark.de/)
|
||||
1. [Cloudsmith](https://cloudsmith.io)
|
||||
1. [CnOvit](http://www.cnovit.com/)
|
||||
1. [Dial Once](https://www.dial-once.com/en/)
|
||||
1. [Digit Game Studios](https://www.digitgaming.com/)
|
||||
1. [Douban](https://www.douban.com/)
|
||||
1. [Endress+Hauser](http://www.endress.com/)
|
||||
1. [FBK - ICT center](http://ict.fbk.eu)
|
||||
1. [Faasos](http://faasos.com/)
|
||||
1. [Fordeal](http://www.fordeal.com)
|
||||
1. [GfK Data Lab](http://datalab.gfk.com)
|
||||
1. [Grassroot](https://www.grassrootinstitute.org/)
|
||||
1. [Hostnfly](https://www.hostnfly.com/)
|
||||
1. [HuiShouBao](http://www.huishoubao.com/)
|
||||
1. [jampp](https://jampp.com/)
|
||||
1. [Konfío](http://konfio.mx)
|
||||
1. [Kuaishou](https://www.kuaishou.com/)
|
||||
1. [Lime](https://www.limebike.com/)
|
||||
1. [Living Goods](https://www.livinggoods.org)
|
||||
1. [Lyft](https://www.lyft.com/)
|
||||
1. [Maieutical Labs](https://maieuticallabs.it)
|
||||
1. [Myra Labs](http://www.myralabs.com/)
|
||||
1. [Now](https://www.now.vn/)
|
||||
1. [PeopleDoc](https://www.people-doc.com)
|
||||
1. [Ona](https://ona.io)
|
||||
1. [Pronto Tools](http://www.prontotools.io)
|
||||
1. [QPID Health](http://www.qpidhealth.com/ )
|
||||
1. [Qunar](https://www.qunar.com/)
|
||||
1. [Scoot](https://scoot.co/)
|
||||
1. [ScopeAI](https://www.getscopeai.com)
|
||||
1. [Shopee](https://shopee.sg)
|
||||
1. [Shopkick](https://www.shopkick.com)
|
||||
1. [Steamroot](https://streamroot.io/)
|
||||
1. [Showmax](https://tech.showmax.com)
|
||||
1. [Tails.com](https://tails.com)
|
||||
1. [THEICONIC](http://theiconic.com.au/)
|
||||
1. [Tobii](http://www.tobii.com/)
|
||||
1. [Tooploox](https://www.tooploox.com/)
|
||||
1. [TrustMedis](https://trustmedis.com)
|
||||
1. [Twitter](https://twitter.com/)
|
||||
1. [Udemy](https://www.udemy.com/)
|
||||
1. [VIPKID](https://www.vipkid.com.cn/)
|
||||
1. [Windsor.ai](https://www.windsor.ai/)
|
||||
1. [Yahoo!](https://yahoo.com/)
|
||||
1. [Zaihang](http://www.zaih.com/)
|
||||
1. [Zalando](https://www.zalando.com)
|
||||
|
||||
Platform: UNKNOWN
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Description-Content-Type: text/markdown
|
||||
Provides-Extra: presto
|
||||
Provides-Extra: hive
|
||||
Provides-Extra: cors
|
||||
Provides-Extra: gsheets
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
bleach<4.0.0,>=3.0.2
|
||||
celery<5.0.0,>=4.2.0
|
||||
click<7.0.0,>=6.0
|
||||
colorama
|
||||
contextlib2
|
||||
croniter>=0.3.28
|
||||
cryptography>=2.4.2
|
||||
flask<2.0.0,>=1.0.0
|
||||
flask-appbuilder<2.3.0,>=2.0.0
|
||||
flask-caching
|
||||
flask-compress
|
||||
flask-talisman
|
||||
flask-migrate
|
||||
flask-wtf
|
||||
geopy
|
||||
gunicorn
|
||||
humanize
|
||||
idna
|
||||
isodate
|
||||
markdown>=3.0
|
||||
pandas<0.24.0,>=0.18.0
|
||||
parsedatetime
|
||||
pathlib2
|
||||
polyline
|
||||
pydruid>=0.5.2
|
||||
python-dateutil
|
||||
python-geohash
|
||||
pyyaml>=3.13
|
||||
requests>=2.20.0
|
||||
retry>=0.9.2
|
||||
selenium>=3.141.0
|
||||
simplejson>=3.15.0
|
||||
sqlalchemy<2.0,>=1.3.1
|
||||
sqlalchemy-utils
|
||||
sqlparse
|
||||
unicodecsv
|
||||
wtforms-json
|
||||
|
||||
[cors]
|
||||
flask-cors>=2.0.0
|
||||
|
||||
[gsheets]
|
||||
gsheetsdb>=0.1.9
|
||||
|
||||
[hive]
|
||||
pyhive[hive]>=0.6.1
|
||||
tableschema
|
||||
|
||||
[presto]
|
||||
pyhive[presto]>=0.4.0
|
||||
@@ -1,2 +0,0 @@
|
||||
superset
|
||||
tests
|
||||
@@ -117,7 +117,7 @@ that the required dependencies are installed: ::
|
||||
**OSX**, system python is not recommended. brew's python also ships with pip ::
|
||||
|
||||
brew install pkg-config libffi openssl python
|
||||
env LDFLAGS="-L$(brew --prefix openssl)/lib" CFLAGS="-I$(brew --prefix openssl)/include" pip install cryptography==1.9
|
||||
env LDFLAGS="-L$(brew --prefix openssl)/lib" CFLAGS="-I$(brew --prefix openssl)/include" pip install cryptography==2.4.2
|
||||
|
||||
**Windows** isn't officially supported at this point, but if you want to
|
||||
attempt it, download `get-pip.py <https://bootstrap.pypa.io/get-pip.py>`_, and run ``python get-pip.py`` which may need admin access. Then run the following: ::
|
||||
@@ -405,6 +405,11 @@ You can also use `PyAthena` library(no java required) like this ::
|
||||
|
||||
See `PyAthena <https://github.com/laughingman7743/PyAthena#sqlalchemy>`_.
|
||||
|
||||
MSSQL
|
||||
-----
|
||||
|
||||
Full Unicode support requires SQLAlchemy 1.3 or later.
|
||||
|
||||
Snowflake
|
||||
---------
|
||||
|
||||
@@ -616,6 +621,18 @@ Upgrading should be as straightforward as running::
|
||||
superset db upgrade
|
||||
superset init
|
||||
|
||||
We recommend to follow standard best practices when upgrading Superset, such
|
||||
as taking a database backup prior to the upgrade, upgrading a staging
|
||||
environment prior to upgrading production, and upgrading production while less
|
||||
users are active on the platform.
|
||||
|
||||
.. note ::
|
||||
Some upgrades may contain backward-incompatible changes, or require
|
||||
scheduling downtime, when that is the case, contributors attach notes in
|
||||
``UPDATING.md`` in the repository. It's recommended to review this
|
||||
file prior to running an upgrade.
|
||||
|
||||
|
||||
Celery Tasks
|
||||
------------
|
||||
On large analytic databases, it's common to run background jobs, reports
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{ if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "superset.fullname" . -}}
|
||||
{{- $ingressPath := .Values.ingress.path -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
|
||||
@@ -17,7 +17,7 @@ chardet==3.0.4 # via requests
|
||||
click==6.7
|
||||
colorama==0.3.9
|
||||
contextlib2==0.5.5
|
||||
croniter==0.3.29
|
||||
croniter==0.3.26
|
||||
cryptography==2.4.2
|
||||
decorator==4.3.0 # via retry
|
||||
defusedxml==0.5.0 # via python3-openid
|
||||
@@ -42,8 +42,8 @@ kombu==4.2.1 # via celery
|
||||
mako==1.0.7 # via alembic
|
||||
markdown==3.0
|
||||
markupsafe==1.0 # via jinja2, mako
|
||||
numpy==1.15.2 # via pandas
|
||||
pandas==0.23.4
|
||||
numpy==1.15.2 # via numpy
|
||||
pandas==0.23.4 # via pandas
|
||||
parsedatetime==2.0.0
|
||||
pathlib2==2.3.0
|
||||
polyline==1.3.2
|
||||
@@ -60,7 +60,7 @@ requests==2.20.0
|
||||
retry==0.9.2
|
||||
selenium==3.141.0
|
||||
simplejson==3.15.0
|
||||
six==1.11.0 # via bleach, cryptography, isodate, pathlib2, polyline, pydruid, python-dateutil, sqlalchemy-utils, wtforms-json
|
||||
six==1.11.0 # via bleach, cryptography, isodate, pathlib2, polyline, pydruid, python-dateutil, sqlalchemy-utils
|
||||
sqlalchemy-utils==0.32.21
|
||||
sqlalchemy==1.2.2
|
||||
sqlparse==0.2.4
|
||||
@@ -69,5 +69,4 @@ urllib3==1.22 # via requests, selenium
|
||||
vine==1.1.4 # via amqp
|
||||
webencodings==0.5.1 # via bleach
|
||||
werkzeug==0.14.1 # via flask
|
||||
wtforms-json==0.3.3
|
||||
wtforms==2.2.1 # via flask-wtf, wtforms-json
|
||||
wtforms==2.2.1 # via flask-wtf
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
# Use this to sign the tar balls generated from
|
||||
# python setup.py sdist --formats=gztar
|
||||
# ie. sign.sh <my_tar_ball>
|
||||
# you will still be required to type in your signing key password
|
||||
# or it needs to be available in your keychain
|
||||
|
||||
NAME=${1}
|
||||
|
||||
gpg --armor --output ${NAME}.asc --detach-sig ${NAME}
|
||||
gpg --print-md SHA512 ${NAME} > ${NAME}.sha512
|
||||
9
setup.py
9
setup.py
@@ -71,10 +71,10 @@ setup(
|
||||
install_requires=[
|
||||
'bleach>=3.0.2, <4.0.0',
|
||||
'celery>=4.2.0, <5.0.0',
|
||||
'click>=6.0, <7.0.0', # click >=7 forces "-" instead of "_"
|
||||
'click>=6.0, <7.0.0', # `click`>=7 forces "-" instead of "_"
|
||||
'colorama',
|
||||
'contextlib2',
|
||||
'croniter>=0.3.28',
|
||||
'croniter>=0.3.26',
|
||||
'cryptography>=2.4.2',
|
||||
'flask>=1.0.0, <2.0.0',
|
||||
'flask-appbuilder>=1.12.1, <2.0.0',
|
||||
@@ -88,7 +88,7 @@ setup(
|
||||
'idna',
|
||||
'isodate',
|
||||
'markdown>=3.0',
|
||||
'pandas>=0.18.0',
|
||||
'pandas>=0.18.0, <0.24.0', # `pandas`>=0.24.0 changes datetimelike API
|
||||
'parsedatetime',
|
||||
'pathlib2',
|
||||
'polyline',
|
||||
@@ -104,13 +104,12 @@ setup(
|
||||
'sqlalchemy-utils',
|
||||
'sqlparse',
|
||||
'unicodecsv',
|
||||
'wtforms-json',
|
||||
],
|
||||
extras_require={
|
||||
'cors': ['flask-cors>=2.0.0'],
|
||||
'console_log': ['console_log==0.2.10'],
|
||||
'hive': [
|
||||
'pyhive>=0.4.0',
|
||||
'pyhive>=0.6.1',
|
||||
'tableschema',
|
||||
'thrift-sasl>=0.2.1',
|
||||
'thrift>=0.9.3',
|
||||
|
||||
@@ -28,7 +28,6 @@ from flask_compress import Compress
|
||||
from flask_migrate import Migrate
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from werkzeug.contrib.fixers import ProxyFix
|
||||
import wtforms_json
|
||||
|
||||
from superset import config
|
||||
from superset.connectors.connector_registry import ConnectorRegistry
|
||||
@@ -36,15 +35,13 @@ from superset.security import SupersetSecurityManager
|
||||
from superset.utils.core import (
|
||||
get_update_perms_flag, pessimistic_connection_handling, setup_cache)
|
||||
|
||||
wtforms_json.init()
|
||||
|
||||
APP_DIR = os.path.dirname(__file__)
|
||||
CONFIG_MODULE = os.environ.get('SUPERSET_CONFIG', 'superset.config')
|
||||
|
||||
if not os.path.exists(config.DATA_DIR):
|
||||
os.makedirs(config.DATA_DIR)
|
||||
|
||||
with open(APP_DIR + '/static/assets/backendSync.json', 'r', encoding='utf-8') as f:
|
||||
with open(APP_DIR + '/static/assets/backendSync.json', 'r') as f:
|
||||
frontend_config = json.load(f)
|
||||
|
||||
app = Flask(__name__)
|
||||
@@ -211,6 +208,18 @@ security_manager = appbuilder.sm
|
||||
|
||||
results_backend = app.config.get('RESULTS_BACKEND')
|
||||
|
||||
# Merge user defined feature flags with default feature flags
|
||||
feature_flags = app.config.get('DEFAULT_FEATURE_FLAGS')
|
||||
feature_flags.update(app.config.get('FEATURE_FLAGS') or {})
|
||||
|
||||
|
||||
def is_feature_enabled(feature):
|
||||
"""
|
||||
Utility function for checking whether a feature is turned on
|
||||
"""
|
||||
return feature_flags.get(feature)
|
||||
|
||||
|
||||
# Registering sources
|
||||
module_datasource_map = app.config.get('DEFAULT_MODULE_DS_MAP')
|
||||
module_datasource_map.update(app.config.get('ADDITIONAL_MODULE_DS_MAP'))
|
||||
|
||||
@@ -89,8 +89,8 @@ describe('AdhocMetrics', () => {
|
||||
cy.get('[data-test=metrics]').within(() => {
|
||||
cy.get('.select-clear').click();
|
||||
cy.get('.Select-control').click({ force: true });
|
||||
cy.get('input').type('num{downarrow}', { force: true });
|
||||
cy.get('.VirtualizedSelectFocusedOption')
|
||||
cy.get('input').type('num', { force: true });
|
||||
cy.get('.VirtualizedSelectOption[data-test=_col_num]')
|
||||
.trigger('mousedown')
|
||||
.click();
|
||||
});
|
||||
|
||||
@@ -29,7 +29,6 @@ flask run -p 8081 --with-threads --reload --debugger &
|
||||
|
||||
#block on the longer running javascript process
|
||||
time npm ci
|
||||
time npm run install-cypress
|
||||
time npm run build
|
||||
echo "[completed js build steps]"
|
||||
|
||||
|
||||
941
superset/assets/package-lock.json
generated
941
superset/assets/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superset",
|
||||
"version": "0.32.0rc2.dev2",
|
||||
"version": "0.999.0dev",
|
||||
"description": "Superset is a data exploration platform designed to be visual, intuitive, and interactive.",
|
||||
"license": "Apache-2.0",
|
||||
"directories": {
|
||||
@@ -19,8 +19,7 @@
|
||||
"lint-fix": "eslint --fix --ignore-path=.eslintignore --ext .js,.jsx . && tslint -c tslint.json --fix ./{src,spec}/**/*.ts{,x}",
|
||||
"sync-backend": "babel-node --preset=@babel/preset-env src/syncBackend.js",
|
||||
"cypress": "cypress",
|
||||
"cypress-debug": "cypress open --config watchForFileChanges=true",
|
||||
"install-cypress": "npm install cypress@3.1.5"
|
||||
"cypress-debug": "cypress open --config watchForFileChanges=true"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -63,7 +62,6 @@
|
||||
"@vx/responsive": "0.0.172",
|
||||
"@vx/scale": "^0.0.165",
|
||||
"abortcontroller-polyfill": "^1.1.9",
|
||||
"bignumber.js": "^8.1.1",
|
||||
"bootstrap": "^3.3.6",
|
||||
"bootstrap-slider": "^10.0.0",
|
||||
"brace": "^0.11.1",
|
||||
@@ -117,10 +115,9 @@
|
||||
"react-select": "1.2.1",
|
||||
"react-select-fast-filter-options": "^0.2.1",
|
||||
"react-sortable-hoc": "^0.8.3",
|
||||
"react-split": "^2.0.4",
|
||||
"react-split-pane": "^0.1.66",
|
||||
"react-sticky": "^6.0.2",
|
||||
"react-syntax-highlighter": "^7.0.4",
|
||||
"react-transition-group": "^2.5.3",
|
||||
"react-virtualized": "9.19.1",
|
||||
"react-virtualized-select": "^3.1.3",
|
||||
"reactable-arc": "0.14.42",
|
||||
@@ -159,6 +156,7 @@
|
||||
"cache-loader": "^1.2.2",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"css-loader": "^1.0.0",
|
||||
"cypress": "^3.0.3",
|
||||
"enzyme": "^3.3.0",
|
||||
"enzyme-adapter-react-16": "^1.1.1",
|
||||
"eslint": "^4.19.1",
|
||||
@@ -194,7 +192,7 @@
|
||||
"terser-webpack-plugin": "^1.1.0",
|
||||
"thread-loader": "^1.2.0",
|
||||
"transform-loader": "^0.2.3",
|
||||
"ts-jest": "^24.0.0",
|
||||
"ts-jest": "^23.10.4",
|
||||
"ts-loader": "^5.2.0",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-react": "^3.6.0",
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import BigNumber from 'bignumber.js';
|
||||
import transform from 'src/chart/transformBigNumber';
|
||||
|
||||
describe('transformBigNumber', () => {
|
||||
it('should transform BigNumber on its own', () => {
|
||||
expect(transform(new BigNumber(123.456))).toBe(123.456);
|
||||
});
|
||||
|
||||
it('should transform BigNumber in objects', () => {
|
||||
expect(transform({
|
||||
foo: new BigNumber(123),
|
||||
bar: 456,
|
||||
baz: null,
|
||||
})).toEqual({ foo: 123, bar: 456, baz: null });
|
||||
});
|
||||
|
||||
it('should transform BigNumber in arrays', () => {
|
||||
expect(transform([
|
||||
{ foo: new BigNumber(123) },
|
||||
{ bar: 456 },
|
||||
])).toEqual([{ foo: 123 }, { bar: 456 }]);
|
||||
});
|
||||
|
||||
it('should transform BigNumber in nested structures', () => {
|
||||
expect(transform([{
|
||||
x: new BigNumber(123),
|
||||
y: [{ foo: new BigNumber(456) }, { bar: 'str' }],
|
||||
z: { some: [new BigNumber(789)] },
|
||||
}])).toEqual([{
|
||||
x: 123,
|
||||
y: [{ foo: 456 }, { bar: 'str' }],
|
||||
z: { some: [789] },
|
||||
}]);
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,7 @@ import { shallow } from 'enzyme';
|
||||
|
||||
import { STATUS_OPTIONS } from '../../../src/SqlLab/constants';
|
||||
import { initialState } from './fixtures';
|
||||
import SouthPaneContainer, { SouthPane } from '../../../src/SqlLab/components/SouthPane';
|
||||
import SouthPane from '../../../src/SqlLab/components/SouthPane';
|
||||
|
||||
describe('SouthPane', () => {
|
||||
const middlewares = [thunk];
|
||||
@@ -42,16 +42,11 @@ describe('SouthPane', () => {
|
||||
};
|
||||
|
||||
const getWrapper = () => (
|
||||
shallow(<SouthPaneContainer {...mockedProps} />, {
|
||||
shallow(<SouthPane {...mockedProps} />, {
|
||||
context: { store },
|
||||
}).dive());
|
||||
|
||||
let wrapper;
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(SouthPane.prototype, 'getSouthPaneHeight').mockImplementation(() => 500);
|
||||
});
|
||||
|
||||
it('should render offline when the state is offline', () => {
|
||||
wrapper = getWrapper();
|
||||
wrapper.setProps({ offline: true });
|
||||
|
||||
@@ -38,11 +38,6 @@ describe('SqlEditor', () => {
|
||||
defaultQueryLimit: 1000,
|
||||
maxRow: 100000,
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(SqlEditor.prototype, 'getSqlEditorHeight').mockImplementation(() => 500);
|
||||
});
|
||||
|
||||
it('is valid', () => {
|
||||
expect(
|
||||
React.isValidElement(<SqlEditor {...mockedProps} />),
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('getBreakPoints', () => {
|
||||
});
|
||||
|
||||
it('returns sorted break points', () => {
|
||||
const fd = { breakPoints: ['0', '10', '100', '50', '1000'] };
|
||||
const fd = { break_points: ['0', '10', '100', '50', '1000'] };
|
||||
const result = getBreakPoints(fd, [], metricAccessor);
|
||||
const expected = ['0', '10', '50', '100', '1000'];
|
||||
expect(result).toEqual(expected);
|
||||
@@ -45,7 +45,7 @@ describe('getBreakPoints', () => {
|
||||
});
|
||||
|
||||
it('formats number with proper precision', () => {
|
||||
const fd = { metric: 'count', numBuckets: 2 };
|
||||
const fd = { metric: 'count', num_buckets: 2 };
|
||||
const features = [0, 1 / 3, 2 / 3, 1].map(count => ({ count }));
|
||||
const result = getBreakPoints(fd, features, metricAccessor);
|
||||
const expected = ['0.0', '0.5', '1.0'];
|
||||
@@ -53,7 +53,7 @@ describe('getBreakPoints', () => {
|
||||
});
|
||||
|
||||
it('works with a zero range', () => {
|
||||
const fd = { metric: 'count', numBuckets: 1 };
|
||||
const fd = { metric: 'count', num_buckets: 1 };
|
||||
const features = [1, 1, 1].map(count => ({ count }));
|
||||
const result = getBreakPoints(fd, features, metricAccessor);
|
||||
const expected = ['1', '1'];
|
||||
@@ -69,7 +69,7 @@ describe('getBreakPointColorScaler', () => {
|
||||
it('returns linear color scaler if there are no break points', () => {
|
||||
const fd = {
|
||||
metric: 'count',
|
||||
linearColorScheme: ['#000000', '#ffffff'],
|
||||
linear_color_scheme: ['#000000', '#ffffff'],
|
||||
opacity: 100,
|
||||
};
|
||||
const features = [10, 20, 30].map(count => ({ count }));
|
||||
@@ -82,8 +82,8 @@ describe('getBreakPointColorScaler', () => {
|
||||
it('returns bucketing scaler if there are break points', () => {
|
||||
const fd = {
|
||||
metric: 'count',
|
||||
linearColorScheme: ['#000000', '#ffffff'],
|
||||
breakPoints: ['0', '1', '10'],
|
||||
linear_color_scheme: ['#000000', '#ffffff'],
|
||||
break_points: ['0', '1', '10'],
|
||||
opacity: 100,
|
||||
};
|
||||
const features = [];
|
||||
@@ -97,8 +97,8 @@ describe('getBreakPointColorScaler', () => {
|
||||
it('mask values outside the break points', () => {
|
||||
const fd = {
|
||||
metric: 'count',
|
||||
linearColorScheme: ['#000000', '#ffffff'],
|
||||
breakPoints: ['0', '1', '10'],
|
||||
linear_color_scheme: ['#000000', '#ffffff'],
|
||||
break_points: ['0', '1', '10'],
|
||||
opacity: 100,
|
||||
};
|
||||
const features = [];
|
||||
@@ -116,8 +116,8 @@ describe('getBuckets', () => {
|
||||
it('computes buckets for break points', () => {
|
||||
const fd = {
|
||||
metric: 'count',
|
||||
linearColorScheme: ['#000000', '#ffffff'],
|
||||
breakPoints: ['0', '1', '10'],
|
||||
linear_color_scheme: ['#000000', '#ffffff'],
|
||||
break_points: ['0', '1', '10'],
|
||||
opacity: 100,
|
||||
};
|
||||
const features = [];
|
||||
|
||||
@@ -84,7 +84,7 @@ class App extends React.PureComponent {
|
||||
content = (
|
||||
<div>
|
||||
<QueryAutoRefresh />
|
||||
<TabbedSqlEditors />
|
||||
<TabbedSqlEditors getHeight={this.getHeight} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -184,21 +184,19 @@ class ExploreResultsButton extends React.PureComponent {
|
||||
render() {
|
||||
const allowsSubquery = this.props.database && this.props.database.allows_subquery;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
bsSize="small"
|
||||
onClick={this.onClick}
|
||||
disabled={!allowsSubquery}
|
||||
tooltip={t('Explore the result set in the data exploration view')}
|
||||
>
|
||||
<InfoTooltipWithTrigger icon="line-chart" placement="top" label="explore" /> {t('Explore')}
|
||||
</Button>
|
||||
<Button
|
||||
bsSize="small"
|
||||
onClick={this.onClick}
|
||||
disabled={!allowsSubquery}
|
||||
tooltip={t('Explore the result set in the data exploration view')}
|
||||
>
|
||||
<Dialog
|
||||
ref={(el) => {
|
||||
this.dialog = el;
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
<InfoTooltipWithTrigger icon="line-chart" placement="top" label="explore" /> {t('Explore')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,13 +31,11 @@ const propTypes = {
|
||||
dbId: PropTypes.number,
|
||||
animation: PropTypes.bool,
|
||||
onSave: PropTypes.func,
|
||||
saveQueryWarning: PropTypes.string,
|
||||
};
|
||||
const defaultProps = {
|
||||
defaultLabel: t('Undefined'),
|
||||
animation: true,
|
||||
onSave: () => {},
|
||||
saveQueryWarning: null,
|
||||
};
|
||||
|
||||
class SaveQuery extends React.PureComponent {
|
||||
@@ -110,18 +108,6 @@ class SaveQuery extends React.PureComponent {
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
{this.props.saveQueryWarning && (
|
||||
<div>
|
||||
<Row>
|
||||
<Col md={12}>
|
||||
<small>
|
||||
{this.props.saveQueryWarning}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
</div>
|
||||
)}
|
||||
<Row>
|
||||
<Col md={12}>
|
||||
<Button
|
||||
|
||||
@@ -48,24 +48,7 @@ const defaultProps = {
|
||||
offline: false,
|
||||
};
|
||||
|
||||
export class SouthPane extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
height: props.height,
|
||||
};
|
||||
this.southPaneRef = React.createRef();
|
||||
this.getSouthPaneHeight = this.getSouthPaneHeight.bind(this);
|
||||
this.switchTab = this.switchTab.bind(this);
|
||||
}
|
||||
componentWillReceiveProps() {
|
||||
// south pane expands the entire height of the tab content on mount
|
||||
this.setState({ height: this.getSouthPaneHeight() });
|
||||
}
|
||||
// One layer of abstraction for easy spying in unit tests
|
||||
getSouthPaneHeight() {
|
||||
return this.southPaneRef.current ? this.southPaneRef.current.clientHeight : 0;
|
||||
}
|
||||
class SouthPane extends React.PureComponent {
|
||||
switchTab(id) {
|
||||
this.props.actions.setActiveSouthPaneTab(id);
|
||||
}
|
||||
@@ -76,7 +59,7 @@ export class SouthPane extends React.PureComponent {
|
||||
{ STATUS_OPTIONS.offline }
|
||||
</Label>);
|
||||
}
|
||||
const innerTabHeight = this.state.height - 55;
|
||||
const innerTabHeight = this.props.height - 55;
|
||||
let latestQuery;
|
||||
const props = this.props;
|
||||
if (props.editorQueries.length > 0) {
|
||||
@@ -115,12 +98,12 @@ export class SouthPane extends React.PureComponent {
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="SouthPane" ref={this.southPaneRef}>
|
||||
<div className="SouthPane">
|
||||
<Tabs
|
||||
bsStyle="tabs"
|
||||
id={shortid.generate()}
|
||||
activeKey={this.props.activeSouthPaneTab}
|
||||
onSelect={this.switchTab}
|
||||
onSelect={this.switchTab.bind(this)}
|
||||
>
|
||||
<Tab
|
||||
title={t('Results')}
|
||||
|
||||
@@ -17,18 +17,21 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import PropTypes from 'prop-types';
|
||||
import { throttle } from 'lodash';
|
||||
import {
|
||||
Col,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
Form,
|
||||
FormControl,
|
||||
Label,
|
||||
OverlayTrigger,
|
||||
Row,
|
||||
Tooltip,
|
||||
Collapse,
|
||||
} from 'react-bootstrap';
|
||||
import Split from 'react-split';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import Button from '../../components/Button';
|
||||
@@ -44,13 +47,9 @@ import AceEditorWrapper from './AceEditorWrapper';
|
||||
import { STATE_BSSTYLE_MAP } from '../constants';
|
||||
import RunQueryActionButton from './RunQueryActionButton';
|
||||
|
||||
const SQL_TOOLBAR_HEIGHT = 51;
|
||||
const GUTTER_HEIGHT = 5;
|
||||
const INITIAL_NORTH_PERCENT = 30;
|
||||
const INITIAL_SOUTH_PERCENT = 70;
|
||||
|
||||
const propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
getHeight: PropTypes.func.isRequired,
|
||||
database: PropTypes.object,
|
||||
latestQuery: PropTypes.object,
|
||||
tables: PropTypes.array.isRequired,
|
||||
@@ -60,14 +59,12 @@ const propTypes = {
|
||||
hideLeftBar: PropTypes.bool,
|
||||
defaultQueryLimit: PropTypes.number.isRequired,
|
||||
maxRow: PropTypes.number.isRequired,
|
||||
saveQueryWarning: PropTypes.string,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
database: null,
|
||||
latestQuery: null,
|
||||
hideLeftBar: false,
|
||||
saveQueryWarning: null,
|
||||
};
|
||||
|
||||
class SqlEditor extends React.PureComponent {
|
||||
@@ -78,18 +75,13 @@ class SqlEditor extends React.PureComponent {
|
||||
ctas: '',
|
||||
sql: props.queryEditor.sql,
|
||||
};
|
||||
this.sqlEditorRef = React.createRef();
|
||||
this.northPaneRef = React.createRef();
|
||||
|
||||
this.onResizeStart = this.onResizeStart.bind(this);
|
||||
this.onResizeEnd = this.onResizeEnd.bind(this);
|
||||
this.onResize = this.onResize.bind(this);
|
||||
this.throttledResize = throttle(this.onResize, 250);
|
||||
this.runQuery = this.runQuery.bind(this);
|
||||
this.stopQuery = this.stopQuery.bind(this);
|
||||
this.onSqlChanged = this.onSqlChanged.bind(this);
|
||||
this.setQueryEditorSql = this.setQueryEditorSql.bind(this);
|
||||
this.queryPane = this.queryPane.bind(this);
|
||||
this.getAceEditorAndSouthPaneHeights = this.getAceEditorAndSouthPaneHeights.bind(this);
|
||||
this.getSqlEditorHeight = this.getSqlEditorHeight.bind(this);
|
||||
}
|
||||
componentWillMount() {
|
||||
if (this.state.autorun) {
|
||||
@@ -99,41 +91,29 @@ class SqlEditor extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
// We need to measure the height of the sql editor post render to figure the height of
|
||||
// the south pane so it gets rendered properly
|
||||
// eslint-disable-next-line react/no-did-mount-set-state
|
||||
this.setState({ height: this.getSqlEditorHeight() });
|
||||
this.onResize();
|
||||
window.addEventListener('resize', this.throttledResize);
|
||||
}
|
||||
onResizeStart() {
|
||||
// Set the heights on the ace editor and the ace content area after drag starts
|
||||
// to smooth out the visual transition to the new heights when drag ends
|
||||
document.getElementById('brace-editor').style.height = `calc(100% - ${SQL_TOOLBAR_HEIGHT}px)`;
|
||||
document.getElementsByClassName('ace_content')[0].style.height = '100%';
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.throttledResize);
|
||||
}
|
||||
onResizeEnd([northPercent, southPercent]) {
|
||||
this.setState(this.getAceEditorAndSouthPaneHeights(
|
||||
this.state.height, northPercent, southPercent));
|
||||
onResize() {
|
||||
const height = this.sqlEditorHeight();
|
||||
const editorPaneHeight = this.props.queryEditor.height || 200;
|
||||
const splitPaneHandlerHeight = 8; // 4px of height + 4px of top-margin
|
||||
this.setState({
|
||||
editorPaneHeight,
|
||||
southPaneHeight: height - editorPaneHeight - splitPaneHandlerHeight,
|
||||
height,
|
||||
});
|
||||
|
||||
if (this.northPaneRef.current && this.northPaneRef.current.clientHeight) {
|
||||
this.props.actions.persistEditorHeight(this.props.queryEditor,
|
||||
this.northPaneRef.current.clientHeight);
|
||||
if (this.refs.ace && this.refs.ace.clientHeight) {
|
||||
this.props.actions.persistEditorHeight(this.props.queryEditor, this.refs.ace.clientHeight);
|
||||
}
|
||||
}
|
||||
onSqlChanged(sql) {
|
||||
this.setState({ sql });
|
||||
}
|
||||
// One layer of abstraction for easy spying in unit tests
|
||||
getSqlEditorHeight() {
|
||||
return this.sqlEditorRef.current ? this.sqlEditorRef.current.clientHeight : 0;
|
||||
}
|
||||
// Return the heights for the ace editor and the south pane as an object
|
||||
// given the height of the sql editor, north pane percent and south pane percent.
|
||||
getAceEditorAndSouthPaneHeights(height, northPercent, southPercent) {
|
||||
return {
|
||||
aceEditorHeight: height * northPercent / 100 - SQL_TOOLBAR_HEIGHT - GUTTER_HEIGHT / 2,
|
||||
southPaneHeight: height * southPercent / 100,
|
||||
};
|
||||
}
|
||||
getHotkeyConfig() {
|
||||
return [
|
||||
{
|
||||
@@ -207,42 +187,9 @@ class SqlEditor extends React.PureComponent {
|
||||
ctasChanged(event) {
|
||||
this.setState({ ctas: event.target.value });
|
||||
}
|
||||
queryPane() {
|
||||
const hotkeys = this.getHotkeyConfig();
|
||||
const { aceEditorHeight, southPaneHeight } = this.getAceEditorAndSouthPaneHeights(
|
||||
this.state.height, INITIAL_NORTH_PERCENT, INITIAL_SOUTH_PERCENT);
|
||||
return (
|
||||
<div className="queryPane">
|
||||
<Split
|
||||
sizes={[INITIAL_NORTH_PERCENT, INITIAL_SOUTH_PERCENT]}
|
||||
minSize={200}
|
||||
direction="vertical"
|
||||
gutterSize={GUTTER_HEIGHT}
|
||||
onDragStart={this.onResizeStart}
|
||||
onDragEnd={this.onResizeEnd}
|
||||
>
|
||||
<div ref={this.northPaneRef}>
|
||||
<AceEditorWrapper
|
||||
actions={this.props.actions}
|
||||
onBlur={this.setQueryEditorSql}
|
||||
onChange={this.onSqlChanged}
|
||||
queryEditor={this.props.queryEditor}
|
||||
sql={this.props.queryEditor.sql}
|
||||
tables={this.props.tables}
|
||||
height={`${this.state.aceEditorHeight || aceEditorHeight}px`}
|
||||
hotkeys={hotkeys}
|
||||
/>
|
||||
{this.renderEditorBottomBar(hotkeys)}
|
||||
</div>
|
||||
<SouthPane
|
||||
editorQueries={this.props.editorQueries}
|
||||
dataPreviewQueries={this.props.dataPreviewQueries}
|
||||
actions={this.props.actions}
|
||||
height={this.state.southPaneHeight || southPaneHeight}
|
||||
/>
|
||||
</Split>
|
||||
</div>
|
||||
);
|
||||
sqlEditorHeight() {
|
||||
const horizontalScrollbarHeight = 25;
|
||||
return parseInt(this.props.getHeight(), 10) - horizontalScrollbarHeight;
|
||||
}
|
||||
renderEditorBottomBar(hotkeys) {
|
||||
let ctasControls;
|
||||
@@ -311,7 +258,6 @@ class SqlEditor extends React.PureComponent {
|
||||
onSave={this.props.actions.saveQuery}
|
||||
schema={qe.schema}
|
||||
dbId={qe.dbId}
|
||||
saveQueryWarning={this.props.saveQueryWarning}
|
||||
/>
|
||||
</span>
|
||||
<span className="m-r-5">
|
||||
@@ -359,23 +305,74 @@ class SqlEditor extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
render() {
|
||||
const height = this.sqlEditorHeight();
|
||||
const defaultNorthHeight = this.props.queryEditor.height || 200;
|
||||
const hotkeys = this.getHotkeyConfig();
|
||||
return (
|
||||
<div ref={this.sqlEditorRef} className="SqlEditor">
|
||||
<CSSTransition
|
||||
classNames="schemaPane"
|
||||
in={!this.props.hideLeftBar}
|
||||
timeout={300}
|
||||
>
|
||||
<div className="schemaPane">
|
||||
<SqlEditorLeftBar
|
||||
database={this.props.database}
|
||||
queryEditor={this.props.queryEditor}
|
||||
tables={this.props.tables}
|
||||
actions={this.props.actions}
|
||||
/>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
{this.queryPane()}
|
||||
<div
|
||||
className="SqlEditor"
|
||||
style={{
|
||||
height: height + 'px',
|
||||
}}
|
||||
>
|
||||
<Row>
|
||||
<Collapse
|
||||
in={!this.props.hideLeftBar}
|
||||
>
|
||||
<Col
|
||||
xs={6}
|
||||
sm={5}
|
||||
md={4}
|
||||
lg={3}
|
||||
>
|
||||
<SqlEditorLeftBar
|
||||
height={height}
|
||||
database={this.props.database}
|
||||
queryEditor={this.props.queryEditor}
|
||||
tables={this.props.tables}
|
||||
actions={this.props.actions}
|
||||
/>
|
||||
</Col>
|
||||
</Collapse>
|
||||
<Col
|
||||
xs={this.props.hideLeftBar ? 12 : 6}
|
||||
sm={this.props.hideLeftBar ? 12 : 7}
|
||||
md={this.props.hideLeftBar ? 12 : 8}
|
||||
lg={this.props.hideLeftBar ? 12 : 9}
|
||||
style={{ height: this.state.height }}
|
||||
>
|
||||
<SplitPane
|
||||
split="horizontal"
|
||||
defaultSize={defaultNorthHeight}
|
||||
minSize={100}
|
||||
onChange={this.onResize}
|
||||
>
|
||||
<div ref="ace" style={{ width: '100%' }}>
|
||||
<div>
|
||||
<AceEditorWrapper
|
||||
actions={this.props.actions}
|
||||
onBlur={this.setQueryEditorSql}
|
||||
onChange={this.onSqlChanged}
|
||||
queryEditor={this.props.queryEditor}
|
||||
sql={this.props.queryEditor.sql}
|
||||
tables={this.props.tables}
|
||||
height={((this.state.editorPaneHeight || defaultNorthHeight) - 50) + 'px'}
|
||||
hotkeys={hotkeys}
|
||||
/>
|
||||
{this.renderEditorBottomBar(hotkeys)}
|
||||
</div>
|
||||
</div>
|
||||
<div ref="south">
|
||||
<SouthPane
|
||||
editorQueries={this.props.editorQueries}
|
||||
dataPreviewQueries={this.props.dataPreviewQueries}
|
||||
actions={this.props.actions}
|
||||
height={this.state.southPaneHeight || 0}
|
||||
/>
|
||||
</div>
|
||||
</SplitPane>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import TableElement from './TableElement';
|
||||
import TableSelector from '../../components/TableSelector';
|
||||
|
||||
@@ -105,7 +106,7 @@ export default class SqlEditorLeftBar extends React.PureComponent {
|
||||
const tableMetaDataHeight = this.props.height - 130; // 130 is the height of the selects above
|
||||
const qe = this.props.queryEditor;
|
||||
return (
|
||||
<div className="sqlEditorLeftBar">
|
||||
<div className="clearfix">
|
||||
<TableSelector
|
||||
dbId={qe.dbId}
|
||||
schema={qe.schema}
|
||||
|
||||
@@ -39,13 +39,12 @@ const propTypes = {
|
||||
queryEditors: PropTypes.array,
|
||||
tabHistory: PropTypes.array.isRequired,
|
||||
tables: PropTypes.array.isRequired,
|
||||
getHeight: PropTypes.func.isRequired,
|
||||
offline: PropTypes.bool,
|
||||
saveQueryWarning: PropTypes.string,
|
||||
};
|
||||
const defaultProps = {
|
||||
queryEditors: [],
|
||||
offline: false,
|
||||
saveQueryWarning: null,
|
||||
};
|
||||
|
||||
let queryCount = 1;
|
||||
@@ -239,6 +238,7 @@ class TabbedSqlEditors extends React.PureComponent {
|
||||
<div className="panel-body">
|
||||
{isSelected && (
|
||||
<SqlEditor
|
||||
getHeight={this.props.getHeight}
|
||||
tables={this.props.tables.filter(xt => xt.queryEditorId === qe.id)}
|
||||
queryEditor={qe}
|
||||
editorQueries={this.state.queriesArray}
|
||||
@@ -249,7 +249,6 @@ class TabbedSqlEditors extends React.PureComponent {
|
||||
hideLeftBar={this.state.hideLeftBar}
|
||||
defaultQueryLimit={this.props.defaultQueryLimit}
|
||||
maxRow={this.props.maxRow}
|
||||
saveQueryWarning={this.props.saveQueryWarning}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -292,7 +291,6 @@ function mapStateToProps({ sqlLab, common }) {
|
||||
offline: sqlLab.offline,
|
||||
defaultQueryLimit: common.conf.DEFAULT_SQLLAB_LIMIT,
|
||||
maxRow: common.conf.SQL_MAX_ROW,
|
||||
saveQueryWarning: common.conf.SQLLAB_SAVE_WARNING_MESSAGE,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
|
||||
@@ -135,8 +135,7 @@ div.Workspace {
|
||||
background-color: #e8e8e8;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border: 1px solid #ccc;
|
||||
border-top: 0;
|
||||
border-bottom: 2px solid #ccc;
|
||||
|
||||
form {
|
||||
margin-block-end: 0;
|
||||
@@ -194,67 +193,21 @@ div.Workspace {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.SqlLab {
|
||||
.tab-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#brace-editor {
|
||||
height: calc(100% - 51px);
|
||||
}
|
||||
|
||||
.ace_content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.SouthPane {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.SqlEditor {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
|
||||
.schemaPane {
|
||||
flex-grow: 1;
|
||||
transition: all .3s ease-in-out;
|
||||
.Resizer {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.schemaPane-enter-done, .schemaPane-exit {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.schemaPane-enter-active, .schemaPane-exit-active {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.schemaPane-enter, .schemaPane-exit-done {
|
||||
transform: translateX(-100%);
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.queryPane {
|
||||
flex-grow: 8;
|
||||
position: relative;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.schemaPane-exit-done + .queryPane {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.gutter {
|
||||
.Resizer.horizontal {
|
||||
height: 4px;
|
||||
border-top: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 3%;
|
||||
margin: 3px 47%;
|
||||
}
|
||||
|
||||
.gutter.gutter-vertical {
|
||||
cursor: row-resize;
|
||||
width: 4%;
|
||||
margin-top: 4px;
|
||||
margin-left: 47%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,6 +298,9 @@ a.Link {
|
||||
.tooltip-inner {
|
||||
max-width: 500px;
|
||||
}
|
||||
.SplitPane.horizontal {
|
||||
padding-right: 4px;
|
||||
}
|
||||
.SouthPane {
|
||||
margin-top: 10px;
|
||||
position: absolute;
|
||||
|
||||
@@ -24,7 +24,6 @@ import { ChartProps } from '@superset-ui/chart';
|
||||
import { Tooltip } from 'react-bootstrap';
|
||||
import { Logger, LOG_ACTIONS_RENDER_CHART } from '../logger';
|
||||
import SuperChart from '../visualizations/core/components/SuperChart';
|
||||
import transformBigNumber from './transformBigNumber';
|
||||
|
||||
const propTypes = {
|
||||
annotationData: PropTypes.object,
|
||||
@@ -68,10 +67,9 @@ class ChartRenderer extends React.Component {
|
||||
this.handleAddFilter = this.handleAddFilter.bind(this);
|
||||
this.handleRenderSuccess = this.handleRenderSuccess.bind(this);
|
||||
this.handleRenderFailure = this.handleRenderFailure.bind(this);
|
||||
this.preTransformProps = this.preTransformProps.bind(this);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (
|
||||
nextProps.queryResponse &&
|
||||
['success', 'rendered'].indexOf(nextProps.chartStatus) > -1 &&
|
||||
@@ -81,7 +79,6 @@ class ChartRenderer extends React.Component {
|
||||
nextProps.queryResponse !== this.props.queryResponse ||
|
||||
nextProps.height !== this.props.height ||
|
||||
nextProps.width !== this.props.width ||
|
||||
nextState.tooltip !== this.state.tooltip ||
|
||||
nextProps.triggerRender)
|
||||
) {
|
||||
return true;
|
||||
@@ -152,18 +149,6 @@ class ChartRenderer extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
preTransformProps(chartProps) {
|
||||
const payload = chartProps.payload;
|
||||
const data = transformBigNumber(payload.data);
|
||||
return new ChartProps({
|
||||
...chartProps,
|
||||
payload: {
|
||||
...payload,
|
||||
data,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
renderTooltip() {
|
||||
const { tooltip } = this.state;
|
||||
if (tooltip && tooltip.content) {
|
||||
@@ -207,7 +192,6 @@ class ChartRenderer extends React.Component {
|
||||
className={`${snakeCase(vizType)}`}
|
||||
chartType={vizType}
|
||||
chartProps={skipChartRendering ? null : this.prepareChartProps()}
|
||||
preTransformProps={this.preTransformProps}
|
||||
onRenderSuccess={this.handleRenderSuccess}
|
||||
onRenderFailure={this.handleRenderFailure}
|
||||
/>
|
||||
|
||||
@@ -246,10 +246,7 @@ export function runQuery(formData, force = false, timeout = 60, key) {
|
||||
export function redirectSQLLab(formData) {
|
||||
return (dispatch) => {
|
||||
const { url } = getExploreUrlAndPayload({ formData, endpointType: 'query' });
|
||||
return SupersetClient.post({
|
||||
url,
|
||||
postPayload: { form_data: formData },
|
||||
})
|
||||
return SupersetClient.get({ url })
|
||||
.then(({ json }) => {
|
||||
const redirectUrl = new URL(window.location);
|
||||
redirectUrl.pathname = '/superset/sqllab';
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
// This method transforms any BigNumber object in the given payload to its
|
||||
// 64-bit float representation. It is a temporary fix so charts receive
|
||||
// floats instead of BigNumber instances in their props to properly render.
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
export default function transform(payload) {
|
||||
if (!payload) {
|
||||
return payload;
|
||||
} else if (BigNumber.isBigNumber(payload)) {
|
||||
return payload.toNumber();
|
||||
} else if (payload.constructor === Object) {
|
||||
for (const key in payload) {
|
||||
if (payload.hasOwnProperty(key)) {
|
||||
// Modify in place to prevent creating large payloads
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
payload[key] = transform(payload[key]);
|
||||
}
|
||||
}
|
||||
} else if (payload.constructor === Array) {
|
||||
payload.forEach((elem, idx) => {
|
||||
// Modify in place to prevent creating large payloads
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
payload[idx] = transform(elem);
|
||||
});
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
@@ -83,15 +83,17 @@ class AsyncSelect extends React.PureComponent {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Select
|
||||
placeholder={this.props.placeholder}
|
||||
options={this.state.options}
|
||||
value={this.props.value}
|
||||
isLoading={this.state.isLoading}
|
||||
onChange={this.onChange}
|
||||
valueRenderer={this.props.valueRenderer}
|
||||
{...this.props}
|
||||
/>
|
||||
<div>
|
||||
<Select
|
||||
placeholder={this.props.placeholder}
|
||||
options={this.state.options}
|
||||
value={this.props.value}
|
||||
isLoading={this.state.isLoading}
|
||||
onChange={this.onChange}
|
||||
valueRenderer={this.props.valueRenderer}
|
||||
{...this.props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ export default class EditableTitle extends React.PureComponent {
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.handleBlur = this.handleBlur.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleKeyUp = this.handleKeyUp.bind(this);
|
||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||
|
||||
// Used so we can access the DOM element if a user clicks on this component.
|
||||
@@ -111,16 +112,21 @@ export default class EditableTitle extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
// this entire method exists to support using EditableTitle as the title of a
|
||||
// react-bootstrap Tab, as a workaround for this line in react-bootstrap https://goo.gl/ZVLmv4
|
||||
//
|
||||
// tl;dr when a Tab EditableTitle is being edited, typically the Tab it's within has been
|
||||
// clicked and is focused/active. for accessibility, when focused the Tab <a /> intercepts
|
||||
// the ' ' key (among others, including all arrows) and onChange() doesn't fire. somehow
|
||||
// keydown is still called so we can detect this and manually add a ' ' to the current title
|
||||
handleKeyDown(event) {
|
||||
if (event.key === ' ') {
|
||||
event.stopPropagation();
|
||||
handleKeyUp(ev) {
|
||||
// this entire method exists to support using EditableTitle as the title of a
|
||||
// react-bootstrap Tab, as a workaround for this line in react-bootstrap https://goo.gl/ZVLmv4
|
||||
//
|
||||
// tl;dr when a Tab EditableTitle is being edited, typically the Tab it's within has been
|
||||
// clicked and is focused/active. for accessibility, when focused the Tab <a /> intercepts
|
||||
// the ' ' key (among others, including all arrows) and onChange() doesn't fire. somehow
|
||||
// keydown is still called so we can detect this and manually add a ' ' to the current title
|
||||
if (ev.key === ' ') {
|
||||
let title = ev.target.value;
|
||||
const titleLength = (title || '').length;
|
||||
if (title && title[titleLength - 1] !== ' ') {
|
||||
title = `${title} `;
|
||||
this.setState(() => ({ title }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +170,7 @@ export default class EditableTitle extends React.PureComponent {
|
||||
required
|
||||
value={value}
|
||||
className={!title ? 'text-muted' : null}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
onChange={this.handleChange}
|
||||
onBlur={this.handleBlur}
|
||||
onClick={this.handleClick}
|
||||
@@ -178,7 +184,7 @@ export default class EditableTitle extends React.PureComponent {
|
||||
type={isEditing ? 'text' : 'button'}
|
||||
value={value}
|
||||
className={!title ? 'text-muted' : null}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
onChange={this.handleChange}
|
||||
onBlur={this.handleBlur}
|
||||
onClick={this.handleClick}
|
||||
|
||||
@@ -17,22 +17,8 @@
|
||||
* under the License.
|
||||
*/
|
||||
.TableSelector .fa-refresh {
|
||||
padding-left: 9px;
|
||||
padding-top: 7px
|
||||
}
|
||||
.TableSelector .refresh-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
}
|
||||
.TableSelector .section {
|
||||
padding-bottom: 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.TableSelector .select {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.TableSelector .divider {
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
margin: 10px 0;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select from 'react-virtualized-select';
|
||||
import createFilterOptions from 'react-select-fast-filter-options';
|
||||
import { ControlLabel, Label } from 'react-bootstrap';
|
||||
import { ControlLabel, Col, Label, Row } from 'react-bootstrap';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { SupersetClient } from '@superset-ui/connection';
|
||||
|
||||
@@ -38,6 +38,7 @@ const propTypes = {
|
||||
tableNameSticky: PropTypes.bool,
|
||||
tableName: PropTypes.string,
|
||||
database: PropTypes.object,
|
||||
horizontal: PropTypes.bool,
|
||||
sqlLabMode: PropTypes.bool,
|
||||
onChange: PropTypes.func,
|
||||
clearable: PropTypes.bool,
|
||||
@@ -51,6 +52,7 @@ const defaultProps = {
|
||||
onTableChange: () => {},
|
||||
onChange: () => {},
|
||||
tableNameSticky: true,
|
||||
horizontal: false,
|
||||
sqlLabMode: true,
|
||||
clearable: true,
|
||||
};
|
||||
@@ -111,11 +113,7 @@ export default class TableSelector extends React.PureComponent {
|
||||
if (data.result.length === 0) {
|
||||
this.props.handleError(t("It seems you don't have access to any database"));
|
||||
}
|
||||
return data.result.map(row => ({
|
||||
...row,
|
||||
// label is used for the typeahead
|
||||
label: `${row.backend} ${row.database_name}`,
|
||||
}));
|
||||
return data.result;
|
||||
}
|
||||
fetchTables(force, substr) {
|
||||
// This can be large so it shouldn't be put in the Redux store
|
||||
@@ -201,10 +199,10 @@ export default class TableSelector extends React.PureComponent {
|
||||
}
|
||||
renderSelectRow(select, refreshBtn) {
|
||||
return (
|
||||
<div className="section">
|
||||
<span className="select">{select}</span>
|
||||
<span className="refresh-col">{refreshBtn}</span>
|
||||
</div>
|
||||
<Row>
|
||||
<Col md={11}>{select}</Col>
|
||||
<Col md={1} className="refresh-col">{refreshBtn}</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
renderDatabaseSelect() {
|
||||
@@ -234,25 +232,29 @@ export default class TableSelector extends React.PureComponent {
|
||||
/>);
|
||||
}
|
||||
renderSchema() {
|
||||
return this.renderSelectRow(
|
||||
<Select
|
||||
name="select-schema"
|
||||
placeholder={t('Select a schema (%s)', this.state.schemaOptions.length)}
|
||||
options={this.state.schemaOptions}
|
||||
value={this.props.schema}
|
||||
valueRenderer={o => (
|
||||
<div>
|
||||
<span className="text-muted">{t('Schema:')}</span> {o.label}
|
||||
</div>
|
||||
return (
|
||||
<div className="m-t-5">
|
||||
{this.renderSelectRow(
|
||||
<Select
|
||||
name="select-schema"
|
||||
placeholder={t('Select a schema (%s)', this.state.schemaOptions.length)}
|
||||
options={this.state.schemaOptions}
|
||||
value={this.props.schema}
|
||||
valueRenderer={o => (
|
||||
<div>
|
||||
<span className="text-muted">{t('Schema:')}</span> {o.label}
|
||||
</div>
|
||||
)}
|
||||
isLoading={this.state.schemaLoading}
|
||||
autosize={false}
|
||||
onChange={this.changeSchema}
|
||||
/>,
|
||||
<RefreshLabel
|
||||
onClick={() => this.onDatabaseChange({ id: this.props.dbId }, true)}
|
||||
tooltipContent={t('Force refresh schema list')}
|
||||
/>,
|
||||
)}
|
||||
isLoading={this.state.schemaLoading}
|
||||
autosize={false}
|
||||
onChange={this.changeSchema}
|
||||
/>,
|
||||
<RefreshLabel
|
||||
onClick={() => this.onDatabaseChange({ id: this.props.dbId }, true)}
|
||||
tooltipContent={t('Force refresh schema list')}
|
||||
/>,
|
||||
</div>
|
||||
);
|
||||
}
|
||||
renderTable() {
|
||||
@@ -288,16 +290,20 @@ export default class TableSelector extends React.PureComponent {
|
||||
value={this.state.tableName}
|
||||
loadOptions={this.getTableNamesBySubStr}
|
||||
/>);
|
||||
return this.renderSelectRow(
|
||||
select,
|
||||
<RefreshLabel
|
||||
onClick={() => this.changeSchema({ value: this.props.schema }, true)}
|
||||
tooltipContent={t('Force refresh table list')}
|
||||
/>);
|
||||
return (
|
||||
<div className="m-t-5">
|
||||
{this.renderSelectRow(
|
||||
select,
|
||||
<RefreshLabel
|
||||
onClick={() => this.changeSchema({ value: this.props.schema }, true)}
|
||||
tooltipContent={t('Force refresh table list')}
|
||||
/>)}
|
||||
</div>);
|
||||
}
|
||||
renderSeeTableLabel() {
|
||||
return (
|
||||
<div className="section">
|
||||
<div>
|
||||
<hr />
|
||||
<ControlLabel>
|
||||
{t('See table schema')}{' '}
|
||||
<small>
|
||||
@@ -313,11 +319,18 @@ export default class TableSelector extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<div className="TableSelector">
|
||||
{this.renderDatabaseSelect()}
|
||||
{this.renderSchema()}
|
||||
<div className="divider" />
|
||||
{this.props.sqlLabMode && this.renderSeeTableLabel()}
|
||||
{this.renderTable()}
|
||||
{this.props.horizontal ?
|
||||
<div>
|
||||
<Col md={4}>{this.renderDatabaseSelect()}</Col>
|
||||
<Col md={4}>{this.renderSchema()}</Col>
|
||||
<Col md={4}>{this.renderTable()}</Col>
|
||||
</div> :
|
||||
<div>
|
||||
<div>{this.renderDatabaseSelect()}</div>
|
||||
<div className="m-t-5">{this.renderSchema()}</div>
|
||||
{this.props.sqlLabMode && this.renderSeeTableLabel()}
|
||||
<div className="m-t-5">{this.renderTable()}</div>
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ export default function VirtualizedRendererWrap(renderer) {
|
||||
key={key}
|
||||
style={{ ...(option.style || {}), ...style }}
|
||||
title={option.title}
|
||||
data-test={option.optionName}
|
||||
{...events}
|
||||
>
|
||||
{renderer(option)}
|
||||
|
||||
@@ -253,10 +253,7 @@ export function addSliceToDashboard(id) {
|
||||
),
|
||||
);
|
||||
}
|
||||
const form_data = {
|
||||
...selectedSlice.form_data,
|
||||
slice_id: selectedSlice.slice_id,
|
||||
};
|
||||
const form_data = selectedSlice.form_data;
|
||||
const newChart = {
|
||||
...initChart,
|
||||
id,
|
||||
|
||||
@@ -448,14 +448,6 @@ export class DatasourceEditor extends React.PureComponent {
|
||||
label={t('Hours offset')}
|
||||
control={<TextControl />}
|
||||
/>
|
||||
{ this.state.isSqla &&
|
||||
<Field
|
||||
fieldKey="template_params"
|
||||
label={t('Template parameters')}
|
||||
descr={t('A set of parameters that become available in the query using Jinja templating syntax')}
|
||||
control={<TextControl />}
|
||||
/>
|
||||
}
|
||||
</Fieldset>);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,8 +49,10 @@ class DatasourceModal extends React.PureComponent {
|
||||
super(props);
|
||||
this.state = {
|
||||
errors: [],
|
||||
showDatasource: false,
|
||||
datasource: props.datasource,
|
||||
};
|
||||
this.toggleShowDatasource = this.toggleShowDatasource.bind(this);
|
||||
this.setSearchRef = this.setSearchRef.bind(this);
|
||||
this.onDatasourceChange = this.onDatasourceChange.bind(this);
|
||||
this.onClickSave = this.onClickSave.bind(this);
|
||||
@@ -109,6 +111,10 @@ class DatasourceModal extends React.PureComponent {
|
||||
this.dialog = ref;
|
||||
}
|
||||
|
||||
toggleShowDatasource() {
|
||||
this.setState({ showDatasource: !this.state.showDatasource });
|
||||
}
|
||||
|
||||
renderSaveDialog() {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -19,19 +19,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Col,
|
||||
Collapse,
|
||||
Label,
|
||||
OverlayTrigger,
|
||||
Row,
|
||||
Tooltip,
|
||||
Well,
|
||||
} from 'react-bootstrap';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import ColumnOption from '../../../components/ColumnOption';
|
||||
import MetricOption from '../../../components/MetricOption';
|
||||
import DatasourceModal from '../../../datasource/DatasourceModal';
|
||||
|
||||
const propTypes = {
|
||||
@@ -58,51 +52,25 @@ class DatasourceControl extends React.PureComponent {
|
||||
};
|
||||
this.toggleShowDatasource = this.toggleShowDatasource.bind(this);
|
||||
this.toggleEditDatasourceModal = this.toggleEditDatasourceModal.bind(this);
|
||||
this.renderDatasource = this.renderDatasource.bind(this);
|
||||
}
|
||||
|
||||
onChange(vizType) {
|
||||
this.props.onChange(vizType);
|
||||
this.setState({ showModal: false });
|
||||
}
|
||||
|
||||
toggleShowDatasource() {
|
||||
this.setState(({ showDatasource }) => ({ showDatasource: !showDatasource }));
|
||||
}
|
||||
|
||||
toggleModal() {
|
||||
this.setState(({ showModal }) => ({ showModal: !showModal }));
|
||||
}
|
||||
toggleEditDatasourceModal() {
|
||||
this.setState(({ showEditDatasourceModal }) => ({
|
||||
showEditDatasourceModal: !showEditDatasourceModal,
|
||||
}));
|
||||
}
|
||||
renderDatasource() {
|
||||
const datasource = this.props.datasource;
|
||||
return (
|
||||
<div className="m-t-10">
|
||||
<Well className="m-t-0">
|
||||
<div className="m-b-10">
|
||||
<Label>
|
||||
<i className="fa fa-database" /> {datasource.database.backend}
|
||||
</Label>
|
||||
{` ${datasource.database.name} `}
|
||||
</div>
|
||||
<Row className="datasource-container">
|
||||
<Col md={6}>
|
||||
<strong>Columns</strong>
|
||||
{datasource.columns.map(col => (
|
||||
<div key={col.column_name}>
|
||||
<ColumnOption showType column={col} />
|
||||
</div>
|
||||
))}
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
<strong>Metrics</strong>
|
||||
{datasource.metrics.map(m => (
|
||||
<div key={m.metric_name}>
|
||||
<MetricOption metric={m} showType />
|
||||
</div>
|
||||
))}
|
||||
</Col>
|
||||
</Row>
|
||||
</Well>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
@@ -117,21 +85,6 @@ class DatasourceControl extends React.PureComponent {
|
||||
{this.props.datasource.name}
|
||||
</Label>
|
||||
</OverlayTrigger>
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={
|
||||
<Tooltip id={'toggle-datasource-tooltip'}>
|
||||
{t('Expand/collapse datasource configuration')}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<a href="#">
|
||||
<i
|
||||
className={`fa fa-${this.state.showDatasource ? 'minus' : 'plus'}-square m-r-5`}
|
||||
onClick={this.toggleShowDatasource}
|
||||
/>
|
||||
</a>
|
||||
</OverlayTrigger>
|
||||
{this.props.datasource.type === 'table' &&
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
@@ -149,7 +102,6 @@ class DatasourceControl extends React.PureComponent {
|
||||
<i className="fa fa-flask m-r-5" />
|
||||
</a>
|
||||
</OverlayTrigger>}
|
||||
<Collapse in={this.state.showDatasource}>{this.renderDatasource()}</Collapse>
|
||||
<DatasourceModal
|
||||
datasource={this.props.datasource}
|
||||
show={this.state.showEditDatasourceModal}
|
||||
|
||||
@@ -60,21 +60,6 @@ function isDictionaryForAdhocMetric(value) {
|
||||
return value && !(value instanceof AdhocMetric) && value.expressionType;
|
||||
}
|
||||
|
||||
function columnsContainAllMetrics(value, nextProps) {
|
||||
const columnNames = new Set(
|
||||
[...nextProps.columns, ...nextProps.savedMetrics]
|
||||
// eslint-disable-next-line camelcase
|
||||
.map(({ column_name, metric_name }) => (column_name || metric_name)),
|
||||
);
|
||||
|
||||
return (Array.isArray(value) ? value : [value])
|
||||
.filter(metric => metric)
|
||||
// find column names
|
||||
.map(metric => metric.column ? metric.column.column_name : metric.column_name || metric)
|
||||
.filter(name => name)
|
||||
.every(name => columnNames.has(name));
|
||||
}
|
||||
|
||||
// adhoc metrics are stored as dictionaries in URL params. We convert them back into the
|
||||
// AdhocMetric class for typechecking, consistency and instance method access.
|
||||
function coerceAdhocMetrics(value) {
|
||||
@@ -150,22 +135,14 @@ export default class MetricsControl extends React.PureComponent {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { value } = this.props;
|
||||
if (
|
||||
!isEqual(this.props.columns, nextProps.columns) ||
|
||||
!isEqual(this.props.savedMetrics, nextProps.savedMetrics)
|
||||
isEqual(this.props.columns) !== isEqual(nextProps.columns) ||
|
||||
isEqual(this.props.savedMetrics) !== isEqual(nextProps.savedMetrics)
|
||||
) {
|
||||
|
||||
this.setState({ options: this.optionsForSelect(nextProps) });
|
||||
|
||||
// Remove metrics if selected value no longer a column
|
||||
const containsAllMetrics = columnsContainAllMetrics(value, nextProps);
|
||||
|
||||
if (!containsAllMetrics) {
|
||||
this.props.onChange([]);
|
||||
}
|
||||
this.props.onChange([]);
|
||||
}
|
||||
if (value !== nextProps.value) {
|
||||
if (this.props.value !== nextProps.value) {
|
||||
this.setState({ value: coerceAdhocMetrics(nextProps.value) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,43 +22,16 @@ import {
|
||||
Row, Col, FormControl, OverlayTrigger, Popover,
|
||||
} from 'react-bootstrap';
|
||||
import Select from 'react-select';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import InfoTooltipWithTrigger from '../../../components/InfoTooltipWithTrigger';
|
||||
import BoundsControl from './BoundsControl';
|
||||
import CheckboxControl from './CheckboxControl';
|
||||
|
||||
const propTypes = {
|
||||
label: PropTypes.string,
|
||||
tooltip: PropTypes.string,
|
||||
colType: PropTypes.string,
|
||||
width: PropTypes.string,
|
||||
height: PropTypes.string,
|
||||
timeLag: PropTypes.string,
|
||||
timeRatio: PropTypes.string,
|
||||
comparisonType: PropTypes.string,
|
||||
showYAxis: PropTypes.bool,
|
||||
yAxisBounds: PropTypes.array,
|
||||
bounds: PropTypes.array,
|
||||
d3format: PropTypes.string,
|
||||
dateFormat: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
label: t('Time Series Columns'),
|
||||
tooltip: '',
|
||||
colType: '',
|
||||
width: '',
|
||||
height: '',
|
||||
timeLag: '',
|
||||
timeRatio: '',
|
||||
comparisonType: '',
|
||||
showYAxis: false,
|
||||
yAxisBounds: [null, null],
|
||||
bounds: [null, null],
|
||||
d3format: '',
|
||||
dateFormat: '',
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
@@ -79,21 +52,7 @@ const colTypeOptions = [
|
||||
export default class TimeSeriesColumnControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const state = {
|
||||
label: this.props.label,
|
||||
tooltip: this.props.tooltip,
|
||||
colType: this.props.colType,
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
timeLag: this.props.timeLag,
|
||||
timeRatio: this.props.timeRatio,
|
||||
comparisonType: this.props.comparisonType,
|
||||
showYAxis: this.props.showYAxis,
|
||||
yAxisBounds: this.props.yAxisBounds,
|
||||
bounds: this.props.bounds,
|
||||
d3format: this.props.d3format,
|
||||
dateFormat: this.props.dateFormat,
|
||||
};
|
||||
const state = { ...props };
|
||||
delete state.onChange;
|
||||
this.state = state;
|
||||
this.onChange = this.onChange.bind(this);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
// For individual deployments to add custom overrides
|
||||
export default function extraOverrides(controlPanelConfigs) {
|
||||
return controlPanelConfigs;
|
||||
}
|
||||
@@ -22,7 +22,6 @@
|
||||
*/
|
||||
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
||||
import * as sections from './sections';
|
||||
import extraOverrides from './extraOverrides';
|
||||
|
||||
import Area from './Area';
|
||||
import Bar from './Bar';
|
||||
@@ -73,7 +72,7 @@ import DeckPolygon from './DeckPolygon';
|
||||
import DeckScatter from './DeckScatter';
|
||||
import DeckScreengrid from './DeckScreengrid';
|
||||
|
||||
export const controlPanelConfigs = extraOverrides({
|
||||
export const controlPanelConfigs = {
|
||||
area: Area,
|
||||
bar: Bar,
|
||||
big_number: BigNumber,
|
||||
@@ -123,7 +122,7 @@ export const controlPanelConfigs = extraOverrides({
|
||||
deck_scatter: DeckScatter,
|
||||
deck_screengrid: DeckScreengrid,
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
export default controlPanelConfigs;
|
||||
|
||||
|
||||
@@ -2311,7 +2311,7 @@ export const controls = {
|
||||
type: 'CollectionControl',
|
||||
label: 'Filters',
|
||||
description: t('Filter configuration for the filter box'),
|
||||
validators: [],
|
||||
validators: [v.nonEmpty],
|
||||
controlName: 'FilterBoxItemControl',
|
||||
mapStateToProps: ({ datasource }) => ({ datasource }),
|
||||
},
|
||||
|
||||
@@ -23,7 +23,6 @@ import { scaleLinear } from 'd3-scale';
|
||||
import { Table, Thead, Th, Tr, Td } from 'reactable-arc';
|
||||
import { formatNumber } from '@superset-ui/number-format';
|
||||
import { formatTime } from '@superset-ui/time-format';
|
||||
import moment from 'moment';
|
||||
|
||||
import MetricOption from '../../components/MetricOption';
|
||||
import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
|
||||
@@ -149,7 +148,7 @@ class TimeTable extends React.PureComponent {
|
||||
renderTooltip={({ index }) => (
|
||||
<div>
|
||||
<strong>{formatNumber(column.d3format, sparkData[index])}</strong>
|
||||
<div>{formatTime(column.dateFormat, moment.utc(entries[index].time).toDate())}</div>
|
||||
<div>{formatTime(column.dateFormat, entries[index].time)}</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -31,9 +31,9 @@ import { fitViewport } from './layers/common';
|
||||
const { getScale } = CategoricalColorNamespace;
|
||||
|
||||
function getCategories(fd, data) {
|
||||
const c = fd.colorPicker || { r: 0, g: 0, b: 0, a: 1 };
|
||||
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
|
||||
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
|
||||
const colorFn = getScale(fd.colorScheme);
|
||||
const colorFn = getScale(fd.color_scheme);
|
||||
const categories = {};
|
||||
data.forEach((d) => {
|
||||
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
|
||||
@@ -108,7 +108,7 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
|
||||
// the granularity has to be read from the payload form_data, not the
|
||||
// props formData which comes from the instantaneous controls state
|
||||
const granularity = (
|
||||
props.payload.form_data.timeGrainSqla ||
|
||||
props.payload.form_data.time_grain_sqla ||
|
||||
props.payload.form_data.granularity ||
|
||||
'P1D'
|
||||
);
|
||||
@@ -154,8 +154,8 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
|
||||
features = this.addColor(features, fd);
|
||||
|
||||
// Apply user defined data mutator if defined
|
||||
if (fd.jsDataMutator) {
|
||||
const jsFnMutator = sandboxedEval(fd.jsDataMutator);
|
||||
if (fd.js_data_mutator) {
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
features = jsFnMutator(features);
|
||||
}
|
||||
|
||||
@@ -180,8 +180,8 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
|
||||
return [getLayer(fd, filteredPayload, onAddFilter, setTooltip)];
|
||||
}
|
||||
addColor(data, fd) {
|
||||
const c = fd.colorPicker || { r: 0, g: 0, b: 0, a: 1 };
|
||||
const colorFn = getScale(fd.colorScheme);
|
||||
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
|
||||
const colorFn = getScale(fd.color_scheme);
|
||||
return data.map((d) => {
|
||||
let color;
|
||||
if (fd.dimension) {
|
||||
@@ -229,14 +229,14 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
|
||||
viewport={this.state.viewport}
|
||||
onViewportChange={this.onViewportChange}
|
||||
mapboxApiAccessToken={this.props.mapboxApiKey}
|
||||
mapStyle={this.props.formData.mapboxStyle}
|
||||
mapStyle={this.props.formData.mapbox_style}
|
||||
setControlValue={this.props.setControlValue}
|
||||
>
|
||||
<Legend
|
||||
categories={this.state.categories}
|
||||
toggleCategory={this.toggleCategory}
|
||||
showSingleCategory={this.showSingleCategory}
|
||||
position={this.props.formData.legendPosition}
|
||||
position={this.props.formData.legend_position}
|
||||
/>
|
||||
</AnimatableDeckGLContainer>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@ class DeckMulti extends React.PureComponent {
|
||||
const filters = [
|
||||
...(subslice.form_data.filters || []),
|
||||
...(formData.filters || []),
|
||||
...(formData.extraFilters || []),
|
||||
...(formData.extra_filters || []),
|
||||
];
|
||||
const subsliceCopy = {
|
||||
...subslice,
|
||||
@@ -70,7 +70,7 @@ class DeckMulti extends React.PureComponent {
|
||||
endpoint: getExploreLongUrl(subsliceCopy.form_data, 'json'),
|
||||
})
|
||||
.then(({ json }) => {
|
||||
const layer = layerGenerators[subsliceCopy.form_data.vizType](
|
||||
const layer = layerGenerators[subsliceCopy.form_data.viz_type](
|
||||
subsliceCopy.form_data,
|
||||
json,
|
||||
);
|
||||
@@ -96,7 +96,7 @@ class DeckMulti extends React.PureComponent {
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport}
|
||||
layers={layers}
|
||||
mapStyle={formData.mapboxStyle}
|
||||
mapStyle={formData.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -90,7 +90,7 @@ export function createDeckGLComponent(getLayer, getPoints) {
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport}
|
||||
layers={[layer]}
|
||||
mapStyle={formData.mapboxStyle}
|
||||
mapStyle={formData.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
onViewportChange={this.onViewportChange}
|
||||
/>);
|
||||
|
||||
@@ -31,14 +31,14 @@ function getPoints(data) {
|
||||
|
||||
export function getLayer(fd, payload, onAddFilter, setTooltip) {
|
||||
const data = payload.data.features;
|
||||
const sc = fd.colorPicker;
|
||||
const tc = fd.targetColorPicker;
|
||||
const sc = fd.color_picker;
|
||||
const tc = fd.target_color_picker;
|
||||
return new ArcLayer({
|
||||
id: `path-layer-${fd.sliceId}`,
|
||||
id: `path-layer-${fd.slice_id}`,
|
||||
data,
|
||||
getSourceColor: d => d.sourceColor || d.color || [sc.r, sc.g, sc.b, 255 * sc.a],
|
||||
getTargetColor: d => d.targetColor || d.color || [tc.r, tc.g, tc.b, 255 * tc.a],
|
||||
strokeWidth: (fd.strokeWidth) ? fd.strokeWidth : 3,
|
||||
strokeWidth: (fd.stroke_width) ? fd.stroke_width : 3,
|
||||
...commonLayerProps(fd, setTooltip),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,8 +77,8 @@ const recurseGeoJson = (node, propOverrides, extraProps) => {
|
||||
|
||||
export function getLayer(formData, payload, onAddFilter, setTooltip) {
|
||||
const fd = formData;
|
||||
const fc = fd.fillColorPicker;
|
||||
const sc = fd.strokeColorPicker;
|
||||
const fc = fd.fill_color_picker;
|
||||
const sc = fd.stroke_color_picker;
|
||||
const fillColor = [fc.r, fc.g, fc.b, 255 * fc.a];
|
||||
const strokeColor = [sc.r, sc.g, sc.b, 255 * sc.a];
|
||||
const propOverrides = {};
|
||||
@@ -93,19 +93,19 @@ export function getLayer(formData, payload, onAddFilter, setTooltip) {
|
||||
recurseGeoJson(payload.data, propOverrides);
|
||||
|
||||
let jsFnMutator;
|
||||
if (fd.jsDataMutator) {
|
||||
if (fd.js_data_mutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
jsFnMutator = sandboxedEval(fd.jsDataMutator);
|
||||
jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
features = jsFnMutator(features);
|
||||
}
|
||||
|
||||
return new GeoJsonLayer({
|
||||
id: `geojson-layer-${fd.sliceId}`,
|
||||
id: `geojson-layer-${fd.slice_id}`,
|
||||
filled: fd.filled,
|
||||
data: features,
|
||||
stroked: fd.stroked,
|
||||
extruded: fd.extruded,
|
||||
pointRadiusScale: fd.pointRadiusScale,
|
||||
pointRadiusScale: fd.point_radius_scale,
|
||||
...commonLayerProps(fd, setTooltip),
|
||||
});
|
||||
}
|
||||
@@ -145,7 +145,7 @@ function deckGeoJson(props) {
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport}
|
||||
layers={[layer]}
|
||||
mapStyle={formData.mapboxStyle}
|
||||
mapStyle={formData.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -24,24 +24,24 @@ import { createDeckGLComponent } from '../../factory';
|
||||
|
||||
export function getLayer(formData, payload, onAddFilter, setTooltip) {
|
||||
const fd = formData;
|
||||
const c = fd.colorPicker;
|
||||
const c = fd.color_picker;
|
||||
let data = payload.data.features.map(d => ({
|
||||
...d,
|
||||
color: [c.r, c.g, c.b, 255 * c.a],
|
||||
}));
|
||||
|
||||
if (fd.jsDataMutator) {
|
||||
if (fd.js_data_mutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutator = sandboxedEval(fd.jsDataMutator);
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
|
||||
const aggFunc = getAggFunc(fd.jsAggFunction, p => p.weight);
|
||||
const aggFunc = getAggFunc(fd.js_agg_function, p => p.weight);
|
||||
return new GridLayer({
|
||||
id: `grid-layer-${fd.sliceId}`,
|
||||
id: `grid-layer-${fd.slice_id}`,
|
||||
data,
|
||||
pickable: true,
|
||||
cellSize: fd.gridSize,
|
||||
cellSize: fd.grid_size,
|
||||
minColor: [0, 0, 0, 0],
|
||||
extruded: fd.extruded,
|
||||
maxColor: [c.r, c.g, c.b, 255 * c.a],
|
||||
|
||||
@@ -24,23 +24,23 @@ import { createDeckGLComponent } from '../../factory';
|
||||
|
||||
export function getLayer(formData, payload, onAddFilter, setTooltip) {
|
||||
const fd = formData;
|
||||
const c = fd.colorPicker;
|
||||
const c = fd.color_picker;
|
||||
let data = payload.data.features.map(d => ({
|
||||
...d,
|
||||
color: [c.r, c.g, c.b, 255 * c.a],
|
||||
}));
|
||||
|
||||
if (fd.jsDataMutator) {
|
||||
if (fd.js_data_mutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutator = sandboxedEval(fd.jsDataMutator);
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
const aggFunc = getAggFunc(fd.jsAggFunction, p => p.weight);
|
||||
const aggFunc = getAggFunc(fd.js_agg_function, p => p.weight);
|
||||
return new HexagonLayer({
|
||||
id: `hex-layer-${fd.sliceId}`,
|
||||
id: `hex-layer-${fd.slice_id}`,
|
||||
data,
|
||||
pickable: true,
|
||||
radius: fd.gridSize,
|
||||
radius: fd.grid_size,
|
||||
minColor: [0, 0, 0, 0],
|
||||
extruded: fd.extruded,
|
||||
maxColor: [c.r, c.g, c.b, 255 * c.a],
|
||||
|
||||
@@ -23,22 +23,22 @@ import { createDeckGLComponent } from '../../factory';
|
||||
|
||||
export function getLayer(formData, payload, onAddFilter, setTooltip) {
|
||||
const fd = formData;
|
||||
const c = fd.colorPicker;
|
||||
const c = fd.color_picker;
|
||||
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
|
||||
let data = payload.data.features.map(feature => ({
|
||||
...feature,
|
||||
path: feature.path,
|
||||
width: fd.lineWidth,
|
||||
width: fd.line_width,
|
||||
color: fixedColor,
|
||||
}));
|
||||
|
||||
if (fd.jsDataMutator) {
|
||||
const jsFnMutator = sandboxedEval(fd.jsDataMutator);
|
||||
if (fd.js_data_mutator) {
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
|
||||
return new PathLayer({
|
||||
id: `path-layer-${fd.sliceId}`,
|
||||
id: `path-layer-${fd.slice_id}`,
|
||||
data,
|
||||
rounded: true,
|
||||
widthScale: 1,
|
||||
|
||||
@@ -50,8 +50,8 @@ function getElevation(d, colorScaler) {
|
||||
|
||||
export function getLayer(formData, payload, setTooltip, selected, onSelect, filters) {
|
||||
const fd = formData;
|
||||
const fc = fd.fillColorPicker;
|
||||
const sc = fd.strokeColorPicker;
|
||||
const fc = fd.fill_color_picker;
|
||||
const sc = fd.stroke_color_picker;
|
||||
let data = [...payload.data.features];
|
||||
|
||||
if (filters != null) {
|
||||
@@ -60,9 +60,9 @@ export function getLayer(formData, payload, setTooltip, selected, onSelect, filt
|
||||
});
|
||||
}
|
||||
|
||||
if (fd.jsDataMutator) {
|
||||
if (fd.js_data_mutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutator = sandboxedEval(fd.jsDataMutator);
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
|
||||
@@ -76,13 +76,13 @@ export function getLayer(formData, payload, setTooltip, selected, onSelect, filt
|
||||
// when polygons are selected, reduce the opacity of non-selected polygons
|
||||
const colorScaler = (d) => {
|
||||
const baseColor = baseColorScaler(d);
|
||||
if (selected.length > 0 && selected.indexOf(d[fd.lineColumn]) === -1) {
|
||||
if (selected.length > 0 && selected.indexOf(d[fd.line_column]) === -1) {
|
||||
baseColor[3] /= 2;
|
||||
}
|
||||
return baseColor;
|
||||
};
|
||||
return new PolygonLayer({
|
||||
id: `path-layer-${fd.sliceId}`,
|
||||
id: `path-layer-${fd.slice_id}`,
|
||||
data,
|
||||
pickable: true,
|
||||
filled: fd.filled,
|
||||
@@ -90,7 +90,7 @@ export function getLayer(formData, payload, setTooltip, selected, onSelect, filt
|
||||
getPolygon: d => d.polygon,
|
||||
getFillColor: colorScaler,
|
||||
getLineColor: [sc.r, sc.g, sc.b, 255 * sc.a],
|
||||
getLineWidth: fd.lineWidth,
|
||||
getLineWidth: fd.line_width,
|
||||
extruded: fd.extruded,
|
||||
getElevation: d => getElevation(d, colorScaler),
|
||||
elevationScale: fd.multiplier,
|
||||
@@ -138,7 +138,7 @@ class DeckGLPolygon extends React.Component {
|
||||
// the granularity has to be read from the payload form_data, not the
|
||||
// props formData which comes from the instantaneous controls state
|
||||
const granularity = (
|
||||
props.payload.form_data.timeGrainSqla ||
|
||||
props.payload.form_data.time_grain_sqla ||
|
||||
props.payload.form_data.granularity ||
|
||||
'P1D'
|
||||
);
|
||||
@@ -177,7 +177,7 @@ class DeckGLPolygon extends React.Component {
|
||||
const selected = [...this.state.selected];
|
||||
if (doubleClick) {
|
||||
selected.splice(0, selected.length, polygon);
|
||||
} else if (formData.togglePolygons) {
|
||||
} else if (formData.toggle_polygons) {
|
||||
const i = selected.indexOf(polygon);
|
||||
if (i === -1) {
|
||||
selected.push(polygon);
|
||||
@@ -189,8 +189,8 @@ class DeckGLPolygon extends React.Component {
|
||||
}
|
||||
|
||||
this.setState({ selected, lastClick: now });
|
||||
if (formData.tableFilter) {
|
||||
onAddFilter(formData.lineColumn, selected, false, true);
|
||||
if (formData.table_filter) {
|
||||
onAddFilter(formData.line_column, selected, false, true);
|
||||
}
|
||||
}
|
||||
onValuesChange(values) {
|
||||
@@ -249,14 +249,14 @@ class DeckGLPolygon extends React.Component {
|
||||
viewport={viewport}
|
||||
onViewportChange={this.onViewportChange}
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
mapStyle={formData.mapboxStyle}
|
||||
mapStyle={formData.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
aggregation
|
||||
>
|
||||
{formData.metric !== null &&
|
||||
<Legend
|
||||
categories={buckets}
|
||||
position={formData.legendPosition}
|
||||
position={formData.legend_position}
|
||||
/>}
|
||||
</AnimatableDeckGLContainer>
|
||||
</div>
|
||||
|
||||
@@ -27,24 +27,24 @@ function getPoints(data) {
|
||||
|
||||
export function getLayer(fd, payload, onAddFilter, setTooltip) {
|
||||
const dataWithRadius = payload.data.features.map((d) => {
|
||||
let radius = unitToRadius(fd.pointUnit, d.radius) || 10;
|
||||
let radius = unitToRadius(fd.point_unit, d.radius) || 10;
|
||||
if (fd.multiplier) {
|
||||
radius *= fd.multiplier;
|
||||
}
|
||||
if (d.color) {
|
||||
return { ...d, radius };
|
||||
}
|
||||
const c = fd.colorPicker || { r: 0, g: 0, b: 0, a: 1 };
|
||||
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
|
||||
const color = [c.r, c.g, c.b, c.a * 255];
|
||||
return { ...d, radius, color };
|
||||
});
|
||||
|
||||
return new ScatterplotLayer({
|
||||
id: `scatter-layer-${fd.sliceId}`,
|
||||
id: `scatter-layer-${fd.slice_id}`,
|
||||
data: dataWithRadius,
|
||||
fp64: true,
|
||||
radiusMinPixels: fd.minRadius || null,
|
||||
radiusMaxPixels: fd.maxRadius || null,
|
||||
radiusMinPixels: fd.min_radius || null,
|
||||
radiusMaxPixels: fd.max_radius || null,
|
||||
outline: false,
|
||||
...commonLayerProps(fd, setTooltip),
|
||||
});
|
||||
|
||||
@@ -32,15 +32,15 @@ function getPoints(data) {
|
||||
|
||||
export function getLayer(formData, payload, onAddFilter, setTooltip, filters) {
|
||||
const fd = formData;
|
||||
const c = fd.colorPicker;
|
||||
const c = fd.color_picker;
|
||||
let data = payload.data.features.map(d => ({
|
||||
...d,
|
||||
color: [c.r, c.g, c.b, 255 * c.a],
|
||||
}));
|
||||
|
||||
if (fd.jsDataMutator) {
|
||||
if (fd.js_data_mutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutator = sandboxedEval(fd.jsDataMutator);
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
|
||||
@@ -53,10 +53,10 @@ export function getLayer(formData, payload, onAddFilter, setTooltip, filters) {
|
||||
// Passing a layer creator function instead of a layer since the
|
||||
// layer needs to be regenerated at each render
|
||||
return new ScreenGridLayer({
|
||||
id: `screengrid-layer-${fd.sliceId}`,
|
||||
id: `screengrid-layer-${fd.slice_id}`,
|
||||
data,
|
||||
pickable: true,
|
||||
cellSizePixels: fd.gridSize,
|
||||
cellSizePixels: fd.grid_size,
|
||||
minColor: [c.r, c.g, c.b, 0],
|
||||
maxColor: [c.r, c.g, c.b, 255 * c.a],
|
||||
outline: false,
|
||||
@@ -102,7 +102,7 @@ class DeckGLScreenGrid extends React.PureComponent {
|
||||
// the granularity has to be read from the payload form_data, not the
|
||||
// props formData which comes from the instantaneous controls state
|
||||
const granularity = (
|
||||
props.payload.form_data.timeGrainSqla ||
|
||||
props.payload.form_data.time_grain_sqla ||
|
||||
props.payload.form_data.granularity ||
|
||||
'P1D'
|
||||
);
|
||||
@@ -176,7 +176,7 @@ class DeckGLScreenGrid extends React.PureComponent {
|
||||
viewport={this.state.viewport}
|
||||
onViewportChange={this.onViewportChange}
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
mapStyle={formData.mapboxStyle}
|
||||
mapStyle={formData.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
aggregation
|
||||
/>
|
||||
|
||||
@@ -54,13 +54,13 @@ export function commonLayerProps(formData, setTooltip, onSelect) {
|
||||
const fd = formData;
|
||||
let onHover;
|
||||
let tooltipContentGenerator;
|
||||
if (fd.jsTooltip) {
|
||||
tooltipContentGenerator = sandboxedEval(fd.jsTooltip);
|
||||
} else if (fd.lineColumn && fd.metric && ['geohash', 'zipcode'].indexOf(fd.lineType) >= 0) {
|
||||
if (fd.js_tooltip) {
|
||||
tooltipContentGenerator = sandboxedEval(fd.js_tooltip);
|
||||
} else if (fd.line_column && fd.metric && ['geohash', 'zipcode'].indexOf(fd.line_type) >= 0) {
|
||||
const metricLabel = fd.metric.label || fd.metric;
|
||||
tooltipContentGenerator = o => (
|
||||
<div>
|
||||
<div>{fd.lineColumn}: <strong>{o.object[fd.lineColumn]}</strong></div>
|
||||
<div>{fd.line_column}: <strong>{o.object[fd.line_column]}</strong></div>
|
||||
{fd.metric &&
|
||||
<div>{metricLabel}: <strong>{o.object[metricLabel]}</strong></div>}
|
||||
</div>);
|
||||
@@ -71,7 +71,7 @@ export function commonLayerProps(formData, setTooltip, onSelect) {
|
||||
setTooltip({
|
||||
content: tooltipContentGenerator(o),
|
||||
x: o.x,
|
||||
y: o.y + 30,
|
||||
y: o.y,
|
||||
});
|
||||
} else {
|
||||
setTooltip(null);
|
||||
@@ -79,13 +79,13 @@ export function commonLayerProps(formData, setTooltip, onSelect) {
|
||||
};
|
||||
}
|
||||
let onClick;
|
||||
if (fd.jsOnclickHref) {
|
||||
if (fd.js_onclick_href) {
|
||||
onClick = (o) => {
|
||||
const href = sandboxedEval(fd.jsOnclickHref)(o);
|
||||
const href = sandboxedEval(fd.js_onclick_href)(o);
|
||||
window.open(href);
|
||||
};
|
||||
} else if (fd.tableFilter && onSelect !== undefined) {
|
||||
onClick = o => onSelect(o.object[fd.lineColumn]);
|
||||
} else if (fd.table_filter && onSelect !== undefined) {
|
||||
onClick = o => onSelect(o.object[fd.line_column]);
|
||||
}
|
||||
return {
|
||||
onClick,
|
||||
|
||||
@@ -24,8 +24,8 @@ import { hexToRGB } from '../../modules/colors';
|
||||
const DEFAULT_NUM_BUCKETS = 10;
|
||||
|
||||
export function getBreakPoints({
|
||||
breakPoints: formDataBreakPoints,
|
||||
numBuckets: formDataNumBuckets,
|
||||
break_points: formDataBreakPoints,
|
||||
num_buckets: formDataNumBuckets,
|
||||
}, features, accessor) {
|
||||
if (!features) {
|
||||
return [];
|
||||
@@ -46,15 +46,15 @@ export function getBreakPoints({
|
||||
}
|
||||
|
||||
export function getBreakPointColorScaler({
|
||||
breakPoints: formDataBreakPoints,
|
||||
numBuckets: formDataNumBuckets,
|
||||
linearColorScheme,
|
||||
break_points: formDataBreakPoints,
|
||||
num_buckets: formDataNumBuckets,
|
||||
linear_color_scheme: linearColorScheme,
|
||||
opacity,
|
||||
}, features, accessor) {
|
||||
const breakPoints = formDataBreakPoints || formDataNumBuckets
|
||||
? getBreakPoints({
|
||||
breakPoints: formDataBreakPoints,
|
||||
numBuckets: formDataNumBuckets,
|
||||
break_points: formDataBreakPoints,
|
||||
num_buckets: formDataNumBuckets,
|
||||
}, features, accessor)
|
||||
: null;
|
||||
const colorScheme = Array.isArray(linearColorScheme)
|
||||
|
||||
@@ -94,9 +94,9 @@ class LineMulti extends React.Component {
|
||||
const combinedFormData = {
|
||||
...subslice.form_data,
|
||||
filters: (subsliceFormData.filters || [])
|
||||
.concat(filters || []),
|
||||
.concat(filters || [])
|
||||
.concat(extraFilters || []),
|
||||
time_range: timeRange,
|
||||
extra_filters: extraFilters || [],
|
||||
};
|
||||
const addPrefix = prefixMetricWithSliceName;
|
||||
return getJson(getExploreLongUrl(combinedFormData, 'json'))
|
||||
|
||||
@@ -641,15 +641,15 @@ function nvd3Vis(element, props) {
|
||||
// If x bounds are shown, we need a right margin
|
||||
margins.right = Math.max(20, maxXAxisLabelHeight / 2) + marginPad;
|
||||
}
|
||||
if (xLabelRotation === 45) {
|
||||
if (staggerLabels) {
|
||||
margins.bottom = 40;
|
||||
} else {
|
||||
margins.bottom = (
|
||||
maxXAxisLabelHeight * Math.sin(Math.PI * xLabelRotation / 180)
|
||||
) + marginPad;
|
||||
margins.right = (
|
||||
maxXAxisLabelHeight * Math.cos(Math.PI * xLabelRotation / 180)
|
||||
) + marginPad;
|
||||
} else if (staggerLabels) {
|
||||
margins.bottom = 40;
|
||||
}
|
||||
|
||||
if (isVizTypes(['dual_line', 'line_multi'])) {
|
||||
@@ -711,9 +711,8 @@ function nvd3Vis(element, props) {
|
||||
.attr('height', height)
|
||||
.call(chart);
|
||||
|
||||
// On scroll, hide (not remove) tooltips so they can reappear on hover.
|
||||
// Throttle to only 4x/second.
|
||||
window.addEventListener('scroll', throttle(() => hideTooltips(false), 250));
|
||||
// on scroll, hide tooltips. throttle to only 4x/second.
|
||||
window.addEventListener('scroll', throttle(hideTooltips, 250));
|
||||
|
||||
// The below code should be run AFTER rendering because chart is updated in call()
|
||||
if (isTimeSeries && activeAnnotationLayers.length > 0) {
|
||||
@@ -934,10 +933,10 @@ function nvd3Vis(element, props) {
|
||||
return chart;
|
||||
};
|
||||
|
||||
// Remove tooltips before rendering chart, if the chart is being re-rendered sometimes
|
||||
// hide tooltips before rendering chart, if the chart is being re-rendered sometimes
|
||||
// there are left over tooltips in the dom,
|
||||
// this will clear them before rendering the chart again.
|
||||
hideTooltips(true);
|
||||
hideTooltips();
|
||||
|
||||
nv.addGraph(drawGraph);
|
||||
}
|
||||
|
||||
@@ -165,19 +165,10 @@ export function generateBubbleTooltipContent({
|
||||
return s;
|
||||
}
|
||||
|
||||
// shouldRemove indicates whether the nvtooltips should be removed from the DOM
|
||||
export function hideTooltips(shouldRemove) {
|
||||
export function hideTooltips() {
|
||||
const targets = document.querySelectorAll('.nvtooltip');
|
||||
if (targets.length > 0) {
|
||||
// Only set opacity to 0 when hiding tooltips so they would reappear
|
||||
// on hover, which sets the opacity to 1
|
||||
for (const t of targets) {
|
||||
if (shouldRemove) {
|
||||
t.remove();
|
||||
} else {
|
||||
t.style.opacity = 0;
|
||||
}
|
||||
}
|
||||
targets.forEach(t => t.remove());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -475,20 +475,17 @@ g.annotation-container {
|
||||
position:relative;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.reactable-header-sortable::before{
|
||||
.reactable-header-sortable::after {
|
||||
font: normal normal normal 14px/1 FontAwesome;
|
||||
content: "\f0dc";
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 5px;
|
||||
margin-left: 10px;
|
||||
color: @brand-primary;
|
||||
}
|
||||
.reactable-header-sort-asc::before{
|
||||
.reactable-header-sort-asc::after{
|
||||
content: "\f0de";
|
||||
color: @brand-primary;
|
||||
}
|
||||
.reactable-header-sort-desc::before{
|
||||
.reactable-header-sort-desc::after{
|
||||
content: "\f0dd";
|
||||
color: @brand-primary;
|
||||
}
|
||||
|
||||
@@ -14,247 +14,30 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# pylint: disable=C,R,W
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import pickle as pkl
|
||||
import traceback
|
||||
# pylint: disable=R
|
||||
from typing import Dict, List
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from superset import app, cache
|
||||
from superset import db
|
||||
from superset.connectors.connector_registry import ConnectorRegistry
|
||||
from superset.utils import core as utils
|
||||
from superset.utils.core import DTTM_ALIAS
|
||||
from .query_object import QueryObject
|
||||
|
||||
config = app.config
|
||||
stats_logger = config.get('STATS_LOGGER')
|
||||
|
||||
|
||||
class QueryContext:
|
||||
"""
|
||||
The query context contains the query object and additional fields necessary
|
||||
to retrieve the data payload for a given viz.
|
||||
"""
|
||||
|
||||
default_fillna = 0
|
||||
cache_type = 'df'
|
||||
enforce_numerical_metrics = True
|
||||
|
||||
# TODO: Type datasource and query_object dictionary with TypedDict when it becomes
|
||||
# a vanilla python type https://github.com/python/mypy/issues/5288
|
||||
def __init__(
|
||||
self,
|
||||
datasource: Dict,
|
||||
queries: List[Dict],
|
||||
force: bool = False,
|
||||
custom_cache_timeout: int = None,
|
||||
):
|
||||
self.datasource = ConnectorRegistry.get_datasource(datasource.get('type'),
|
||||
int(datasource.get('id')),
|
||||
db.session)
|
||||
self.queries = list(map(lambda query_obj: QueryObject(**query_obj), queries))
|
||||
|
||||
self.force = force
|
||||
|
||||
self.custom_cache_timeout = custom_cache_timeout
|
||||
|
||||
self.enforce_numerical_metrics = True
|
||||
|
||||
def get_query_result(self, query_object):
|
||||
"""Returns a pandas dataframe based on the query object"""
|
||||
|
||||
# Here, we assume that all the queries will use the same datasource, which is
|
||||
# is a valid assumption for current setting. In a long term, we may or maynot
|
||||
# support multiple queries from different data source.
|
||||
|
||||
timestamp_format = None
|
||||
if self.datasource.type == 'table':
|
||||
dttm_col = self.datasource.get_col(query_object.granularity)
|
||||
if dttm_col:
|
||||
timestamp_format = dttm_col.python_date_format
|
||||
|
||||
# The datasource here can be different backend but the interface is common
|
||||
result = self.datasource.query(query_object.to_dict())
|
||||
|
||||
df = result.df
|
||||
# Transform the timestamp we received from database to pandas supported
|
||||
# datetime format. If no python_date_format is specified, the pattern will
|
||||
# be considered as the default ISO date format
|
||||
# If the datetime format is unix, the parse will use the corresponding
|
||||
# parsing logic
|
||||
if df is not None and not df.empty:
|
||||
if DTTM_ALIAS in df.columns:
|
||||
if timestamp_format in ('epoch_s', 'epoch_ms'):
|
||||
# Column has already been formatted as a timestamp.
|
||||
df[DTTM_ALIAS] = df[DTTM_ALIAS].apply(pd.Timestamp)
|
||||
else:
|
||||
df[DTTM_ALIAS] = pd.to_datetime(
|
||||
df[DTTM_ALIAS], utc=False, format=timestamp_format)
|
||||
if self.datasource.offset:
|
||||
df[DTTM_ALIAS] += timedelta(hours=self.datasource.offset)
|
||||
df[DTTM_ALIAS] += query_object.time_shift
|
||||
|
||||
if self.enforce_numerical_metrics:
|
||||
self.df_metrics_to_num(df, query_object)
|
||||
|
||||
df.replace([np.inf, -np.inf], np.nan)
|
||||
df = self.handle_nulls(df)
|
||||
return {
|
||||
'query': result.query,
|
||||
'status': result.status,
|
||||
'error_message': result.error_message,
|
||||
'df': df,
|
||||
}
|
||||
|
||||
def df_metrics_to_num(self, df, query_object):
|
||||
"""Converting metrics to numeric when pandas.read_sql cannot"""
|
||||
metrics = [metric for metric in query_object.metrics]
|
||||
for col, dtype in df.dtypes.items():
|
||||
if dtype.type == np.object_ and col in metrics:
|
||||
df[col] = pd.to_numeric(df[col], errors='coerce')
|
||||
|
||||
def handle_nulls(self, df):
|
||||
fillna = self.get_fillna_for_columns(df.columns)
|
||||
return df.fillna(fillna)
|
||||
|
||||
def get_fillna_for_col(self, col):
|
||||
"""Returns the value to use as filler for a specific Column.type"""
|
||||
if col and col.is_string:
|
||||
return ' NULL'
|
||||
return self.default_fillna
|
||||
|
||||
def get_fillna_for_columns(self, columns=None):
|
||||
"""Returns a dict or scalar that can be passed to DataFrame.fillna"""
|
||||
if columns is None:
|
||||
return self.default_fillna
|
||||
columns_dict = {col.column_name: col for col in self.datasource.columns}
|
||||
fillna = {
|
||||
c: self.get_fillna_for_col(columns_dict.get(c))
|
||||
for c in columns
|
||||
}
|
||||
return fillna
|
||||
|
||||
def get_data(self, df):
|
||||
return df.to_dict(orient='records')
|
||||
|
||||
def get_single_payload(self, query_obj):
|
||||
"""Returns a payload of metadata and data"""
|
||||
payload = self.get_df_payload(query_obj)
|
||||
df = payload.get('df')
|
||||
status = payload.get('status')
|
||||
if status != utils.QueryStatus.FAILED:
|
||||
if df is not None and df.empty:
|
||||
payload['error'] = 'No data'
|
||||
else:
|
||||
payload['data'] = self.get_data(df)
|
||||
if 'df' in payload:
|
||||
del payload['df']
|
||||
return payload
|
||||
|
||||
def get_payload(self):
|
||||
"""Get all the paylaods from the arrays"""
|
||||
return [self.get_single_payload(query_ojbect) for query_ojbect in self.queries]
|
||||
|
||||
@property
|
||||
def cache_timeout(self):
|
||||
if self.custom_cache_timeout is not None:
|
||||
return self.custom_cache_timeout
|
||||
if self.datasource.cache_timeout is not None:
|
||||
return self.datasource.cache_timeout
|
||||
if (
|
||||
hasattr(self.datasource, 'database') and
|
||||
self.datasource.database.cache_timeout) is not None:
|
||||
return self.datasource.database.cache_timeout
|
||||
return config.get('CACHE_DEFAULT_TIMEOUT')
|
||||
|
||||
def get_df_payload(self, query_obj, **kwargs):
|
||||
"""Handles caching around the df paylod retrieval"""
|
||||
cache_key = query_obj.cache_key(
|
||||
datasource=self.datasource.uid, **kwargs) if query_obj else None
|
||||
logging.info('Cache key: {}'.format(cache_key))
|
||||
is_loaded = False
|
||||
stacktrace = None
|
||||
df = None
|
||||
cached_dttm = datetime.utcnow().isoformat().split('.')[0]
|
||||
cache_value = None
|
||||
status = None
|
||||
query = ''
|
||||
error_message = None
|
||||
if cache_key and cache and not self.force:
|
||||
cache_value = cache.get(cache_key)
|
||||
if cache_value:
|
||||
stats_logger.incr('loaded_from_cache')
|
||||
try:
|
||||
cache_value = pkl.loads(cache_value)
|
||||
df = cache_value['df']
|
||||
query = cache_value['query']
|
||||
status = utils.QueryStatus.SUCCESS
|
||||
is_loaded = True
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.error('Error reading cache: ' +
|
||||
utils.error_msg_from_exception(e))
|
||||
logging.info('Serving from cache')
|
||||
|
||||
if query_obj and not is_loaded:
|
||||
try:
|
||||
query_result = self.get_query_result(query_obj)
|
||||
status = query_result['status']
|
||||
query = query_result['query']
|
||||
error_message = query_result['error_message']
|
||||
df = query_result['df']
|
||||
if status != utils.QueryStatus.FAILED:
|
||||
stats_logger.incr('loaded_from_source')
|
||||
is_loaded = True
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
if not error_message:
|
||||
error_message = '{}'.format(e)
|
||||
status = utils.QueryStatus.FAILED
|
||||
stacktrace = traceback.format_exc()
|
||||
|
||||
if (
|
||||
is_loaded and
|
||||
cache_key and
|
||||
cache and
|
||||
status != utils.QueryStatus.FAILED):
|
||||
try:
|
||||
cache_value = dict(
|
||||
dttm=cached_dttm,
|
||||
df=df if df is not None else None,
|
||||
query=query,
|
||||
)
|
||||
cache_binary = pkl.dumps(
|
||||
cache_value, protocol=pkl.HIGHEST_PROTOCOL)
|
||||
|
||||
logging.info('Caching {} chars at key {}'.format(
|
||||
len(cache_binary), cache_key))
|
||||
|
||||
stats_logger.incr('set_cache_key')
|
||||
cache.set(
|
||||
cache_key,
|
||||
cache_value=cache_binary,
|
||||
timeout=self.cache_timeout)
|
||||
except Exception as e:
|
||||
# cache.set call can fail if the backend is down or if
|
||||
# the key is too large or whatever other reasons
|
||||
logging.warning('Could not cache key {}'.format(cache_key))
|
||||
logging.exception(e)
|
||||
cache.delete(cache_key)
|
||||
return {
|
||||
'cache_key': cache_key,
|
||||
'cached_dttm': cache_value['dttm'] if cache_value is not None else None,
|
||||
'cache_timeout': self.cache_timeout,
|
||||
'df': df,
|
||||
'error': error_message,
|
||||
'is_cached': cache_key is not None,
|
||||
'query': query,
|
||||
'status': status,
|
||||
'stacktrace': stacktrace,
|
||||
'rowcount': len(df.index) if df is not None else 0,
|
||||
}
|
||||
def get_data(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -15,17 +15,15 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# pylint: disable=R
|
||||
import hashlib
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
import simplejson as json
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from superset import app
|
||||
from superset.utils import core as utils
|
||||
|
||||
|
||||
# TODO: Type Metrics dictionary with TypedDict when it becomes a vanilla python type
|
||||
# https://github.com/python/mypy/issues/5288
|
||||
Metric = Dict
|
||||
|
||||
|
||||
class QueryObject:
|
||||
"""
|
||||
@@ -35,87 +33,31 @@ class QueryObject:
|
||||
def __init__(
|
||||
self,
|
||||
granularity: str,
|
||||
metrics: List[Union[Dict, str]],
|
||||
groupby: List[str] = None,
|
||||
metrics: List[Metric] = None,
|
||||
filters: List[str] = None,
|
||||
time_range: Optional[str] = None,
|
||||
time_shift: Optional[str] = None,
|
||||
is_timeseries: bool = False,
|
||||
timeseries_limit: int = 0,
|
||||
row_limit: int = app.config.get('ROW_LIMIT'),
|
||||
timeseries_limit_metric: Optional[Dict] = None,
|
||||
limit: int = 0,
|
||||
timeseries_limit_metric: Optional[Metric] = None,
|
||||
order_desc: bool = True,
|
||||
extras: Optional[Dict] = None,
|
||||
prequeries: Optional[Dict] = None,
|
||||
is_prequery: bool = False,
|
||||
columns: List[str] = None,
|
||||
orderby: List[List] = None,
|
||||
):
|
||||
self.granularity = granularity
|
||||
self.from_dttm, self.to_dttm = utils.get_since_until(time_range, time_shift)
|
||||
self.is_timeseries = is_timeseries
|
||||
self.time_range = time_range
|
||||
self.time_shift = utils.parse_human_timedelta(time_shift)
|
||||
self.groupby = groupby if groupby is not None else []
|
||||
|
||||
# Temporal solution for backward compatability issue
|
||||
# due the new format of non-ad-hoc metric.
|
||||
self.metrics = [metric if 'expressionType' in metric else metric['label']
|
||||
for metric in metrics]
|
||||
self.groupby = groupby or []
|
||||
self.metrics = metrics or []
|
||||
self.filter = filters or []
|
||||
self.row_limit = row_limit
|
||||
self.filter = filters if filters is not None else []
|
||||
self.timeseries_limit = timeseries_limit
|
||||
self.timeseries_limit = int(limit)
|
||||
self.timeseries_limit_metric = timeseries_limit_metric
|
||||
self.order_desc = order_desc
|
||||
self.prequeries = prequeries
|
||||
self.is_prequery = is_prequery
|
||||
self.extras = extras if extras is not None else {}
|
||||
self.columns = columns if columns is not None else []
|
||||
self.orderby = orderby if orderby is not None else []
|
||||
self.prequeries = []
|
||||
self.is_prequery = False
|
||||
self.extras = extras
|
||||
|
||||
def to_dict(self):
|
||||
query_object_dict = {
|
||||
'granularity': self.granularity,
|
||||
'from_dttm': self.from_dttm,
|
||||
'to_dttm': self.to_dttm,
|
||||
'is_timeseries': self.is_timeseries,
|
||||
'groupby': self.groupby,
|
||||
'metrics': self.metrics,
|
||||
'row_limit': self.row_limit,
|
||||
'filter': self.filter,
|
||||
'timeseries_limit': self.timeseries_limit,
|
||||
'timeseries_limit_metric': self.timeseries_limit_metric,
|
||||
'order_desc': self.order_desc,
|
||||
'prequeries': self.prequeries,
|
||||
'is_prequery': self.is_prequery,
|
||||
'extras': self.extras,
|
||||
'columns': self.columns,
|
||||
'orderby': self.orderby,
|
||||
}
|
||||
return query_object_dict
|
||||
|
||||
def cache_key(self, **extra):
|
||||
"""
|
||||
The cache key is made out of the key/values in `query_obj`, plus any
|
||||
other key/values in `extra`
|
||||
We remove datetime bounds that are hard values, and replace them with
|
||||
the use-provided inputs to bounds, which may be time-relative (as in
|
||||
"5 days ago" or "now").
|
||||
"""
|
||||
cache_dict = self.to_dict()
|
||||
cache_dict.update(extra)
|
||||
|
||||
for k in ['from_dttm', 'to_dttm']:
|
||||
del cache_dict[k]
|
||||
if self.time_range:
|
||||
cache_dict['time_range'] = self.time_range
|
||||
json_data = self.json_dumps(cache_dict, sort_keys=True)
|
||||
return hashlib.md5(json_data.encode('utf-8')).hexdigest()
|
||||
|
||||
def json_dumps(self, obj, sort_keys=False):
|
||||
return json.dumps(
|
||||
obj,
|
||||
default=utils.json_int_dttm_ser,
|
||||
ignore_nan=True,
|
||||
sort_keys=sort_keys,
|
||||
)
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -181,14 +181,18 @@ LANGUAGES = {
|
||||
'pt': {'flag': 'pt', 'name': 'Portuguese'},
|
||||
'pt_BR': {'flag': 'br', 'name': 'Brazilian Portuguese'},
|
||||
'ru': {'flag': 'ru', 'name': 'Russian'},
|
||||
'ko': {'flag': 'kr', 'name': 'Korean'},
|
||||
}
|
||||
|
||||
# ---------------------------------------------------
|
||||
# Feature flags
|
||||
# ---------------------------------------------------
|
||||
# Feature flags that are on by default go here. Their
|
||||
# values can be overridden by those in super_config.py
|
||||
FEATURE_FLAGS = {}
|
||||
# Feature flags that are set by default go here. Their values can be
|
||||
# overwritten by those specified under FEATURE_FLAGS in super_config.py
|
||||
# For example, DEFAULT_FEATURE_FLAGS = { 'FOO': True, 'BAR': False } here
|
||||
# and FEATURE_FLAGS = { 'BAR': True, 'BAZ': True } in superset_config.py
|
||||
# will result in combined feature flags of { 'FOO': True, 'BAR': True, 'BAZ': True }
|
||||
DEFAULT_FEATURE_FLAGS = {}
|
||||
|
||||
# ---------------------------------------------------
|
||||
# Image and file configuration
|
||||
@@ -326,9 +330,6 @@ DEFAULT_SQLLAB_LIMIT = 1000
|
||||
# Maximum number of tables/views displayed in the dropdown window in SQL Lab.
|
||||
MAX_TABLE_NAMES = 3000
|
||||
|
||||
# Adds a warning message on sqllab save query modal.
|
||||
SQLLAB_SAVE_WARNING_MESSAGE = None
|
||||
|
||||
# If defined, shows this text in an alert-warning box in the navbar
|
||||
# one example use case may be "STAGING" to make it clear that this is
|
||||
# not the production version of the site.
|
||||
|
||||
@@ -86,7 +86,7 @@ class BaseDatasource(AuditMixinNullable, ImportMixin):
|
||||
|
||||
@property
|
||||
def column_names(self):
|
||||
return sorted([c.column_name for c in self.columns], key=lambda x: x or '')
|
||||
return sorted([c.column_name for c in self.columns])
|
||||
|
||||
@property
|
||||
def columns_types(self):
|
||||
@@ -166,9 +166,7 @@ class BaseDatasource(AuditMixinNullable, ImportMixin):
|
||||
def data(self):
|
||||
"""Data representation of the datasource sent to the frontend"""
|
||||
order_by_choices = []
|
||||
# self.column_names return sorted column_names
|
||||
for s in self.column_names:
|
||||
s = str(s or '')
|
||||
for s in sorted(self.column_names):
|
||||
order_by_choices.append((json.dumps([s, True]), s + ' [asc]'))
|
||||
order_by_choices.append((json.dumps([s, False]), s + ' [desc]'))
|
||||
|
||||
|
||||
@@ -280,7 +280,7 @@ class DruidColumn(Model, BaseColumn):
|
||||
export_parent = 'datasource'
|
||||
|
||||
def __repr__(self):
|
||||
return self.column_name or str(self.id)
|
||||
return self.column_name
|
||||
|
||||
@property
|
||||
def expression(self):
|
||||
@@ -1144,9 +1144,7 @@ class DruidDatasource(Model, BaseDatasource):
|
||||
pre_qry['aggregations'] = aggs_dict
|
||||
pre_qry['post_aggregations'] = post_aggs_dict
|
||||
else:
|
||||
agg_keys = qry['aggregations'].keys()
|
||||
order_by = list(agg_keys)[0] if agg_keys else None
|
||||
|
||||
order_by = list(qry['aggregations'].keys())[0]
|
||||
# Limit on the number of timeseries, doing a two-phases query
|
||||
pre_qry['granularity'] = 'all'
|
||||
pre_qry['threshold'] = min(row_limit,
|
||||
|
||||
@@ -41,7 +41,7 @@ from . import models
|
||||
class DruidColumnInlineView(CompactCRUDMixin, SupersetModelView): # noqa
|
||||
datamodel = SQLAInterface(models.DruidColumn)
|
||||
|
||||
list_title = _('List Druid Column')
|
||||
list_title = _('Columns')
|
||||
show_title = _('Show Druid Column')
|
||||
add_title = _('Add Druid Column')
|
||||
edit_title = _('Edit Druid Column')
|
||||
@@ -109,7 +109,7 @@ appbuilder.add_view_no_menu(DruidColumnInlineView)
|
||||
class DruidMetricInlineView(CompactCRUDMixin, SupersetModelView): # noqa
|
||||
datamodel = SQLAInterface(models.DruidMetric)
|
||||
|
||||
list_title = _('List Druid Metric')
|
||||
list_title = _('Metrics')
|
||||
show_title = _('Show Druid Metric')
|
||||
add_title = _('Add Druid Metric')
|
||||
edit_title = _('Edit Druid Metric')
|
||||
@@ -160,7 +160,7 @@ appbuilder.add_view_no_menu(DruidMetricInlineView)
|
||||
class DruidClusterModelView(SupersetModelView, DeleteMixin, YamlExportMixin): # noqa
|
||||
datamodel = SQLAInterface(models.DruidCluster)
|
||||
|
||||
list_title = _('List Druid Cluster')
|
||||
list_title = _('Druid Clusters')
|
||||
show_title = _('Show Druid Cluster')
|
||||
add_title = _('Add Druid Cluster')
|
||||
edit_title = _('Edit Druid Cluster')
|
||||
@@ -212,7 +212,7 @@ appbuilder.add_view(
|
||||
class DruidDatasourceModelView(DatasourceModelView, DeleteMixin, YamlExportMixin): # noqa
|
||||
datamodel = SQLAInterface(models.DruidDatasource)
|
||||
|
||||
list_title = _('List Druid Datasource')
|
||||
list_title = _('Druid Datasources')
|
||||
show_title = _('Show Druid Datasource')
|
||||
add_title = _('Add Druid Datasource')
|
||||
edit_title = _('Edit Druid Datasource')
|
||||
|
||||
@@ -115,7 +115,9 @@ class TableColumn(Model, BaseColumn):
|
||||
label = label if label else self.column_name
|
||||
label = self.table.get_label(label)
|
||||
if not self.expression:
|
||||
col = column(self.column_name).label(label)
|
||||
db_engine_spec = self.table.database.db_engine_spec
|
||||
type_ = db_engine_spec.get_sqla_column_type(self.type)
|
||||
col = column(self.column_name, type_=type_).label(label)
|
||||
else:
|
||||
col = literal_column(self.expression).label(label)
|
||||
return col
|
||||
@@ -422,7 +424,6 @@ class SqlaTable(Model, BaseDatasource):
|
||||
d['time_grain_sqla'] = grains
|
||||
d['main_dttm_col'] = self.main_dttm_col
|
||||
d['fetch_values_predicate'] = self.fetch_values_predicate
|
||||
d['template_params'] = self.template_params
|
||||
return d
|
||||
|
||||
def values_for_column(self, column_name, limit=10000):
|
||||
@@ -730,11 +731,15 @@ class SqlaTable(Model, BaseDatasource):
|
||||
|
||||
ob = inner_main_metric_expr
|
||||
if timeseries_limit_metric:
|
||||
ob = self._get_timeseries_orderby(
|
||||
timeseries_limit_metric,
|
||||
metrics_dict,
|
||||
cols,
|
||||
)
|
||||
if utils.is_adhoc_metric(timeseries_limit_metric):
|
||||
ob = self.adhoc_metric_to_sqla(timeseries_limit_metric, cols)
|
||||
elif timeseries_limit_metric in metrics_dict:
|
||||
timeseries_limit_metric = metrics_dict.get(
|
||||
timeseries_limit_metric,
|
||||
)
|
||||
ob = timeseries_limit_metric.get_sqla_col()
|
||||
else:
|
||||
raise Exception(_("Metric '{}' is not valid".format(m)))
|
||||
direction = desc if order_desc else asc
|
||||
subq = subq.order_by(direction(ob))
|
||||
subq = subq.limit(timeseries_limit)
|
||||
@@ -749,16 +754,6 @@ class SqlaTable(Model, BaseDatasource):
|
||||
|
||||
tbl = tbl.join(subq.alias(), and_(*on_clause))
|
||||
else:
|
||||
if timeseries_limit_metric:
|
||||
orderby = [(
|
||||
self._get_timeseries_orderby(
|
||||
timeseries_limit_metric,
|
||||
metrics_dict,
|
||||
cols,
|
||||
),
|
||||
False,
|
||||
)]
|
||||
|
||||
# run subquery to get top groups
|
||||
subquery_obj = {
|
||||
'prequeries': prequeries,
|
||||
@@ -787,19 +782,6 @@ class SqlaTable(Model, BaseDatasource):
|
||||
|
||||
return qry.select_from(tbl)
|
||||
|
||||
def _get_timeseries_orderby(self, timeseries_limit_metric, metrics_dict, cols):
|
||||
if utils.is_adhoc_metric(timeseries_limit_metric):
|
||||
ob = self.adhoc_metric_to_sqla(timeseries_limit_metric, cols)
|
||||
elif timeseries_limit_metric in metrics_dict:
|
||||
timeseries_limit_metric = metrics_dict.get(
|
||||
timeseries_limit_metric,
|
||||
)
|
||||
ob = timeseries_limit_metric.get_sqla_col()
|
||||
else:
|
||||
raise Exception(_("Metric '{}' is not valid".format(timeseries_limit_metric)))
|
||||
|
||||
return ob
|
||||
|
||||
def _get_top_groups(self, df, dimensions):
|
||||
cols = {col.column_name: col for col in self.columns}
|
||||
groups = []
|
||||
@@ -876,13 +858,13 @@ class SqlaTable(Model, BaseDatasource):
|
||||
dbcol = dbcols.get(col.name, None)
|
||||
if not dbcol:
|
||||
dbcol = TableColumn(column_name=col.name, type=datatype)
|
||||
dbcol.groupby = dbcol.is_string
|
||||
dbcol.filterable = dbcol.is_string
|
||||
dbcol.sum = dbcol.is_num
|
||||
dbcol.avg = dbcol.is_num
|
||||
dbcol.is_dttm = dbcol.is_time
|
||||
else:
|
||||
dbcol.type = datatype
|
||||
dbcol.groupby = True
|
||||
dbcol.filterable = True
|
||||
self.columns.append(dbcol)
|
||||
if not any_date_col and dbcol.is_time:
|
||||
any_date_col = col.name
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
# under the License.
|
||||
# pylint: disable=C,R,W
|
||||
"""Views used by the SqlAlchemy connector"""
|
||||
import logging
|
||||
|
||||
from flask import flash, Markup, redirect
|
||||
from flask_appbuilder import CompactCRUDMixin, expose
|
||||
from flask_appbuilder.actions import action
|
||||
@@ -33,11 +35,13 @@ from superset.views.base import (
|
||||
)
|
||||
from . import models
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TableColumnInlineView(CompactCRUDMixin, SupersetModelView): # noqa
|
||||
datamodel = SQLAInterface(models.TableColumn)
|
||||
|
||||
list_title = _('List Columns')
|
||||
list_title = _('Columns')
|
||||
show_title = _('Show Column')
|
||||
add_title = _('Add Column')
|
||||
edit_title = _('Edit Column')
|
||||
@@ -110,7 +114,7 @@ appbuilder.add_view_no_menu(TableColumnInlineView)
|
||||
class SqlMetricInlineView(CompactCRUDMixin, SupersetModelView): # noqa
|
||||
datamodel = SQLAInterface(models.SqlMetric)
|
||||
|
||||
list_title = _('List Metrics')
|
||||
list_title = _('Metrics')
|
||||
show_title = _('Show Metric')
|
||||
add_title = _('Add Metric')
|
||||
edit_title = _('Edit Metric')
|
||||
@@ -164,7 +168,7 @@ appbuilder.add_view_no_menu(SqlMetricInlineView)
|
||||
class TableModelView(DatasourceModelView, DeleteMixin, YamlExportMixin): # noqa
|
||||
datamodel = SQLAInterface(models.SqlaTable)
|
||||
|
||||
list_title = _('List Tables')
|
||||
list_title = _('Tables')
|
||||
show_title = _('Show Table')
|
||||
add_title = _('Import a table definition')
|
||||
edit_title = _('Edit Table')
|
||||
@@ -269,12 +273,13 @@ class TableModelView(DatasourceModelView, DeleteMixin, YamlExportMixin): # noqa
|
||||
# Fail before adding if the table can't be found
|
||||
try:
|
||||
table.get_sqla_table_object()
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.exception(f'Got an error in pre_add for {table.name}')
|
||||
raise Exception(_(
|
||||
'Table [{}] could not be found, '
|
||||
'please double check your '
|
||||
'database connection, schema, and '
|
||||
'table name').format(table.name))
|
||||
'table name, error: {}').format(table.name, str(e)))
|
||||
|
||||
def post_add(self, table, flash_message=True):
|
||||
table.fetch_metadata()
|
||||
|
||||
BIN
superset/data/airports.csv.gz
Normal file
BIN
superset/data/airports.csv.gz
Normal file
Binary file not shown.
BIN
superset/data/bart-lines.json.gz
Normal file
BIN
superset/data/bart-lines.json.gz
Normal file
Binary file not shown.
@@ -14,7 +14,9 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
|
||||
import pandas as pd
|
||||
import polyline
|
||||
@@ -22,17 +24,16 @@ from sqlalchemy import String, Text
|
||||
|
||||
from superset import db
|
||||
from superset.utils.core import get_or_create_main_db
|
||||
from .helpers import TBL, get_example_data
|
||||
from .helpers import DATA_FOLDER, TBL
|
||||
|
||||
|
||||
def load_bart_lines():
|
||||
tbl_name = 'bart_lines'
|
||||
content = get_example_data('bart-lines.json.gz')
|
||||
df = pd.read_json(content, encoding='latin-1')
|
||||
df['path_json'] = df.path.map(json.dumps)
|
||||
df['polyline'] = df.path.map(polyline.encode)
|
||||
del df['path']
|
||||
|
||||
with gzip.open(os.path.join(DATA_FOLDER, 'bart-lines.json.gz')) as f:
|
||||
df = pd.read_json(f, encoding='latin-1')
|
||||
df['path_json'] = df.path.map(json.dumps)
|
||||
df['polyline'] = df.path.map(polyline.encode)
|
||||
del df['path']
|
||||
df.to_sql(
|
||||
tbl_name,
|
||||
db.engine,
|
||||
|
||||
97
superset/data/birth_france_data_for_country_map.csv
Normal file
97
superset/data/birth_france_data_for_country_map.csv
Normal file
@@ -0,0 +1,97 @@
|
||||
DEPT_ID,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014
|
||||
FR-01,6866,6706,6976,7228,6949,7323,7157,7282,7265,7242,7296,7354
|
||||
FR-02,6841,6761,6889,7041,6847,7012,6941,7050,6939,6755,6559,6468
|
||||
FR-03,3391,3335,3363,3503,3277,3289,3308,3402,3196,3288,3198,3152
|
||||
FR-04,1460,1522,1514,1536,1569,1569,1513,1547,1578,1561,1629,1538
|
||||
FR-05,1408,1403,1395,1461,1448,1441,1513,1470,1399,1441,1406,1383
|
||||
FR-06,11144,11514,11631,11754,11633,12275,11949,12257,11999,12087,12149,12170
|
||||
FR-07,3367,3176,3414,3484,3484,3447,3307,3380,3360,3405,3179,3254
|
||||
FR-08,3532,3422,3420,3343,3552,3522,3312,3254,3137,3258,3021,2966
|
||||
FR-09,1350,1412,1389,1499,1570,1493,1452,1473,1404,1425,1413,1364
|
||||
FR-10,3428,3553,3692,3685,3619,3721,3745,3722,3635,3587,3436,3377
|
||||
FR-11,3421,3321,3502,3661,3723,3778,3797,3770,3789,3669,3618,3516
|
||||
FR-12,2558,2614,2701,2829,2769,2748,2640,2694,2682,2615,2475,2555
|
||||
FR-13,23908,24056,24411,25371,25126,25412,25547,26410,25889,26328,26762,26384
|
||||
FR-14,8231,8257,8251,8531,8310,8183,8304,8111,8041,7833,7644,7466
|
||||
FR-15,1344,1396,1391,1398,1357,1300,1377,1274,1237,1230,1290,1214
|
||||
FR-16,3401,3514,3570,3653,3618,3666,3408,3564,3459,3490,3472,3378
|
||||
FR-17,5935,5900,6069,6089,5903,6136,6209,6185,6065,5916,5778,5846
|
||||
FR-18,3301,3271,3313,3231,3341,3303,3229,3341,3159,3120,3128,3097
|
||||
FR-19,2133,2250,2319,2327,2245,2263,2231,2247,2196,2163,2055,2094
|
||||
FR-21,6079,6052,5844,5986,6015,5960,5852,5963,5906,5905,5769,5779
|
||||
FR-22,6413,6317,6287,6743,6473,6494,6559,6438,6221,6184,5927,5790
|
||||
FR-23,1011,957,1054,1038,1013,1029,1044,919,967,998,897,879
|
||||
FR-24,3607,3690,3662,3758,3760,3832,3672,3665,3645,3547,3486,3479
|
||||
FR-25,6529,6798,6782,6993,6804,7097,6914,7105,6826,6778,6732,6659
|
||||
FR-26,5525,5703,5579,5945,5833,5927,5846,5915,5978,5912,6026,5965
|
||||
FR-27,7213,7220,7386,7402,7471,7717,7714,7715,7738,7676,7352,7242
|
||||
FR-28,5370,5363,5585,5632,5440,5677,5573,5716,5540,5548,5312,5295
|
||||
FR-29,9900,9963,9851,10184,9962,10040,9733,9823,9615,9597,9277,9088
|
||||
FR-2A,1232,1228,1348,1337,1284,1370,1422,1408,1422,1398,1317,1371
|
||||
FR-2B,1455,1444,1525,1474,1564,1569,1580,1591,1662,1612,1599,1616
|
||||
FR-30,7446,7777,7901,8384,8190,8449,8354,8494,8467,8196,8427,8216
|
||||
FR-31,13989,13900,14233,14957,14968,15415,15317,15770,16031,16347,16290,16641
|
||||
FR-32,1635,1625,1666,1580,1669,1689,1718,1671,1587,1668,1648,1643
|
||||
FR-33,15610,15819,15722,16539,16514,16636,17072,17271,17098,17097,17265,17303
|
||||
FR-34,11380,11562,11636,12191,12252,12564,12531,12658,13000,12902,12899,13008
|
||||
FR-35,12134,12072,12405,12687,12606,12837,12917,12876,13033,12892,12729,12555
|
||||
FR-36,2312,2314,2394,2283,2341,2371,2178,2221,2137,2136,2006,2030
|
||||
FR-37,6620,6594,6644,6813,6434,6811,6828,6886,6696,6796,6594,6718
|
||||
FR-38,14885,15356,15447,15830,15646,15999,15916,16136,15739,15948,15724,15664
|
||||
FR-39,2964,3017,2924,3021,3037,3045,2897,2865,2758,2741,2675,2637
|
||||
FR-40,3477,3621,3574,3755,3953,3862,3914,3993,3853,3880,3864,3696
|
||||
FR-41,3617,3678,3724,3815,3752,3847,3786,3777,3667,3704,3581,3517
|
||||
FR-42,8804,8906,8975,9184,9222,9357,9174,9403,9357,9473,9086,9183
|
||||
FR-43,2458,2416,2485,2426,2301,2398,2390,2348,2300,2244,2247,2157
|
||||
FR-44,15795,15988,16301,16530,16664,16763,16766,17159,16747,16821,16822,16700
|
||||
FR-45,8265,8424,8200,8635,8644,8524,8499,8757,8686,8689,8526,8355
|
||||
FR-46,1537,1430,1477,1563,1511,1555,1435,1506,1423,1487,1345,1415
|
||||
FR-47,3173,3245,3341,3426,3399,3378,3445,3359,3397,3332,3361,3347
|
||||
FR-48,768,772,760,784,781,779,798,736,695,711,663,651
|
||||
FR-49,10018,10085,10148,10548,10227,10270,10165,10312,10320,10061,10016,9781
|
||||
FR-50,5490,5487,5538,5448,5356,5384,5231,5238,5193,5282,4998,4911
|
||||
FR-51,6916,6979,7108,7118,6932,7065,7061,7182,7070,6761,7000,6887
|
||||
FR-52,2100,2095,2029,2104,2062,2037,1944,1889,1916,1847,1923,1881
|
||||
FR-53,3846,3932,3981,4118,3835,3912,3897,3962,3733,3750,3656,3456
|
||||
FR-54,8398,8671,8542,8743,8421,8559,8487,8536,8499,8387,8197,8135
|
||||
FR-55,2218,2287,2158,2294,2296,2220,2122,2221,2119,2107,2070,1928
|
||||
FR-56,7817,8036,7802,8221,7968,8288,7942,8029,7894,7909,7645,7554
|
||||
FR-57,11710,11970,12048,12114,11853,12012,11831,11856,11474,11579,11421,11385
|
||||
FR-58,2123,2181,2115,2137,2151,2049,1986,1982,1999,1942,1850,1801
|
||||
FR-59,36099,36257,35960,36858,36531,36572,36508,36703,36678,36513,36354,35923
|
||||
FR-60,10696,10630,10753,11144,11097,11162,11013,10960,11032,10941,10814,10802
|
||||
FR-61,3323,3243,3117,3276,3316,3185,3248,3192,3105,2933,2834,2810
|
||||
FR-62,18888,19304,19407,19780,19668,19902,19661,19784,19720,19017,19054,18809
|
||||
FR-63,6576,6632,6701,6902,6896,6865,6774,7131,6828,6933,6699,6908
|
||||
FR-64,6436,6338,6395,6680,6288,6455,6652,6569,6459,6490,6269,6497
|
||||
FR-65,2144,2186,2095,2284,2266,2095,2161,2149,2110,2201,2057,2111
|
||||
FR-66,4456,4320,4563,4779,4638,4756,4837,4869,4843,4943,4914,4800
|
||||
FR-67,13024,12828,13195,13388,13152,13231,13218,13346,13030,12895,13043,13262
|
||||
FR-68,9045,8945,8912,9324,8941,8909,8938,9177,8927,8818,8713,8826
|
||||
FR-69,23376,23796,24270,24808,24465,25120,25528,25973,25921,26294,25914,26712
|
||||
FR-70,2675,2773,2827,2975,2888,2755,2785,2761,2643,2609,2510,2458
|
||||
FR-71,5717,5709,5789,5876,5736,5860,5838,5865,5811,5752,5514,5552
|
||||
FR-72,6871,6935,6770,7133,6808,6909,6957,6942,6810,6703,6645,6664
|
||||
FR-73,4687,4736,4795,4903,5000,4971,4863,5074,4917,4786,4762,4798
|
||||
FR-74,8839,8753,8967,9124,8939,9333,9271,9521,9476,9829,9893,9982
|
||||
FR-75,31493,31817,31378,31748,30820,30623,31063,31447,30094,29291,28945,29134
|
||||
FR-76,15862,15650,15691,16004,16066,16041,15947,16338,16146,16014,15574,15199
|
||||
FR-77,17501,17729,18317,18986,18978,19240,19331,19712,19824,19678,19331,19708
|
||||
FR-78,19937,19431,19766,20438,19899,19895,19868,20312,19886,19827,19886,19525
|
||||
FR-79,3994,4100,4191,4057,4037,4331,4157,4060,4006,4029,3986,3718
|
||||
FR-80,7134,7035,7024,7021,6939,7094,6838,7103,6989,6843,6743,6506
|
||||
FR-81,3579,3611,3837,3933,3869,4056,4030,3925,4006,3939,3829,3831
|
||||
FR-82,2398,2591,2590,2823,2858,2932,2935,2926,2978,2940,2827,2829
|
||||
FR-83,10388,10622,10646,10889,10938,11131,10955,11159,11146,11240,10917,11123
|
||||
FR-84,6547,6629,6608,6805,6694,7000,7014,6967,7008,7107,7171,7058
|
||||
FR-85,6874,7062,7299,7589,7647,7629,7718,7601,7442,7436,7164,7070
|
||||
FR-86,4594,4568,4725,4850,4753,4909,4953,5006,4885,4880,4708,4686
|
||||
FR-87,3449,3659,3834,3754,3829,3891,3985,3848,3907,3825,3723,3724
|
||||
FR-88,4291,4264,4310,4416,4274,4215,4252,4057,3883,3715,3796,3679
|
||||
FR-89,3710,3844,3821,3929,3917,4045,3991,3842,3699,3729,3780,3621
|
||||
FR-90,1896,1766,1837,1888,1880,1818,1822,1802,1794,1763,1675,1707
|
||||
FR-91,17122,17614,17753,18281,17932,18134,18040,18509,18493,18506,18510,18903
|
||||
FR-92,24607,24649,24588,25426,24937,25217,25192,25194,25083,24790,24614,24675
|
||||
FR-93,25868,26313,26760,27916,27743,28062,28313,28513,28362,28675,28687,29471
|
||||
FR-94,19637,19866,19947,20948,20331,20736,21022,21391,20991,20967,20748,21566
|
||||
FR-95,17346,17863,18012,19015,18624,18761,18728,19506,19551,19495,19550,19737
|
||||
|
BIN
superset/data/birth_names.json.gz
Normal file
BIN
superset/data/birth_names.json.gz
Normal file
Binary file not shown.
@@ -14,7 +14,9 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
import textwrap
|
||||
|
||||
import pandas as pd
|
||||
@@ -26,7 +28,7 @@ from superset.utils.core import get_or_create_main_db
|
||||
from .helpers import (
|
||||
config,
|
||||
Dash,
|
||||
get_example_data,
|
||||
DATA_FOLDER,
|
||||
get_slice_json,
|
||||
merge_slice,
|
||||
Slice,
|
||||
@@ -37,8 +39,8 @@ from .helpers import (
|
||||
|
||||
def load_birth_names():
|
||||
"""Loading birth name dataset from a zip file in the repo"""
|
||||
data = get_example_data('birth_names.json.gz')
|
||||
pdf = pd.read_json(data)
|
||||
with gzip.open(os.path.join(DATA_FOLDER, 'birth_names.json.gz')) as f:
|
||||
pdf = pd.read_json(f)
|
||||
pdf.ds = pd.to_datetime(pdf.ds, unit='ms')
|
||||
pdf.to_sql(
|
||||
'birth_names',
|
||||
|
||||
BIN
superset/data/countries.json.gz
Normal file
BIN
superset/data/countries.json.gz
Normal file
Binary file not shown.
@@ -15,6 +15,7 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import datetime
|
||||
import os
|
||||
|
||||
import pandas as pd
|
||||
from sqlalchemy import BigInteger, Date, String
|
||||
@@ -23,7 +24,7 @@ from superset import db
|
||||
from superset.connectors.sqla.models import SqlMetric
|
||||
from superset.utils import core as utils
|
||||
from .helpers import (
|
||||
get_example_data,
|
||||
DATA_FOLDER,
|
||||
get_slice_json,
|
||||
merge_slice,
|
||||
misc_dash_slices,
|
||||
@@ -34,9 +35,8 @@ from .helpers import (
|
||||
|
||||
def load_country_map_data():
|
||||
"""Loading data for map with country map"""
|
||||
csv_bytes = get_example_data(
|
||||
'birth_france_data_for_country_map.csv', is_gzip=False, make_bytes=True)
|
||||
data = pd.read_csv(csv_bytes, encoding='utf-8')
|
||||
csv_path = os.path.join(DATA_FOLDER, 'birth_france_data_for_country_map.csv')
|
||||
data = pd.read_csv(csv_path, encoding='utf-8')
|
||||
data['dttm'] = datetime.datetime.now().date()
|
||||
data.to_sql( # pylint: disable=no-member
|
||||
'birth_france_by_region',
|
||||
|
||||
BIN
superset/data/energy.json.gz
Normal file
BIN
superset/data/energy.json.gz
Normal file
Binary file not shown.
@@ -16,6 +16,8 @@
|
||||
# under the License.
|
||||
"""Loads datasets, dashboards and slices in a new superset instance"""
|
||||
# pylint: disable=C,R,W
|
||||
import gzip
|
||||
import os
|
||||
import textwrap
|
||||
|
||||
import pandas as pd
|
||||
@@ -24,16 +26,14 @@ from sqlalchemy import Float, String
|
||||
from superset import db
|
||||
from superset.connectors.sqla.models import SqlMetric
|
||||
from superset.utils import core as utils
|
||||
from .helpers import (
|
||||
DATA_FOLDER, get_example_data, merge_slice, misc_dash_slices, Slice, TBL,
|
||||
)
|
||||
from .helpers import DATA_FOLDER, merge_slice, misc_dash_slices, Slice, TBL
|
||||
|
||||
|
||||
def load_energy():
|
||||
"""Loads an energy related dataset to use with sankey and graphs"""
|
||||
tbl_name = 'energy_usage'
|
||||
data = get_example_data('energy.json.gz')
|
||||
pdf = pd.read_json(data)
|
||||
with gzip.open(os.path.join(DATA_FOLDER, 'energy.json.gz')) as f:
|
||||
pdf = pd.read_json(f)
|
||||
pdf.to_sql(
|
||||
tbl_name,
|
||||
db.engine,
|
||||
|
||||
BIN
superset/data/flight_data.csv.gz
Normal file
BIN
superset/data/flight_data.csv.gz
Normal file
Binary file not shown.
@@ -14,23 +14,26 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import gzip
|
||||
import os
|
||||
|
||||
import pandas as pd
|
||||
from sqlalchemy import DateTime
|
||||
|
||||
from superset import db
|
||||
from superset.utils import core as utils
|
||||
from .helpers import get_example_data, TBL
|
||||
from .helpers import DATA_FOLDER, TBL
|
||||
|
||||
|
||||
def load_flights():
|
||||
"""Loading random time series data from a zip file in the repo"""
|
||||
tbl_name = 'flights'
|
||||
data = get_example_data('flight_data.csv.gz', make_bytes=True)
|
||||
pdf = pd.read_csv(data, encoding='latin-1')
|
||||
with gzip.open(os.path.join(DATA_FOLDER, 'flight_data.csv.gz')) as f:
|
||||
pdf = pd.read_csv(f, encoding='latin-1')
|
||||
|
||||
# Loading airports info to join and get lat/long
|
||||
airports_bytes = get_example_data('airports.csv.gz', make_bytes=True)
|
||||
airports = pd.read_csv(airports_bytes, encoding='latin-1')
|
||||
with gzip.open(os.path.join(DATA_FOLDER, 'airports.csv.gz')) as f:
|
||||
airports = pd.read_csv(f, encoding='latin-1')
|
||||
airports = airports.set_index('IATA_CODE')
|
||||
|
||||
pdf['ds'] = pdf.YEAR.map(str) + '-0' + pdf.MONTH.map(str) + '-0' + pdf.DAY.map(str)
|
||||
|
||||
@@ -16,19 +16,13 @@
|
||||
# under the License.
|
||||
"""Loads datasets, dashboards and slices in a new superset instance"""
|
||||
# pylint: disable=C,R,W
|
||||
from io import BytesIO
|
||||
import json
|
||||
import os
|
||||
import zlib
|
||||
|
||||
import requests
|
||||
|
||||
from superset import app, db
|
||||
from superset.connectors.connector_registry import ConnectorRegistry
|
||||
from superset.models import core as models
|
||||
|
||||
BASE_URL = 'https://github.com/apache-superset/examples-data/blob/master/'
|
||||
|
||||
# Shortcuts
|
||||
DB = models.Database
|
||||
Slice = models.Slice
|
||||
@@ -66,12 +60,3 @@ def get_slice_json(defaults, **kwargs):
|
||||
d = defaults.copy()
|
||||
d.update(kwargs)
|
||||
return json.dumps(d, indent=4, sort_keys=True)
|
||||
|
||||
|
||||
def get_example_data(filepath, is_gzip=True, make_bytes=False):
|
||||
content = requests.get(f'{BASE_URL}{filepath}?raw=true').content
|
||||
if is_gzip:
|
||||
content = zlib.decompress(content, zlib.MAX_WBITS|16)
|
||||
if make_bytes:
|
||||
content = BytesIO(content)
|
||||
return content
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import datetime
|
||||
import gzip
|
||||
import os
|
||||
import random
|
||||
|
||||
import geohash
|
||||
@@ -24,7 +26,7 @@ from sqlalchemy import DateTime, Float, String
|
||||
from superset import db
|
||||
from superset.utils import core as utils
|
||||
from .helpers import (
|
||||
get_example_data,
|
||||
DATA_FOLDER,
|
||||
get_slice_json,
|
||||
merge_slice,
|
||||
misc_dash_slices,
|
||||
@@ -35,8 +37,8 @@ from .helpers import (
|
||||
|
||||
def load_long_lat_data():
|
||||
"""Loading lat/long data from a csv file in the repo"""
|
||||
data = get_example_data('san_francisco.csv.gz', make_bytes=True)
|
||||
pdf = pd.read_csv(data, encoding='utf-8')
|
||||
with gzip.open(os.path.join(DATA_FOLDER, 'san_francisco.csv.gz')) as f:
|
||||
pdf = pd.read_csv(f, encoding='utf-8')
|
||||
start = datetime.datetime.now().replace(
|
||||
hour=0, minute=0, second=0, microsecond=0)
|
||||
pdf['datetime'] = [
|
||||
|
||||
BIN
superset/data/multiformat_time_series.json.gz
Normal file
BIN
superset/data/multiformat_time_series.json.gz
Normal file
Binary file not shown.
@@ -14,6 +14,8 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import gzip
|
||||
import os
|
||||
|
||||
import pandas as pd
|
||||
from sqlalchemy import BigInteger, Date, DateTime, String
|
||||
@@ -22,7 +24,7 @@ from superset import db
|
||||
from superset.utils import core as utils
|
||||
from .helpers import (
|
||||
config,
|
||||
get_example_data,
|
||||
DATA_FOLDER,
|
||||
get_slice_json,
|
||||
merge_slice,
|
||||
misc_dash_slices,
|
||||
@@ -33,9 +35,8 @@ from .helpers import (
|
||||
|
||||
def load_multiformat_time_series():
|
||||
"""Loading time series data from a zip file in the repo"""
|
||||
data = get_example_data('multiformat_time_series.json.gz')
|
||||
pdf = pd.read_json(data)
|
||||
|
||||
with gzip.open(os.path.join(DATA_FOLDER, 'multiformat_time_series.json.gz')) as f:
|
||||
pdf = pd.read_json(f)
|
||||
pdf.ds = pd.to_datetime(pdf.ds, unit='s')
|
||||
pdf.ds2 = pd.to_datetime(pdf.ds2, unit='s')
|
||||
pdf.to_sql(
|
||||
|
||||
@@ -14,22 +14,24 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
|
||||
import pandas as pd
|
||||
from sqlalchemy import String, Text
|
||||
|
||||
from superset import db
|
||||
from superset.utils import core as utils
|
||||
from .helpers import TBL, get_example_data
|
||||
from .helpers import DATA_FOLDER, TBL
|
||||
|
||||
|
||||
def load_paris_iris_geojson():
|
||||
tbl_name = 'paris_iris_mapping'
|
||||
|
||||
data = get_example_data('paris_iris.json.gz')
|
||||
df = pd.read_json(data)
|
||||
df['features'] = df.features.map(json.dumps)
|
||||
with gzip.open(os.path.join(DATA_FOLDER, 'paris_iris.json.gz')) as f:
|
||||
df = pd.read_json(f)
|
||||
df['features'] = df.features.map(json.dumps)
|
||||
|
||||
df.to_sql(
|
||||
tbl_name,
|
||||
|
||||
BIN
superset/data/paris_iris.json.gz
Normal file
BIN
superset/data/paris_iris.json.gz
Normal file
Binary file not shown.
BIN
superset/data/random_time_series.json.gz
Normal file
BIN
superset/data/random_time_series.json.gz
Normal file
Binary file not shown.
@@ -14,6 +14,8 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import gzip
|
||||
import os
|
||||
|
||||
import pandas as pd
|
||||
from sqlalchemy import DateTime
|
||||
@@ -22,7 +24,7 @@ from superset import db
|
||||
from superset.utils import core as utils
|
||||
from .helpers import (
|
||||
config,
|
||||
get_example_data,
|
||||
DATA_FOLDER,
|
||||
get_slice_json,
|
||||
merge_slice,
|
||||
Slice,
|
||||
@@ -32,8 +34,8 @@ from .helpers import (
|
||||
|
||||
def load_random_time_series_data():
|
||||
"""Loading random time series data from a zip file in the repo"""
|
||||
data = get_example_data('random_time_series.json.gz')
|
||||
pdf = pd.read_json(data)
|
||||
with gzip.open(os.path.join(DATA_FOLDER, 'random_time_series.json.gz')) as f:
|
||||
pdf = pd.read_json(f)
|
||||
pdf.ds = pd.to_datetime(pdf.ds, unit='s')
|
||||
pdf.to_sql(
|
||||
'random_time_series',
|
||||
|
||||
BIN
superset/data/san_francisco.csv.gz
Normal file
BIN
superset/data/san_francisco.csv.gz
Normal file
Binary file not shown.
BIN
superset/data/sf_population.json.gz
Normal file
BIN
superset/data/sf_population.json.gz
Normal file
Binary file not shown.
@@ -14,22 +14,24 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
|
||||
import pandas as pd
|
||||
from sqlalchemy import BigInteger, Text
|
||||
|
||||
from superset import db
|
||||
from superset.utils import core as utils
|
||||
from .helpers import TBL, get_example_data
|
||||
from .helpers import DATA_FOLDER, TBL
|
||||
|
||||
|
||||
def load_sf_population_polygons():
|
||||
tbl_name = 'sf_population_polygons'
|
||||
|
||||
data = get_example_data('sf_population.json.gz')
|
||||
df = pd.read_json(data)
|
||||
df['contour'] = df.contour.map(json.dumps)
|
||||
with gzip.open(os.path.join(DATA_FOLDER, 'sf_population.json.gz')) as f:
|
||||
df = pd.read_json(f)
|
||||
df['contour'] = df.contour.map(json.dumps)
|
||||
|
||||
df.to_sql(
|
||||
tbl_name,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user