Compare commits

..

312 Commits

Author SHA1 Message Date
Maxime Beauchemin
08b88fd4d2 0.23.0rc2 2018-02-07 15:00:50 -08:00
Maxime Beauchemin
2e172d77cf Fix caching issues (#4316) 2018-02-07 14:49:19 -08:00
Maxime Beauchemin
8fe1f8fb3f Set default row_limit to 50k (#4372) 2018-02-07 14:34:14 -08:00
Maxime Beauchemin
c4eba9e467 [line] improve feature (#4363)
Require Since and Until parameter as they are needed to time shift
2018-02-07 14:33:04 -08:00
Maxime Beauchemin
90d9616f2b Remove dangerouslySetInnerHTML in StackTraceMessage component (#4373)
Druid sometimes returns error message that are contained in "<>", as in
`<urlopen error [Errno 61] Connection refused>`. Since Superset's
approach is often to bubble up messages coming from external library,
it's impossible to predict whether it will contain special characters.

There are some cases where our error handling does return some html
(presto?),
but we should manage that upstream. Plus the current setup has security concerns,
so let's move away from that.
2018-02-07 14:32:45 -08:00
Maxime Beauchemin
803738436e Bump python dependencies (#4341)
* Bump python dependencies

* Fix conflict
2018-02-07 14:32:29 -08:00
Beto Dealmeida
f14c1bb593 Add hour grain to Sqlite (#4333) 2018-02-07 14:07:15 -08:00
Maxime Beauchemin
31a0b6e5b0 [druid] fix bug around handling NULLs (#4358)
fillna would miss out on identifying STRING columns for Druid and
replace None in string columns with a numeric `0`. This
mixed type column would confuse
pandas down the line on some operations like `df.pivot_table`.
2018-02-07 08:19:48 -08:00
Xiao Hanyu
27538386bc Add ipdb to dev dependencies. (#4368)
Though flask has a builtin web debugger, ipdb some times still work
better. So I think add ipdb to dev dependencies is a good option for
people who prefer CLI debugging.
2018-02-07 08:19:18 -08:00
Teemu Haapoja
3b35ddf135 convert postgresql date_trunc() to UTC to prevent pandas error (#4319)
* cast postgresql date_trunc() to timestamp without time zone to prevent pandas error

* fix formatting for flake8

* change cast to timezone conversion instead
2018-02-07 08:18:11 -08:00
Xiao Hanyu
d5ab6c8d3d Remove useless empty npm-debug.log (#4367)
The log file comes from
a7a6678d5c

Also modify .gitignore to ignore all future npm-debug.log
2018-02-07 08:12:45 -08:00
Raffaele Spangaro
a4ecff4e23 New options for european time format in in D3_TIME_FORMAT_OPTIONS (#4364)
* Update installation.rst for Ubuntu 16.04 LTS 

Ubuntu 16.04 by default install python2.7 alongside with python 3.5 and set python2.7 as default. If you have created a virtualenv with python3.5 compilation fails due to wrong python-dev library installed. 

If you install ``python3.5-dev`` the build for the wheel package of  ``cryptography`` run fine.

* Add options in D3_TIME_FORMAT_OPTIONS for non-english Time Format.
Added '%d/%m/%Y' and '%d/%m/%Y %H:%M:%S' as valid drop-down option for Axis Format
2018-02-06 16:24:00 -08:00
michellethomas
19a0827d1f Adding dashboard add view (#4344)
* Adding DashboardAddView and setting show_columns on SLiceAddView

* Adding tests for sliceaddview read api

* Dedupe show_columns
2018-02-06 12:38:07 -08:00
timifasubaa
2d8a0cc6c9 fix uri form data' (#4345)
'
2018-02-05 20:49:37 -08:00
Hugh A. Miles II
2789385688 read query params for json in dashboard endpoint (#4337) 2018-02-05 11:48:12 -08:00
Grace Guo
e965f95477 1. fix check filters change logic (#4339)
2. should show chart after loading completed
2018-02-05 10:21:17 -08:00
liutgnu
ad212272d1 Fix the bug of charts/slices cannot be filtered by datasource name. (#4338) 2018-02-04 23:03:44 -08:00
timifasubaa
6d37d97ba5 Refactor import csv (#4298)
* move helpers to utils

* make form use queryselector

* refactor exception throwing and handling

* update db_connection access point

* nits
2018-02-03 20:22:06 -08:00
Jeff Niu
fdd42ef4b6 [New Viz] Nightingale Rose Chart (#3676)
* Nightingale Rose Chart

* Review comments
2018-02-03 20:18:24 -08:00
Maxime Beauchemin
a616bf4082 [cli] permission cleanup on 'superset init' (#4241)
* [cli] permission cleanup on 'superset init'

FAB sometimes creates NULL/None permissions in the database,
presumably a race condition when multiple gunicorn workers start at the
same time, and those create issues raising "AttributeError: 'NoneType'
object has no attribute 'name'"

* Linting
2018-02-03 20:12:45 -08:00
fabianmenges
a9e1e685ba [BugFix]: Creating a PostgresBaseEngineSpec so changes to the Postgre… (#4224)
* [BugFix]: Creating a PostgresBaseEngineSpec so changes to the PostgresEngineSpec don't affect every subclass

* Empty engine for abstract Engine
2018-02-03 20:03:02 -08:00
Maxime Beauchemin
d41418eaa0 Bump pyrdruid to 0.4.0 (#4325) 2018-02-03 19:48:38 -08:00
Maxime Beauchemin
1f8fccc0f9 [explore] fix missing CacheLabel (#4326)
Also adding a basic unit test.
2018-02-02 10:28:38 -08:00
Raffaele Spangaro
75a2b4f610 Update installation.rst for Ubuntu 16.04 LTS (#4321)
Ubuntu 16.04 by default install python2.7 alongside with python 3.5 and set python2.7 as default. If you have created a virtualenv with python3.5 compilation fails due to wrong python-dev library installed. 

If you install ``python3.5-dev`` the build for the wheel package of  ``cryptography`` run fine.
2018-01-31 18:03:16 -08:00
Grace Guo
133f98ad58 [Bug] Resize should trigger chart re-render (#4322) 2018-01-31 18:02:16 -08:00
Grace Guo
1a7ef4758b [Explore] Fix Stop Query Button behavior (#4301) 2018-01-31 09:34:08 -08:00
Beto Dealmeida
c77bab8160 Refactoring deckgl (#4293)
* Refactoring dekgl

* Refactor layers

* Standardize function name

* Fix exports

* Fix require

* Fix lint
2018-01-30 23:03:35 -08:00
Grace Guo
724c3f48a4 add frontend logging utility function (#4226)
add loading log for dash and exploreview
breakdown whole page load action to multiple charts loading events and render events
2018-01-30 10:27:13 -08:00
Hugh A. Miles II
073d56cb33 Added Path, Polygon, and Arcs to deckGL example dashboard (#4242)
* Added Path, Polygon, and Arcs to deckGL example dashboard

* reorder task

* formatting...

* fix flights reference

* cleanup on aisle 9
2018-01-29 11:51:36 -08:00
Maxime Beauchemin
e4a95f9428 Use the query_obj as the basis for the cache key (#4260)
* Use the query_obj as the basis for the cache key

When we recently moved from hashing form_data to define the cache_key
towards using the rendered query instead,
it made is such that non deterministic form
control values like relative times specified in "from" and "until" time
bound resulted in making those miss cache 100% of the time.

Here we move away from using the rendered query and using the query_obj
instead.

* Deprecating using form_data in templates
2018-01-28 09:46:13 -08:00
Riccardo Magliocchetti
1b06140bde Bump sqlalchemy to 1.2.2 (#4299)
* Bump sqlalchemy to 1.2.2

Fix #4196

* Bump sqlalchemy-utils to 0.32.21
2018-01-28 09:32:41 -08:00
Maxime Beauchemin
f8dcbf70c5 Reverts apache/incubator-superset#4244 (#4303) 2018-01-26 18:09:09 -08:00
michellethomas
b9299d61ac Fixing json decode error on druiddatasourcemodelview/api/read (#4291) 2018-01-25 17:12:34 -08:00
Hugh A. Miles II
2384ad4eb5 [geo] Add JS controls to remaining layers (#4272)
* Update viz.py

* added JS controls

* add JS to grid layout

* add JS to hexagon layer

* added JS controls to screengrid

* update to js_data_mutator controls

* remove .map()
2018-01-25 14:07:34 -08:00
Maxime Beauchemin
2b66eadee2 Set point size control's default for deck_scatter viz (#4261) 2018-01-24 16:49:37 -08:00
Maxime Beauchemin
94d9337e0b deck_multi to pass down filters to layers (#4270)
Filters applied to deck_multi will be passed down to layers as.

If the column isn't set as "filterable", the filter is ignored.

Also note that Dashboard configuration in regards to
"filter_immune_slices" and such will be disregarded in this context as
it isn't the dashboard controller passing down the filter and that
context is not easily accessible here.
2018-01-24 16:26:31 -08:00
Maxime Beauchemin
a0621e10a8 Handle 'pd.Timestamp' when jsonifying (#4275) 2018-01-24 16:09:22 -08:00
Maxime Beauchemin
b72d5b03dc Prevent FilterBox extra query (#4276)
closes https://github.com/apache/incubator-superset/issues/4249
2018-01-24 16:09:03 -08:00
Maxime Beauchemin
914480ad3c Fix SUPERSET_WEBSERVER_TIMEOUT in VisualizeModal (#4277)
* Fix SUPERSET_WEBSERVER_TIMEOUT in VisualizeModal

* Fix test

* lint
2018-01-24 16:08:48 -08:00
Maxime Beauchemin
ff2f85f39b [geo] JS function to receive the whole data array instead of individual object (#4262)
Moving from having the user define an interceptor function that operates
on one object at a time.

By passing the entire array, it's possible to do multiple pass where
needed. A common pattern might be to figure out the max value in order
to define a scaler function. That's only possible if dealing with the
whole array.
2018-01-24 13:16:14 -08:00
Maxime Beauchemin
9cf16a4ff2 Fix click on now in DateFilterControl (#4265)
When clicking on `now` or the infinity sign, the popover closes but the
value doesn't show in the label as expected.
2018-01-24 13:15:44 -08:00
Maxime Beauchemin
b90c410c01 [explore] fix empty query message in 'View Query' (#4273) 2018-01-24 13:14:38 -08:00
Xiao Hanyu
77d1e5d046 Always use fluid container for navbar. (#4279)
As in https://github.com/apache/incubator-superset/pull/4147 removes the
final non fluid container navbar, I think there's no need to keep this
line of code. Just use navbar with fluid container, always.
2018-01-24 08:55:57 -08:00
Marcus Levine
4bc5fe5495 [BUGFIX]: Check datatype of results before converting to DataFrame (#4108)
* conditional check on datatype of results before converting to df

fix type checking

fix conditional checks

remove trailing whitespace and fix df_data fallback def

actually remove trailing whitespace

generalized type check to check all columns for dict

refactor dict col check

* move df conversion to helper and add unit test

add missing newlines

another missing newline

fix quotes

more quote fixes
2018-01-23 20:58:06 -08:00
timifasubaa
2c72a7ae4f Use json for imports and exports, not pickle (#4243)
* make superset imports and exports use json, not pickle

* fix tests
2018-01-23 20:55:27 -08:00
Maxime Beauchemin
4b11f45f72 Using a NullPool for external connections by default (#4251)
Currently, even though `get_sqla_engine` calls get memoized, engines are
still short lived since they are attached to an models.Database ORM
object. All engines created through this method have the scope of a web
request.

Knowing that the SQLAlchemy objects are short lived means that
a related connection pool would also be short lived and mostly useless.
I think it's pretty rare that connections get reused within the context
of a view or Celery worker task.

We've noticed on Redshift that Superset was leaving many connections
opened (hundreds). This is probably due to a combination of the current
process not garbage collecting connections properly, and perhaps the
absence of connection timeout on the redshift side of things. This
could also be related to the fact that we experience web requests timeouts
(enforced by gunicorn) and that process-killing may not allow SQLAlchemy
to clean up connections as they occur (which this PR may not help
fixing...)

For all these reasons, it seems like the right thing to do to use
NullPool for external connection (but not for our connection to the metadata
db!).

Opening the PR for conversation. Putting this query into our staging
today to run some tests.
2018-01-23 15:13:50 -08:00
Maxime Beauchemin
04ae004f43 Set 'Range Filter' default to false (#4264)
It got set to true mistakenly.
2018-01-23 11:23:41 -08:00
Andres Botero
29ef8c4af8 Fix heatmap tooltip disappears under the slice's header (#4268) 2018-01-23 10:16:14 -08:00
Maxime Beauchemin
718230cdf2 Bump flower==0.9.2 (#4263) 2018-01-23 10:15:10 -08:00
John Bodley
8175e19f72 [cache] Fixing json.dumps for timestamp (#4240) 2018-01-19 12:10:39 -08:00
Grace Guo
7b76356182 [Sql Lab] Fix query results display at the bottom of screen (#4246) 2018-01-19 10:19:54 -08:00
Grace Guo
1c56319be4 [Sql Lab] Fix Autorefresh component pulling not stopped. (#4244) 2018-01-19 08:57:23 -08:00
Maxime Beauchemin
36caca3244 Fix 'argument to reversed() must be a sequence' (#4237)
When passing empty/null location data out of certain rows in the spatial
control, Superset raises an error when trying to reverse the tuple.
2018-01-18 15:22:22 -08:00
Hugh A. Miles II
5079b2aa95 Added DeckGL.Polygon Layer w/ JS controls (#4227)
* Working polygon layer for deckGL

* add js controls

* add thumbnail

* better description

* refactor to leverage line_column controls

* templates: open code and documentation on a new tab (#4217)

As they are external resources.

* Fix tutorial doesn't match the current interface #4138 (#4215)

* [bugfix] markup and iframe viz raise 'Empty query' (#4225)

closes https://github.com/apache/incubator-superset/issues/4222

Related to: https://github.com/apache/incubator-superset/pull/4016

* [bugfix] time_pivot entry got missing in merge conflict (#4221)

PR here https://github.com/apache/incubator-superset/pull/3518 missed a
line of code while merging conflicts with time_pivot viz

* Improve deck.gl GeoJSON visualization (#4220)

* Improve geoJSON

* Addressing comments

* lint

* refactor to leverage line_column controls

* refactor to use DeckPathViz

* oops
2018-01-18 13:28:46 -08:00
Hugh A. Miles II
cab8e7d22d remove setting spatial in DeckPathViz class (#4235) 2018-01-18 12:03:46 -08:00
michellethomas
85d137b20a Don't cache if there's no cache key (#4229) 2018-01-18 08:28:26 -08:00
Peter Lubell-Doughtie
a942f81dfd add Ona as a user (#4234) 2018-01-18 08:27:56 -08:00
Maxime Beauchemin
01043c9bf4 Improve deck.gl GeoJSON visualization (#4220)
* Improve geoJSON

* Addressing comments

* lint
2018-01-17 14:01:32 -08:00
Maxime Beauchemin
a9610e2886 [bugfix] time_pivot entry got missing in merge conflict (#4221)
PR here https://github.com/apache/incubator-superset/pull/3518 missed a
line of code while merging conflicts with time_pivot viz
2018-01-17 13:54:45 -08:00
Maxime Beauchemin
5897d85f7a [bugfix] markup and iframe viz raise 'Empty query' (#4225)
closes https://github.com/apache/incubator-superset/issues/4222

Related to: https://github.com/apache/incubator-superset/pull/4016
2018-01-17 13:54:10 -08:00
Yongjie Zhao
0367dce38b Fix tutorial doesn't match the current interface #4138 (#4215) 2018-01-16 21:18:00 -08:00
Riccardo Magliocchetti
1ca1395382 templates: open code and documentation on a new tab (#4217)
As they are external resources.
2018-01-16 21:17:27 -08:00
michellethomas
2607e4be4d Adding limit to time_table viz to get druid query to work (#4207) 2018-01-16 10:27:35 -08:00
Maxime Beauchemin
04680e5ff1 [line chart] fix time shift color (#4202) 2018-01-12 15:10:17 -08:00
John Bodley
a7a6678d5c [cache] Using the query as the basis of the cache key (#4016) 2018-01-12 12:05:12 -08:00
Maxime Beauchemin
8069d6221d [druid] fix 2 phases queries that specify 'Sort By' on 'Series limit' (#4203) 2018-01-12 11:29:24 -08:00
Maxime Beauchemin
269f55c29a [bugfix] dealing with DBAPIs that return unserilizable types (#4200)
Funky datatypes in some databases like BLOBs will have the DBAPI return
python types that can't be serialized to JSON out of the box.

Currently, when this happens SQL Lab fails in a bad way with a gigantic
HTML error message.

This allows specifying a pessimistic JSON serializer handler that will
simply show "Unserializable [type]"
2018-01-12 11:11:31 -08:00
Hugh A. Miles II
bca27b436b [Geo] Added DeckGL Arc Layer and Refactor on BaseDeckGL class (#4134)
* Added DeckGL.arc layer

* added color controls

* added stroke_width control

* added process spatial key methods

* change exception to ValueError

* put location into tuple

* reference global spatial keys array

* linting

* refactor on process_spatial_data_obj

* rm whitespace

* refactor arc.get_data

* Revert "refactor arc.get_data"

This reverts commit 8d01b2a22e.

* add spatial controls array

* refactor on spatial keys again :)

* return altered df

* Working refactor with deckGL Arcs

* working arcs refactor :)

* refactored all other deckGL viz types
2018-01-12 11:06:11 -08:00
Benedict Jin
aecaa85905 Hanization (#4126)
* Hanization

* Hanization step two

* 1. Update mo & json file; 2. Remove necessary msgid & msgstr; 3. Fix error python-format; 4. Other improvements

* Hanization step three

* Hanization step four (49%)

* Translate chart options

* Translate Search

* Translate view results Data preview new table name 10 seconds 30 seconds 1 minute 5 minutes

* Hanization & update mo/json files

* Update filter translation

* Hanization step 5th (60%)

* Hanization step 6th (70%)

* Hanization step 7th (80%)

* Hanization step 8th (83%)
2018-01-12 08:11:37 -08:00
bolkedebruin
7e36488f03 Superset was using undefined metrics for specifying limits (#4114)
in case a form did not specify a metric (e.g. mapbox). If it is
not available it now defaults to the first dimension instead.

This fixes issue #3604
2018-01-11 21:45:34 -08:00
Maxime Beauchemin
87c3e831a8 Using user-defined Javascript to customize geospatial visualization (#4173)
* Using JS to customize spatial viz and tooltips

* Add missing deck_multi.png

* Improve GeoJSON layer with JS support and extra controls

* Addressing comments
2018-01-11 15:42:44 -08:00
Maxime Beauchemin
ee63ebc8ec [datasource editor] click checkbox creates metrics instantly (#4183)
* [datasource editor] click checkbox creates metrics instantly

* Fix tests
2018-01-11 15:42:19 -08:00
Maxime Beauchemin
5916291901 [explore] fix json highlighting for Druid queries (#4201) 2018-01-11 15:41:28 -08:00
Maxime Beauchemin
4b0f252170 Sort out dependencies in travis/tox (#4186)
* Make travis a bit more lean

* Bump npm to 5.6.0
2018-01-10 21:46:10 -08:00
Beto Dealmeida
9176a4072b Enable SQL syntax highlighting in View Query (#4184)
* Enable SQL syntax highlighting in View Query

* Enable SQL syntax highlighting in View Query
2018-01-10 20:49:28 -08:00
John Bodley
0cb7c5e4a6 [annotations] Fixing migration for annotation layers (#4187) 2018-01-10 08:50:05 -08:00
Yongjie Zhao
e182f7f962 fix since or until is empty value #4170 (#4176) 2018-01-09 16:54:18 -08:00
fabianmenges
23c98294bd Moving the custom_password_store out of Database class (#4182) 2018-01-09 13:14:20 -08:00
John Bodley
22bdd9e324 [security] Adding all derived FAB UserModelView views to admin only (#4180) 2018-01-09 13:05:37 -08:00
Maxime Beauchemin
b159e51787 Don't use fully qualified column names in metric definitions (#4101)
When generating an auto SUM() metric on a column, Superset currently
will go `SUM(table_name.column_name)`. This is an issue when moving to
point to another table. It's common to work on some temporary table or
work table and eventually need to point Superset to an alternate table.
2018-01-08 22:03:37 -08:00
John Bodley
d57012067b [FAB] configuring updating of permissions (#4172) 2018-01-08 14:39:18 -08:00
timifasubaa
9364fb5b79 Allow alpha role import csv (#4164)
* allow alphas upload csv

* nits
2018-01-08 13:36:30 -08:00
Maxime Beauchemin
c49fb0aa9b Make Welcome page into a simple React app (#4147)
* Make Welcome page into a simple React app

This removes a dependency on datatables, we should be able to get rid
of it as we re-write the Table and PivotTable viz

* tests/lint

* Bump node version to latest
2018-01-07 22:13:06 -08:00
Grace Guo
b9af019567 Fix chart rendering error in time series table (#4156) 2018-01-06 16:49:59 -08:00
Grace Guo
e7f8143c3b [Bug] Closing change datasource modal throws JS error (#4157) 2018-01-05 14:21:52 -08:00
Alexander Tronchin-James
c9e47f0bb3 Check for non-None database before using. (#4162)
Some valid sqlalchemy uri's return a URL object with database=None, which causes the following error:
```
2018-01-05 17:59:47,560:ERROR:root:argument of type 'NoneType' is not iterable
Traceback (most recent call last):
  File "/opt/incubator-superset/superset/sql_lab.py", line 186, in execute_sql
    user_name=user_name,
  File "/opt/incubator-superset/superset/utils.py", line 124, in __call__
    return self.func(*args, **kwargs)
  File "/opt/incubator-superset/superset/models/core.py", line 644, in get_sqla_engine
    url = self.db_engine_spec.adjust_database_uri(url, schema)
  File "/opt/incubator-superset/superset/db_engine_specs.py", line 505, in adjust_database_uri
    if '/' in database:
TypeError: argument of type 'NoneType' is not iterable
```
This patch corrects that problem.
2018-01-05 13:54:17 -08:00
Beto Dealmeida
686023c8dd Druid support via SQLAlchemy (#4163)
* Use druiddb

* Remove auto formatting

* Show prequeries

* Fix subtle bug with lists

* Move arguments to query object

* Fix druid run_query
2018-01-05 13:52:58 -08:00
Benedict Jin
d997a450cf Fix invaild gitter url (#4125) 2018-01-04 14:45:58 -08:00
zhao yongjie
9e053923d4 Adding Apache Kylin datasource for documentation (#4148) 2018-01-03 20:24:15 -08:00
Leonardo Rochael Almeida
ef06a9d497 Create DATA_DIR after importing config (#4143)
Delay creating DATA_DIR until config is fully imported.

This allows superset_config.py to override DATA_DIR before superset
attempts to create it in a potentially unwriteable location.
2018-01-03 09:54:59 -08:00
Maxime Beauchemin
37205099db Fix USA's state geojson for 'Country Map' visualization (#4121)
* Fix USA's state geojson for 'Country Map' visualization

Turns out the ISO codes were missing from the geojson file, this adds it
and uses human-readable indents.

* using proper ISO codes

* Linting

New linting rules started applying, I'm guessing a new version of
pylint?
2018-01-02 20:21:33 -08:00
timifasubaa
e498f2fcb6 fix variable name (#4139) 2018-01-02 14:32:24 -08:00
Jeff Niu
f7c55270db Remedy for dual axis annotation (#4130) 2018-01-02 14:31:16 -08:00
Maxime Beauchemin
0a6208296e [explore] add datasource metadata (#4104) 2018-01-02 08:41:27 -08:00
Hugh A. Miles II
bf4d3a0dff better thumbnail for deck_geojson (#4135) 2017-12-28 13:38:07 -08:00
Hugh A. Miles II
b227612f6e Added guard statement for spatial controls (#4124) 2017-12-26 12:27:27 -08:00
Maxime Beauchemin
45686a1af6 Multi layers DECK.GL visualization (#4096)
* Multi layers DECK.GL viz

* Fix tests

* rebasing

* Fix error handling in chartActions

* Addressing comments
2017-12-26 10:47:29 -08:00
Benedict Jin
82ed4878c4 Fix rst grammar problems (#4116) 2017-12-25 23:39:28 -08:00
James Pesculis
6e1ec8347d Update UserInfo.jsx and set additional properties for react-gravatar (#4118) 2017-12-25 23:29:27 -08:00
Hugh A. Miles II
f905726c24 [geo] Added DeckGL GeoJson layer (#4097)
* added deckgl geojson layer

* linting

* fixed comments

* addressed comments

* added override with controls.color_picker > 0

* set var properly

* set colors if property doesnt exist at all

* refacator on property mapping
2017-12-22 14:40:08 -08:00
Maxime Beauchemin
69195f8d2d Introduce Javascript controls (#4076)
* Introduce Javascript controls

This allows power-users to perform intricate transformations on data and
objects using javascript code.

The operations allowed are "sanboxed" or limited using node's vm
`runInNewContext`
https://nodejs.org/api/vm.html#vm_vm_runinnewcontext_code_sandbox_options

For now I'm only enabling in the line chart visualization, but the plan
would be to go towards offering more power to people who can write some
JS moving forward.

* Not applied
2017-12-20 21:24:35 -08:00
Nicolas Bonnotte
b4909f2d03 [Bugfix] Issues with merge_extra_filters (#4042) (#4091) 2017-12-20 16:22:43 -08:00
Maxime Beauchemin
44e753d94d [sql lab] deeper support for templating (#3996)
* [sql lab] deeper support for templating

* Fixing py tests

* Fix typo
2017-12-19 15:55:58 -08:00
Maxime Beauchemin
e4903e6dc6 [geo] add support for deck.gl's path layer (#4067)
* [geo] add support for deck.gl's path layer

Works with json and polyline data.

* Lint
2017-12-19 12:38:03 -08:00
Maxime Beauchemin
d4e8d57fc4 Using TextAreaControl for WHERE and HAVING clause section (#4090) 2017-12-19 12:11:35 -08:00
kuriancheeramelil
281ae45495 Fix for SQL editor throwing can't deserialize google.cloud.bigquery._helpers.Row with BigQuery (#4071)
* fix for SQL editor throwing cant deserialize google.cloud.bigquery._helpers.Row with BigQuery

* linted code

* disable pylint import error of bigquery row

* fixed spacing issue before inline-comment
2017-12-18 21:22:34 -08:00
fabianmenges
ff4f9b4527 Bugfix: Druid having filters are broken (#4089) 2017-12-18 17:06:12 -08:00
fabianmenges
86f9087ea2 Event annotation should have min width (#4083) 2017-12-18 15:37:05 -08:00
Maxime Beauchemin
7cd9b85831 [bugfix] iframe and markup are broken (#4082)
fixes https://github.com/apache/incubator-superset/issues/4080
2017-12-18 14:42:18 -08:00
fabianmenges
71e1eea9f4 DB migration of annotation_layers on slice objects and slimming down annotation object. (#4072) 2017-12-18 13:11:06 -08:00
Jeff Niu
1e79e9cd2a [Bugfix] Issues with table filtering (#4073)
* Fixing some issues with table filtering

* Added some comments
2017-12-17 15:43:34 -08:00
Jeff Niu
af7cdeba4d [Feature] enhanced memoized on get_sqla_engine and other functions (#3530)
* added watch to memoized

* added unit tests for memoized

* code style changes
2017-12-17 10:35:00 -08:00
fabianmenges
500e6256c0 Full Annotation Framework (#3518)
* Adding full Annotation Framework

* Viz types

* Re organizing native annotations

* liniting

* Bug fix

* Handle no data

* Cleanup

* Refactor slice form_data to data
2017-12-16 16:10:45 -08:00
Nic
e79d05fd77 #4058 Fix Oracle timestamps (Oracle "ORA-00907: missing right parenthesis" error) (#4065) 2017-12-15 21:31:09 -08:00
Maxime Beauchemin
fc85756c20 [geo] turn off renderTrigger on viewport control (#4066)
For context, the viewport gets updated dynamically from the user
actions on the map. This is done on a timer every second or so to keep
the form data updated with the viewport settings.

With renderTrigger=true on that control that generates re-renders which
introduces glitches while zooming/panning. So turning it off as we don't
really expect users to input viewport info directly in the control
anyways.
2017-12-15 14:59:54 -08:00
John Bodley
6081f7161a [health] Adding DB check to /health (#4062) 2017-12-15 14:59:41 -08:00
Maxime Beauchemin
c21513fb8c Adding rowcount label to explore view header (#4059) 2017-12-15 11:47:44 -08:00
Maxime Beauchemin
ec752b1378 [geo] provide more flexible Spatial controls (#4032)
Before this PR the only way to query lat/long is in the shape of 2
columns that contains lat and long.

Now we're adding 2 more options:
* a single column that has lat and long with a delimiter in between
* support for geohashes - geohashes are cool
2017-12-15 11:47:27 -08:00
Maxime Beauchemin
cf1d9ce1e6 Add db_engine_spec for Druid (#4063)
The `druiddb` pypi package provides a dbapi and sqlalchemy dialect for
Druid. This PR hooks adds some superset-specific conf.
2017-12-15 11:47:00 -08:00
Maxime Beauchemin
6188d60fec Bump dev version on trunk (#4048) 2017-12-12 21:29:19 -08:00
Maxime Beauchemin
dfc28f37eb Changelog for 0.21.0 (#4045)
* Changelog for 0.21.0

* more entries
2017-12-12 21:29:01 -08:00
Maxime Beauchemin
23c834f04e Fix the pypi build (#4047) 2017-12-12 18:12:26 -08:00
Hugh A. Miles II
c84211ec44 Change reference for slices to chart (#4049)
* change reference for slices to chart

* change profile page reference

* change reference for Associated Slices

* change back to single quotes

* fix other single quotes

* linting

* last one

* fix test
2017-12-12 18:02:17 -08:00
Jeff Niu
7d374428d3 [Bugfix] _add_filters_from_pre_query doesn't handle dim specs (#3974)
* Fixed _add_filters_from_pre_query for dimension specs

* add_filters_from_pre_query ignores extraction functions
2017-12-11 12:35:25 -08:00
John Bodley
3a2974f589 [API] Deprecate /update_role/ API endpoint (#4041) 2017-12-10 21:14:15 -08:00
timifasubaa
3ed8f5fc23 resolve python2 str() issue (#4030)
* make string conversion use utf8

* Update viz.py

* make utf-8 consistent with other usages

* Update viz.py

* Update viz.py
2017-12-09 14:23:24 -08:00
Grace Guo
61755f0b7d apply custom css for dashboard initially load (#4031) 2017-12-07 16:56:40 -08:00
fabianmenges
0a3d2fccd4 [BUGFIX]: Fixing dttm_sql_literal to use python_date_format when specified. (#3891) 2017-12-07 16:38:22 -08:00
Maxime Beauchemin
0b40c8a26f Add fastdom js dependency (#3947)
The nvd3 docs say that it if the fastdom library is present it makes use
of it.

"Including Fastdom in your project can greatly increase the performance
of the line chart (particularly in Firefox and Internet Explorer) by
batching DOM read and write operations to avoid layout thrashing. NVD3
will take advantage of Fastdom if present."
2017-12-07 16:36:46 -08:00
Beto Dealmeida
81df7087db Remove unused callbacks when setting state (#4015) 2017-12-06 21:56:28 -08:00
Jeff Niu
cb7c5aa70c Fixed finding postaggregations (#4017) 2017-12-06 21:55:43 -08:00
Maxime Beauchemin
5bc581fd44 New time_pivot visualization (#3941)
* New time_pivot visualization

* Minor tweaks

* Addressing comments
2017-12-06 21:50:33 -08:00
Maxime Beauchemin
5ee70b244b Add type MONEY as numeric type (#3959)
fixes https://github.com/apache/incubator-superset/issues/3953
2017-12-06 21:49:42 -08:00
Maxime Beauchemin
a26cf001c4 Add row_limit to heatmap controls (#3969) 2017-12-06 21:49:27 -08:00
Dmitry Goryunov
e02d35ed5c Add support of another DatabaseError format (#4019) 2017-12-06 21:49:03 -08:00
rumbin
e98a1c3537 asciifying http header for csv download; fixes #3952 (#3975)
* asciifying http header for csv download; fixes #3952

* fixed order of imports and added unidecode to requirements in setup.py
2017-12-05 12:04:58 -08:00
timifasubaa
4404751a1d Add has_access to import_dashboard (#4001)
* Add has_access to import_dashboard

* Update core.py

* Update core.py
2017-12-05 12:03:13 -08:00
Maxime Beauchemin
defe6789c0 [sql lab] fix position of 'save query' Popover (#3999)
* [sql lab] fix position of 'save query' Popover

The "Save Query" popover renders on the upper left corner as opposed to
where it should (relative to the Save Query button). After a bit of
research, it seems like Popover won't render in the right place when
parents are absolute.

I'm guessing this stopped working properly when I added the resizable
panes.

Anyhow, the solution here is to use a modal instead.

* Fixing tests
2017-12-05 11:37:13 -08:00
Beto Dealmeida
823f306f24 Call props.onChange only when closing filter (#4003) 2017-12-05 11:17:12 -08:00
fabianmenges
72627b1761 Adding YAML Import-Export for Datasources to CLI (#3978)
* Adding import and export for databases

* Linting
2017-12-05 11:14:52 -08:00
Alan Cruickshank
1702b020be Rollback bulk-delete of table columns (#4009)
Fix likely required upstream in FAB before we can properly enable this.
2017-12-05 11:14:21 -08:00
Grace Guo
89f6ccc1c6 Add Datasource Name filter under slice list view (#4000) 2017-12-04 13:45:36 -08:00
Maxime Beauchemin
eff5952641 Alternate PR to #3970 (#3997) 2017-12-04 08:19:51 -08:00
rumbin
f10395b2f7 [doc] added setting X-Forwarded-Proto to https behind reverse proxy with ssl encryption; fixes #3655 (#3976) 2017-12-03 22:00:58 -08:00
Maxime Beauchemin
b2647567c0 Create CODE_OF_CONDUCT.md (#3991) 2017-12-02 14:57:54 -08:00
Grace Guo
028456572b [Dashboard] fix a filter refresh bug and add Test (#3967) 2017-12-01 10:58:55 -08:00
Maxime Beauchemin
84a7730f47 [docs] making it clear sqlite shouldn't be used in a cluster (#3965) 2017-11-30 22:06:16 -08:00
Chris Williams
76a2f95231 [time series table] visual improvements (#3957)
* [time series table] visual improvements

* [time series table] don't set cell color if text color isn't set
2017-11-30 20:48:17 -08:00
michellethomas
9904593dc3 Improving speed of dashboard import (#3958)
* Improve dashboard import

* Updating tests for Slice.import_obj
2017-11-30 20:47:22 -08:00
Jeff Niu
8f00e9e30b [Bugfix] Druid run_query dimensions part 3 + Unit tests (#3949)
* Fixed and added tests for druid run query

* Fixes style and python3
2017-11-30 20:32:53 -08:00
Maxime Beauchemin
16ab696d7c [country_map] use Albers USA projection (#3946)
* [country_map] use Albers USA projection

* Minor touchups

* Adding color scheme
2017-11-30 14:09:53 -08:00
Maxime Beauchemin
1ce14df43d fix 'superset db history' (#3948)
* fix 'superset db history'

Related Error msg when running `superset db history`:
"NameError: Can't invoke function 'get_bind', as the proxy object has not
yet been established for the Alembic 'Operations' class.  Try placing
this code inside a callable."

* Lint
2017-11-29 20:52:56 -08:00
michellethomas
34d6618b2e Allow underscores in slugs (#3951)
* Allow underscores in slugs

* Switching regex to use shorter \w
2017-11-29 14:55:13 -08:00
Riccardo Magliocchetti
abdd1d537f config: bring back sqlite default database (#3955)
That became postgres in 268edcf

Fix #3954
2017-11-29 10:03:01 -08:00
Maxime Beauchemin
d9fda346cb Add an "Edit Mode" to Dashboard view (#3940)
* Add a togglable edit mode to dashboard

* Submenu for controls

* Allowing 'Save as' outside of editMode

* Set editMode to false as default
2017-11-28 09:10:21 -08:00
Jeff Niu
6cbe0e6096 Fixed branching condition with dimension spec (#3920) 2017-11-27 21:12:20 -08:00
timifasubaa
268edcfedd Import CSV (#3643)
* add upload csv button to sources dropdown

* upload csv to non-hive datasources

* upload csv to hive datasource

* update FAQ page

* add tests

* fix linting errors and merge conflicts

* Update .travis.yml

* Update tox.ini
2017-11-27 21:07:12 -08:00
Maxime Beauchemin
c5ddf57124 Fix call in Chart (#3945) 2017-11-27 21:06:47 -08:00
kkalyan
f9202ba179 minor filter select enhancements (#3933)
* `values_for_column` configurable row limit

* `FilterControl` cancels active ajax request if any
2017-11-27 21:05:53 -08:00
Alan Cruickshank
17635e1a2b Make Table Columns & Metrics Bulk-deletable (#3929) 2017-11-25 23:05:51 -08:00
John Bodley
285197926e [travis] Standardizing before_install (#3922) 2017-11-21 22:10:17 -08:00
Jeff Niu
5466fab2a0 Switched to span instead of textarea for copytoclipboard (#3923) 2017-11-21 22:09:41 -08:00
Jeff Niu
ed85032277 Moved percent metrics to its own row (#3924) 2017-11-21 22:08:15 -08:00
Maxime Beauchemin
680e1cbb42 Revert "Filter out unavailable databases (#3875)" (#3918)
This reverts commit ae2205aeb5.
2017-11-21 12:58:47 -08:00
Maxime Beauchemin
2d37dec5ff [bugfix] remove quotes from Postgres time grains (#3913) 2017-11-21 11:24:17 -08:00
Maxime Beauchemin
3f4c306bd6 Fix left padding in dashboard widgets (#3915) 2017-11-21 10:24:28 -08:00
Maxime Beauchemin
ac432495d7 [cosmetic] remove border from table viz (#3916) 2017-11-21 10:23:48 -08:00
michellethomas
12fb7c1a62 When checking if you should renderTriggered make sure key exists in controls (#3912) 2017-11-21 10:22:55 -08:00
Yu Xiao
feb15a30a2 fix the schema-fetching problem for impala in sql_lab (#3906)
* fix the schema-fetching problem for impala in sql_lab

* delete redundant print

* remove blank lines...

* minior corrections
2017-11-21 09:28:31 -08:00
Alan Cruickshank
eb0f3970cf Add UK Metropolitan Districts and Isle of Man (#3911) 2017-11-20 18:06:58 -08:00
Maxime Beauchemin
b82d15af76 Bumping webpack related deps (#3904) 2017-11-20 15:27:02 -08:00
Maxime Beauchemin
32b38ee2d6 [bugfix] allow limiting word cloud (#3902) 2017-11-20 10:32:14 -08:00
Maxime Beauchemin
1d702f2142 Fixes default hanlding in Altered slice tag (#3903) 2017-11-20 08:43:38 -08:00
bolkedebruin
4ae77ba8af Workaround pandas bug in datetimes with time zones (#3910)
A bug in to_dict(orient="records") in pandas/core/frame.py prevents
datetimes with time zones to be worked with. This works around the
issue in superset by re-implementing the logic of pandas in the
correct way. Until pandas fixes the issue this code should stay.

https://github.com/pandas-dev/pandas/issues/18372

This closes #1929
2017-11-20 08:33:18 -08:00
John Bodley
3c72e1f8fb [3541] Augmenting datasources uniqueness constraints (#3583) 2017-11-19 20:09:18 -08:00
John Bodley
4bfe08d7c3 [druid] Fixing issue 3894 multi-processing w/ Gunicorn (#3895) 2017-11-18 21:40:40 -08:00
John Bodley
3a7ed8d194 [druid] Catch IOError when fetching Druid datasource time boundary (#3897) 2017-11-17 20:20:47 -08:00
John Bodley
4d204b3b36 [druid] Renaming refresh_async method (#3899) 2017-11-17 20:11:44 -08:00
Alan Cruickshank
39ee33aeff Add datasource to the SliceAddView modal (#3884) (#3900) 2017-11-17 20:11:23 -08:00
Grace Guo
831cd21737 [dashboard bug]Instant control should take effect instantly (#3890)
in explore view, controls like color cheme, legend, rich tooltip, etc., change these controls should see effect instantly, without click Run Query.
2017-11-17 16:34:53 -08:00
Maxime Beauchemin
a82bb588f4 Allow users to specify label->color mapping (#3879)
Users can define `label_colors` in a dashboard's JSON metadata that
enforces a label to color mapping.

This also makes the function that maps labels to colors case insensitive.
2017-11-17 15:56:04 -08:00
michellethomas
a84bd5225c Only refreshing non instant filters on apply (#3893) 2017-11-17 12:52:48 -08:00
John Bodley
f0acc11249 [druid] Fix datasource column enumeration (#3896) 2017-11-16 22:41:54 -08:00
Grace Guo
fa35d7d2f4 fix input height to match with react-select (#3852) 2017-11-16 11:50:32 -08:00
Maxime Beauchemin
e65aba3c46 Fixing the build's linting errors (#3887)
master has new linting rules, PRs got merged with lint that was ok at
branching but not ok in masert anymore
2017-11-16 11:18:33 -08:00
Maxime Beauchemin
fab7b1083b A better looking favicon (#3851) 2017-11-16 10:14:54 -08:00
Maxime Beauchemin
d9161fb76a Fix slug function (#3876) 2017-11-16 09:47:00 -08:00
Maxime Beauchemin
85b18ff5e7 [table] show 'Time' column header instead of '__timestamp' (#3880) 2017-11-16 09:33:42 -08:00
Maxime Beauchemin
3a8af5d0b0 DECKGL integration - Phase 1 (#3771)
* DECKGL integration

Adding a new set of geospatial visualizations building on top of the
awesome deck.gl library. https://github.com/uber/deck.gl

While the end goal it to expose all types of layers and let users bind
their data to control most props exposed by the deck.gl API, this
PR focusses on a first set of visualizations and props:

* ScatterLayer
* HexagonLayer
* GridLayer
* ScreenGridLayer

* Addressing comments

* lint

* Linting

* Addressing chri's comments
2017-11-16 00:30:02 -08:00
Maxime Beauchemin
1c545d3a2d Further refactoring around dashboards (#3843)
I was wondering what was left to do in order to remove Dashboard.jsx
and superset.js, and it looks like they can just be pulled out.

I am so happy to get rid of what used to be the messiest JS files in the
whole repo.

Thanks @graceguo!
2017-11-16 00:27:15 -08:00
Grace Guo
120a5d08f9 [dashboard bug] Fix standalone slice (#3877) 2017-11-15 12:38:07 -08:00
Riccardo Magliocchetti
b586cb0ba7 Add mailing list and move screenshot at the end of README (#3872)
* README: add the mailing list

And mark the google group as deprecated

* README: move the screenshots at the end

So hopefully it'll be easier to find the resources
2017-11-15 08:40:18 -08:00
Dmitry Goryunov
ae2205aeb5 Filter out unavailable databases (#3875) 2017-11-15 08:39:43 -08:00
John Bodley
2e25fc4161 [issue] Resolving issue 2530 (#3865) 2017-11-14 21:13:00 -08:00
John Bodley
ba89b2d091 [cache] Fixing cache key w/ merged extra filters (#3809) 2017-11-14 21:12:26 -08:00
michellethomas
aee8438924 Fixing an issue with stripping filter values (#3869) 2017-11-14 19:22:03 -08:00
John Bodley
a6ba841e57 [flake8] Updaing CONTRIBUTING.md (#3862) 2017-11-14 18:17:53 -08:00
Grace Guo
8643228b51 [Dashboard bug] Fix merged filter param name (#3866)
front-end merge time filter params, and update query with param name 'extra_filters'
2017-11-14 12:28:55 -08:00
Grace Guo
de869973c7 Fix cachedDttm prop type (#3858) 2017-11-14 08:14:59 -08:00
John Bodley
ac57780607 [flake8] Resolving Q??? errors (#3847) 2017-11-13 21:06:51 -08:00
Mike Schiller
630604bc6b adding support for getting list of foreign tables for PostgreSQL (#3856)
* adding support for getting list of foreign tables for PostgreSQL

* need extra newline to pass lint
2017-11-13 21:05:22 -08:00
Grace Guo
eb5d220b5e [Dashboard bug] Slice doesn't show loading icon when loading (#3834) 2017-11-13 16:07:15 -08:00
Grace Guo
3f076b00cd [Dashboard bug]Fix userId prop in Explore view Save_Modal (#3857)
For userId, the attribute name in bootstrap data is user_id
2017-11-13 16:06:45 -08:00
Maxime Beauchemin
514f9452f3 [sql lab] minor cosmetic touchups on Run / Save buttons (#3850) 2017-11-13 12:39:28 -08:00
Maxime Beauchemin
068c343be0 [sqllab] fix wrong error msg (#3849)
I was getting some "Could not connect to server" when there was
a proper json payload with an `error` key, the change here makes sure to
prioritize those messages over the generic one.
2017-11-12 21:24:20 -08:00
Maxime Beauchemin
500455fc72 Add CHANGELOG.md entries for 0.20.0 to 0.20.5 (#3842) 2017-11-12 11:28:37 -08:00
John Bodley
1b4f128f55 [flake8] Resolving F5?? errors (#3846) 2017-11-12 11:09:22 -08:00
Grace Guo
1a3a8daf49 [Dashboard bug] should reset chartAlert when start new query (#3841) 2017-11-11 22:38:40 -08:00
王洁玉
7fce8eab3a Update setup.py (#3510) 2017-11-11 21:51:53 -08:00
Grace Guo
b4c9402737 [Dashboard bug] Fix Cache status and dttm information display for each slice (#3833) 2017-11-11 21:51:25 -08:00
Grace Guo
8459347bdc [Dashboard bug] should reset chartAlert when start new query (#3837) 2017-11-11 21:45:29 -08:00
Riccardo Magliocchetti
f7bf17290c run_tests.sh: call coveralls only on CI (#3836) 2017-11-11 21:44:55 -08:00
John Bodley
d908e48d61 [slice] Removing deprecated argument (#3838) 2017-11-11 21:44:24 -08:00
John Bodley
a3a4687ebf [viz] Fix payload force logic (#3839) 2017-11-11 21:43:55 -08:00
Jeff Niu
4d48d5d854 [Explore] Altered Slice Tag (#3668)
* Added altered tag to explore slice view and fixes #3616

* unit tests

* Moved getDiffs logic into AlteredSliceTag

* code style fixs
2017-11-10 21:33:31 -08:00
Maxime Beauchemin
83e6807fa0 [docs] add StatsD setup instructions (#3813) 2017-11-10 17:54:56 -08:00
John Bodley
ba96984048 [flake8] Resolving E3?? errors (#3814) 2017-11-10 17:52:34 -08:00
Maxime Beauchemin
591e5ec32e Bump celery to 4.1.0 (#3831)
* Bump celery to 4.1.0

* Also bumping boto3 to allow for celery 4 on SQS
2017-11-10 16:28:56 -08:00
John Bodley
690de862e8 [flake8] Resolve E1?? errors (#3805) 2017-11-10 12:06:22 -08:00
John Bodley
35810ce2bf [docstring] Refining warm_up_cache comment (#3815) 2017-11-10 08:05:20 -08:00
Grace Guo
6c52f2ff72 First time fetching chart should not force refresh. (#3822) 2017-11-09 21:48:05 -08:00
Alan Cruickshank
d663bea5e6 Basic German Translation (#3740)
Not complete but most of the core interface
2017-11-09 20:45:37 -08:00
John Bodley
1ea4521d0c [flake8] Resolving E7?? errors (#3816) 2017-11-09 20:23:59 -08:00
John Bodley
c4153c0bbe [flake8] Resolving E4?? errors (#3817) 2017-11-09 20:23:47 -08:00
Hugh A. Miles II
ae8b249dc2 Added /healthcheck endpoint for integrations with envoy (#3819)
* fixed mergeconflicts

* fixed mergeconflicts forreal this time

* added healthcheck test
2017-11-09 20:23:28 -08:00
Prasanna Swaminathan
9500f0aae3 Fix typo in installation.rst (#3818)
`superser runserver` should be `superset runserver`
2017-11-09 20:22:45 -08:00
Maxime Beauchemin
be3da6396f Fix misleading SQL Lab timeout error message (#3825) 2017-11-09 19:09:16 -08:00
Grace Guo
330926c167 fix error message format when long query timeout (#3823) 2017-11-09 19:07:49 -08:00
michellethomas
cbcc00c929 Make overflow important to allow scrolling on dashboard (#3810) 2017-11-08 20:35:45 -08:00
John Bodley
d03b74f754 [flake8] Resolving F4?? errors (#3811) 2017-11-08 20:34:33 -08:00
John Bodley
ec21d5af21 [flake8] Resolving E2?? errors (#3812) 2017-11-08 20:34:23 -08:00
michellethomas
70c7315ae0 Making time table viz scrollable (#3808) 2017-11-08 16:18:59 -08:00
Grace Guo
4fa1f0ab17 Dashboard refactory (#3581)
Create Chart component for all chart fetching and rendering, and apply redux architecture in dashboard view.
2017-11-08 10:46:21 -08:00
Maxime Beauchemin
39e502faae Stamping version to 0.21.0dev (#3801)
Making it clear that master has a `dev`-suffixed version. Proper release
number will be created in release branches.

There are constraints as to what npm and setuptools will accept as a
proper version number and N.N.Ndev seems to work so I'm rolling with it

Open to suggestion as to how to tag `master`
2017-11-08 09:47:41 -08:00
Ishpreet Singh
0280bc52e0 Allowing Leading and Trailing spaces in connection (#3433) 2017-11-07 22:13:18 -08:00
Jeff Niu
dee47864c4 Fixed single extraction dimension error (#3796) 2017-11-07 21:35:56 -08:00
John Bodley
17623f71d4 [flake8] Resolving C??? errors (#3787) 2017-11-07 21:32:45 -08:00
Magicansk
7453131858 Update messages.json (#3716)
* Update messages.json

* Update messages.json
2017-11-07 20:28:47 -08:00
John Bodley
e822fb50d8 [flake8] Resolving W??? errors (#3784) 2017-11-07 20:25:10 -08:00
John Bodley
e2bca47421 [flake8] Resolve I??? errors (#3797) 2017-11-07 20:23:40 -08:00
Maxime Beauchemin
7987cb794b Add Lyft and Twitter to list of companies (#3789) 2017-11-07 17:07:14 -08:00
Chris Williams
7483e2c942 [time table] use sparkData values in tooltip (#3794) 2017-11-07 15:52:51 -08:00
michellethomas
e6129eb492 Adding back iso and correctly filtering iso from contrib total (#3793) 2017-11-07 13:34:31 -08:00
michellethomas
b10aca2de1 Removing iso from data (#3788) 2017-11-06 23:29:02 -08:00
John Bodley
02cbad59de [flake8] Resolving F8?? errors (#3778) 2017-11-06 21:15:36 -08:00
Stephanie Rivera
ccb87d337c Rename files to allow RPM build (#3785)
I was having issues getting an RPM to build. My error was

Processing files: superset-0.20.1-1.noarch
error: File must begin with "/": Lockup
error: File must begin with "/": With
error: File must begin with "/": Text.svg
error: File must begin with "/": Lockup
error: File must begin with "/": With
error: File must begin with "/": Text@2x.png
error: File must begin with "/": Lockup
error: File must begin with "/": Without
error: File must begin with "/": Text@1x.svg
error: File must begin with "/": Lockup
error: File must begin with "/": Without
error: File must begin with "/": Text@2x.png
error: File must begin with "/": Mark.png
error: File must begin with "/": Mark@1x.svg

for ref https://github.com/pypa/setuptools/issues/767
File renaming fixes this issue
2017-11-06 17:13:53 -08:00
John Bodley
63a49983eb [falke8] Resolving F6?? errors (#3783) 2017-11-06 16:56:03 -08:00
Maxime Beauchemin
81dd622fdb [explore] using verbose_name in 'Time Column' control (#3529) 2017-11-06 15:21:34 -08:00
Jeff Niu
9a49b1c41d [Performance] VirtualizedSelect for SelectControl and FilterBox (#3654)
* Added virtualized select to SelectControl, allow onPaste to create new options

* Added unit tests

* Added virtualized/paste select to filterbox
2017-11-06 15:20:13 -08:00
Alejandro Fernandez
b059506afa DI-1113. ADDENDUM. Authentication: Enable user impersonation for Superset to HiveServer2 using hive.server2.proxy.user (a.fernandez) (#3697) 2017-11-06 10:20:38 -08:00
John Bodley
13c17e1526 [flake8] Enabling flake8 linting (#3776) 2017-11-04 22:50:42 -07:00
John Bodley
8e3217a921 [sql-lab] Fixing Run Query tooltip (#3774) 2017-11-04 00:10:47 -07:00
michellethomas
aed7c7436a Fix dashboard export download (#3773) 2017-11-04 00:10:02 -07:00
Chris Williams
7f3edad119 [time table] add tooltip to sparkline (#3767)
* [time table] add tooltip to sparkline

* [time table] open link in new tab

* [time table] add back Mustache
2017-11-03 12:37:15 -07:00
Stephanie Rivera
7fd9c82ae8 Update to reflect new version of cryptography (#3748)
update to 1.9

I have built and pip installed with latest cryptography.
2017-11-02 13:51:17 -07:00
Riccardo Magliocchetti
f3c7052f30 docs: reword the FAQ regarding table changes (#3763)
So we stop confusing people that thinks we do DDL.

Fix #3761
2017-11-02 13:50:11 -07:00
Dmitry Goryunov
326d90a5e4 add stackoverflow tag (#3764) 2017-11-02 13:49:47 -07:00
Maxime Beauchemin
cccc47311b Add dummy file to fix symlink (#3759)
In `assets/static` we have a symlink pointing to `docs/_build/html`
which is .gitignored and missing in the repo since we don't want the
html-generated docs in the repo, meaning the symlink is broken

Having the symlink broken creates issues with pip-compile somehow.

Adding a dummy file here addresses that
2017-11-02 10:17:28 -07:00
Grace Guo
5c03167948 fix https://github.com/apache/incubator-superset/pull/3726 (#3751) 2017-11-01 21:07:44 -07:00
Alan Cruickshank
87b6d76c32 Consolidate all translation config (#3750)
Move all translation config to superset/translations
2017-11-01 20:13:22 -07:00
Maxime Beauchemin
abfa03474c Bumping react-select to rc10 (#3726) 2017-10-31 22:03:18 -07:00
mxmzdlv
5bc734b2e5 Fix has_table method (#3741)
Dialect's has_table method requires connection as the first argument, not engine (https://github.com/zzzeek/sqlalchemy/blob/master/lib/sqlalchemy/engine/interfaces.py#L454). Instead, we can use engine's has_table method that handles the connection for us (https://github.com/zzzeek/sqlalchemy/blob/master/lib/sqlalchemy/engine/base.py#L2141). Alternatively, we could call engine.dialect.has_table(engine.connect(), ...).
2017-10-30 21:04:23 -07:00
Ryan Harmuth
814b70ffd8 Escape columns names for time grains - postgres (#3736) 2017-10-30 21:02:46 -07:00
Baldo Alessandro
1e18bfdea4 Fix 3657 (#3739)
* Reorders MANIFEST.in

* Includes the translations dir in MANIFEST
2017-10-30 11:26:42 -07:00
michellethomas
200b66d088 Using indexOf instead of includes for isXAxisString (#3733) 2017-10-27 16:00:44 -07:00
Grace Guo
cbd01074ba bump react-bootstrap version (#3723)
1. avoid exports is not defined error
2. per jordan's suggestion, update .babelrc only use airbnb presets
2017-10-26 23:10:00 -07:00
Maxime Beauchemin
1582fa1964 Add CRUD action to refresh table metadata (#3721)
A shortcut to make it easy to refresh a table's schema
2017-10-26 16:17:56 -07:00
Maxime Beauchemin
a9b6d11ade Validate JSON in slice's params on save (#3720)
fixes https://github.com/apache/incubator-superset/issues/3507

This prevents malformed JSON from getting saved in a slice's params. It
also prevents the issue described in #3507 from happening though as a
result malformed slices will render using default control values.
2017-10-26 16:16:21 -07:00
Maxime Beauchemin
c4b6324e74 Fix box_plot NaN issue (#3722)
fixes https://github.com/apache/incubator-superset/issues/3712
2017-10-26 12:00:22 -07:00
Magicansk
9432ea80be Update messages.po (#3715)
[Chinese Translation]
2017-10-25 23:26:38 +00:00
Rogan
f412b4c158 Missing the data of one province and two regions of China (#3686) 2017-10-25 22:27:18 +00:00
Rogan
547a3bf4e7 Fix the ISO code description of region/province/department (#3685)
* Fix the ISO code description of region/province/department

*  Missing the data of one province and two regions of China

* Revert " Missing the data of one province and two regions of China"

This reverts commit abff4555cd.
2017-10-25 00:58:54 +00:00
Maxime Beauchemin
e97dc9d3cb Set logging level to debug for DummyStatsLogger (#3662) 2017-10-25 00:50:15 +00:00
Jeff Niu
efae14592e fixes for bugs in #3689 (#3692) 2017-10-24 21:58:15 +00:00
Angus Ma
1d06495629 add VIPKID to the orgs. (#3703) 2017-10-24 21:30:54 +00:00
Jeff Niu
ffdfdb94ab changed metric heading from h1 to h3 (#3696) 2017-10-24 21:30:31 +00:00
Yu Xiao
8d7e97a26e [translation] added japanese support (#3713)
* [translation] added japanese support

* fix
2017-10-24 21:29:00 +00:00
Maxime Beauchemin
f8b8f6a343 [minor] fix label showing description in time_table's URL (#3663) 2017-10-24 21:27:53 +00:00
Grace Guo
4967342362 fix the slice permission issue after user click-edit new slice title (#3711) 2017-10-24 11:19:51 -07:00
John Bodley
9893847991 [form-data] Quoting form data (#3701) 2017-10-24 10:08:08 -07:00
Grace Guo
18e9640d99 fixing the datasource inconsistence but in visualize flow (#3698)
datasource in landing explore view is not the datasource created in sal lab.
2017-10-23 20:29:49 -07:00
Maxime Beauchemin
58ea736ed6 [cleanup] removing print() artefacts (#3683) 2017-10-23 15:48:47 -07:00
Jay Lindquist
b4bdc45a6b Add support for IE 11 for markup slices (#3702)
* Add srcdoc-polyfill tosupport Internet Explorer iframes in markup slices. Add allow-top-navigation and allow-popups to support links within iframes

* Remove jquery from markup.js
2017-10-23 15:35:15 -07:00
michellethomas
fa07b8d51b defaultSort should be false when no sort is necessary (#3693) 2017-10-23 11:17:28 -07:00
Jeff Niu
e121a8585e [Feature] Percentage columns in Table Viz (#3586)
* Added percent metric options to table viz

* Added unit tests for TableViz

* fixed code for python3

* bump travis
2017-10-16 20:16:20 -07:00
Alejandro Fernandez
adef519583 DI-1113. Authentication: Enable user impersonation for Superset to HiveServer2 using hive.server2.proxy.user (a.fernandez) (#3652) 2017-10-16 20:15:16 -07:00
Maxime Beauchemin
08f09b4761 [minor] fix padding in Time Table (#3664)
When in dashboard view, the Time Table needs some paddding
2017-10-16 17:01:35 -07:00
Jeff Niu
2a89c90e0b unit tests for OptionDescription component (#3678) 2017-10-16 16:31:43 -07:00
michellethomas
ce5fa379ec Avoid dividing by zero for sparkline in time table viz (#3679) 2017-10-16 16:31:07 -07:00
timifasubaa
d4d4a9b1f1 Sqllab error troubleshooting (#3680)
* provide troubleshooting link

* add option to append error troubleshooting link beneath sqllab error
2017-10-16 16:30:08 -07:00
Maxime Beauchemin
d0b5b449b2 Add a ColorPickerControl (#3653)
* Add a ColorPickerControl

* Tests
2017-10-15 23:38:38 -07:00
Jeff Niu
bad6938d1a [New Viz] Partition Diagram (#3642)
* Added Partition Visualization

* added unit tests
2017-10-12 21:54:59 -07:00
timifasubaa
48e28eff9b Add description for running specific test (#3665) 2017-10-12 20:53:37 -07:00
michellethomas
f87163413b Making the sort order for metrics pull from fd for time table viz (#3661) 2017-10-12 17:51:26 -07:00
Ali Cirik
52a9f2742b Make columns that return an exception on click unsortable. (#3417) 2017-10-11 22:36:12 -07:00
michellethomas
7f07fbefbc Adding sort time table (#3651)
* Updating time_table component to sort

* Removing old sort
2017-10-11 21:25:17 -07:00
Ganesh Krishnan
93660c6838 added aihello as superset user. (#3647) 2017-10-11 21:17:54 -07:00
Maxime Beauchemin
3ebadbcda9 Fix #3612 - reverse sign in difference calculation (#3646) 2017-10-11 21:14:39 -07:00
Denny Biasiolli
3df3e0d681 Fixing some warnings during tests (#3648)
* ColorSchemeControl: fixing bad use of PropTypes

Accessing PropTypes via the main React package is deprecated, and will be
removed in React v16.0. Use the latest available v15.* prop-types package from
npm instead. For info on usage, compatibility, migration and more, see
https://fb.me/prop-types-docs

* Control: adding PropTypes.func in types allowed inside `value` prop

This removes a warning during yarn tests

Fix #3589

* tests(QueryStateLabel): removing missing prop warning

```
Warning: Failed prop type: The prop `query` is marked as required in
`QueryStateLabel`, but its value is `undefined`.
    in QueryStateLabel
```

* SaveQuery: removing invalid prop `target` supplied to `Overlay`.

This removes a warning during yarn tests:

```
Warning: Failed prop type: Invalid prop `target` supplied to `Overlay`.
```

* RunQueryActionButton: removing `isRequired` from queryState props

This removes a warning during yarn tests:
```
Warning: Failed prop type: The prop `queryState` is marked as required in
`RunQueryActionButton`, but its value is `null`.
```
2017-10-11 21:07:52 -07:00
Jeff Niu
4a3c09187a [Translations] Restored lost French translations (#3645)
* Added some missing translations

squash

* Restored previous French translations
2017-10-10 17:52:39 -07:00
Maxime Beauchemin
76f8d33d81 [sql lab] fix impersonation + template issue (#3644)
When the database impersonation flag is on, a query using a template
fails. It has to do with templating using a database connection without
a username being specified by the caller, along with the fact that the
work is taking place on a worker, outside a web request, where
referencing g.user raises this exception.
2017-10-10 17:52:22 -07:00
Maxime Beauchemin
6cc6637454 Pin moment.js library since 2.19.0 creates problem (#3641)
The 2.19.0 released today (2017-10-10) creates issues with the
DateFilterControl somehow. Pining lib to latest known version.
2017-10-10 15:53:03 -07:00
Maxime Beauchemin
d7f8a7fde3 [time_table] adding support for URLs / links (#3600)
Using Mustache templating and providing {{ metric }} as well as
{{ ...formData }} as context.
2017-10-10 11:54:21 -07:00
michellethomas
80eb9c2c64 Set tooltip to show extent of sparkData (#3626)
* Set tooltip to show extent of sparkData

* Using d3format instead of round
2017-10-10 11:53:53 -07:00
Grace Guo
bd45e3b19a add explicit message display for 'Fetching Annotation Layer' error (#3631) 2017-10-10 11:49:53 -07:00
Maxime Beauchemin
b866b33dee [bugfix] Template rendering failed: '_AppCtxGlobals' object has no attribute 'user' (#3637)
Somehow the nature of `g` in Flask has changed where `g.user` used to
be provided outside the web request scope and its not anymore.

The fix here should address that.
2017-10-10 11:46:28 -07:00
Grace Guo
8994bdacbd fix long title text wrapping in editable-title component (#3638)
fix https://github.com/apache/incubator-superset/issues/3628
2017-10-10 11:45:48 -07:00
Maxime Beauchemin
f3b403d346 [minor] proper tooltip on ControlHeader's instant re-render trigger (#3625) 2017-10-09 23:28:14 -07:00
Yu Xiao
5ad4167512 add annotation option and a linear color map for heatmap viz. (#3634)
* add annotation option and a linear color map for heatmap viz.

* error fixes.

* fixes for requested changes
2017-10-09 23:17:51 -07:00
Maxime Beauchemin
ca67a7a4e9 [bugfix] empty From date filter NoneType error (#3633)
Error "AttributeError: 'NoneType' object has no attribute 'split'" is
fired.
2017-10-09 20:59:11 -07:00
Riccardo Magliocchetti
64ef8b14b4 remove unused imports (#3621)
* superset: remove unused imports

Spotted by pyflakes

* superset: removed unused get_session
2017-10-08 21:04:09 -07:00
Jeff Niu
912c6f6231 fixing date/time filter keys (#3611) 2017-10-07 13:07:23 -07:00
301 changed files with 235121 additions and 9457 deletions

2
.gitignore vendored
View File

@@ -31,7 +31,7 @@ app.db
*.entry.js
*.js.map
node_modules
npm-debug.log
npm-debug.log*
yarn.lock
superset/assets/version_info.json

View File

@@ -1,2 +0,0 @@
[pycodestyle]
max-line-length = 90

View File

@@ -102,7 +102,7 @@ evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / stateme
good-names=i,j,k,ex,Run,_,d,e,v,o,l,x,ts
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
bad-names=foo,bar,baz,toto,tutu,tata,d,fd
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.

View File

@@ -10,25 +10,22 @@ cache:
env:
global:
- TRAVIS_CACHE=$HOME/.travis_cache/
- TRAVIS_NODE_VERSION="7.10.0"
matrix:
- TOX_ENV=flake8
- TOX_ENV=javascript
- TOX_ENV=pylint
- TOX_ENV=py34-postgres
- TOX_ENV=py34-sqlite
- TOX_ENV=py27-mysql
- TOX_ENV=py27-sqlite
before_install:
- npm install -g npm@'>=5.4.1'
before_script:
- mysql -e 'drop database if exists superset; create database superset DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci' -u root
- mysql -u root -e "DROP DATABASE IF EXISTS superset; CREATE DATABASE superset DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci"
- mysql -u root -e "CREATE USER 'mysqluser'@'localhost' IDENTIFIED BY 'mysqluserpassword';"
- mysql -u root -e "GRANT ALL ON superset.* TO 'mysqluser'@'localhost';"
- psql -c 'create database superset;' -U postgres
- psql -c "CREATE USER postgresuser WITH PASSWORD 'pguserpassword';" -U postgres
- psql -U postgres -c "CREATE DATABASE superset;"
- psql -U postgres -c "CREATE USER postgresuser WITH PASSWORD 'pguserpassword';"
- export PATH=${PATH}:/tmp/hive/bin
install:
- pip install --upgrade pip
- pip install tox tox-travis
- rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install $TRAVIS_NODE_VERSION
script: tox -e $TOX_ENV

View File

@@ -1,5 +1,250 @@
## Change Log
### 0.22.1
Fixes 0.22.0
### 0.22.0
Bad empty release
### 0.21.2 (2017/12/11 21:18 +00:00)
- [#3974](https://github.com/apache/incubator-superset/pull/3974) [Bugfix] `_add_filters_from_pre_query` doesn't handle dim specs (#3974) (@Mogball)
- [#4041](https://github.com/apache/incubator-superset/pull/4041) [API] Deprecate /update_role/ API endpoint (#4041) (@john-bodley)
- [#4030](https://github.com/apache/incubator-superset/pull/4030) resolve python2 str() issue (#4030) (@timifasubaa)
### 0.21.1
Bad-empty release
### 0.21.0 (2017/12/08 09:11 +00:00)
- [#4031](https://github.com/apache/incubator-superset/pull/4031) apply custom css for dashboard initially load (#4031) (@graceguo-supercat)
- [#3891](https://github.com/apache/incubator-superset/pull/3891) [BUGFIX]: Fixing dttm_sql_literal to use python_date_format when specified. (#3891) (@fabianmenges)
- [#3947](https://github.com/apache/incubator-superset/pull/3947) Add fastdom js dependency (#3947) (@mistercrunch)
- [#4015](https://github.com/apache/incubator-superset/pull/4015) Remove unused callbacks when setting state (#4015) (@betodealmeida)
- [#4017](https://github.com/apache/incubator-superset/pull/4017) Fixed finding postaggregations (#4017) (@Mogball)
- [#3941](https://github.com/apache/incubator-superset/pull/3941) New time_pivot visualization (#3941) (@mistercrunch)
- [#3959](https://github.com/apache/incubator-superset/pull/3959) Add type MONEY as numeric type (#3959) (@mistercrunch)
- [#3969](https://github.com/apache/incubator-superset/pull/3969) Add row_limit to heatmap controls (#3969) (@mistercrunch)
- [#4019](https://github.com/apache/incubator-superset/pull/4019) Add support of another DatabaseError format (#4019) (@dmigo)
- [#3975](https://github.com/apache/incubator-superset/pull/3975) asciifying http header for csv download; fixes #3952 (#3975) (@rumbin)
- [#4001](https://github.com/apache/incubator-superset/pull/4001) Add has_access to import_dashboard (#4001) (@timifasubaa)
- [#3999](https://github.com/apache/incubator-superset/pull/3999) [sql lab] fix position of 'save query' Popover (#3999) (@mistercrunch)
- [#4003](https://github.com/apache/incubator-superset/pull/4003) Call props.onChange only when closing filter (#4003) (@betodealmeida)
- [#3978](https://github.com/apache/incubator-superset/pull/3978) Adding YAML Import-Export for Datasources to CLI (#3978) (@fabianmenges)
- [#4009](https://github.com/apache/incubator-superset/pull/4009) Rollback bulk-delete of table columns (#4009) (@alanmcruickshank)
- [#4000](https://github.com/apache/incubator-superset/pull/4000) Add Datasource Name filter under slice list view (#4000) (@graceguo-supercat)
- [#3997](https://github.com/apache/incubator-superset/pull/3997) Alternate PR to #3970 (#3997) (@mistercrunch)
- [#3976](https://github.com/apache/incubator-superset/pull/3976) [doc] added setting X-Forwarded-Proto to https behind reverse proxy with ssl encryption; fixes #3655 (#3976) (@rumbin)
- [#3991](https://github.com/apache/incubator-superset/pull/3991) Create CODE_OF_CONDUCT.md (#3991) (@mistercrunch)
- [#3967](https://github.com/apache/incubator-superset/pull/3967) [Dashboard] fix a filter refresh bug and add Test (#3967) (@graceguo-supercat)
- [#3965](https://github.com/apache/incubator-superset/pull/3965) [docs] making it clear sqlite shouldn't be used in a cluster (#3965) (@mistercrunch)
- [#3957](https://github.com/apache/incubator-superset/pull/3957) [time series table] visual improvements (#3957) (@williaster)
- [#3958](https://github.com/apache/incubator-superset/pull/3958) Improving speed of dashboard import (#3958) (@michellethomas)
- [#3949](https://github.com/apache/incubator-superset/pull/3949) [Bugfix] Druid `run_query` dimensions part 3 + Unit tests (#3949) (@Mogball)
- [#3946](https://github.com/apache/incubator-superset/pull/3946) [country_map] use Albers USA projection (#3946) (@mistercrunch)
- [#3948](https://github.com/apache/incubator-superset/pull/3948) fix 'superset db history' (#3948) (@mistercrunch)
- [#3951](https://github.com/apache/incubator-superset/pull/3951) Allow underscores in slugs (#3951) (@michellethomas)
- [#3955](https://github.com/apache/incubator-superset/pull/3955) config: bring back sqlite default database (#3955) (@xrmx)
- [#3940](https://github.com/apache/incubator-superset/pull/3940) Add an "Edit Mode" to Dashboard view (#3940) (@mistercrunch)
- [#3920](https://github.com/apache/incubator-superset/pull/3920) Fixed branching condition with dimension spec (#3920) (@Mogball)
- [#3643](https://github.com/apache/incubator-superset/pull/3643) Import CSV (#3643) (@timifasubaa)
- [#3945](https://github.com/apache/incubator-superset/pull/3945) Fix call in Chart (#3945) (@mistercrunch)
- [#3933](https://github.com/apache/incubator-superset/pull/3933) minor filter select enhancements (#3933) (@kkalyan)
- [#3929](https://github.com/apache/incubator-superset/pull/3929) Make Table Columns & Metrics Bulk-deletable (#3929) (@alanmcruickshank)
- [#3922](https://github.com/apache/incubator-superset/pull/3922) [travis] Standardizing before_install (#3922) (@john-bodley)
- [#3923](https://github.com/apache/incubator-superset/pull/3923) Switched to span instead of textarea for copytoclipboard (#3923) (@Mogball)
- [#3924](https://github.com/apache/incubator-superset/pull/3924) Moved percent metrics to its own row (#3924) (@Mogball)
- [#3875](https://github.com/apache/incubator-superset/pull/3875) Revert "Filter out unavailable databases (#3875)" (#3918) (@mistercrunch)
- [#3913](https://github.com/apache/incubator-superset/pull/3913) [bugfix] remove quotes from Postgres time grains (#3913) (@mistercrunch)
- [#3915](https://github.com/apache/incubator-superset/pull/3915) Fix left padding in dashboard widgets (#3915) (@mistercrunch)
- [#3916](https://github.com/apache/incubator-superset/pull/3916) [cosmetic] remove border from table viz (#3916) (@mistercrunch)
- [#3912](https://github.com/apache/incubator-superset/pull/3912) When checking if you should renderTriggered make sure key exists in controls (#3912) (@michellethomas)
- [#3906](https://github.com/apache/incubator-superset/pull/3906) fix the schema-fetching problem for impala in sql_lab (#3906) (@xiaoyugit)
- [#3911](https://github.com/apache/incubator-superset/pull/3911) Add UK Metropolitan Districts and Isle of Man (#3911) (@alanmcruickshank)
- [#3904](https://github.com/apache/incubator-superset/pull/3904) Bumping webpack related deps (#3904) (@mistercrunch)
- [#3902](https://github.com/apache/incubator-superset/pull/3902) [bugfix] allow limiting word cloud (#3902) (@mistercrunch)
### 0.21.0rc2 (2017/11/20 17:18 +00:00)
- [#3903](https://github.com/apache/incubator-superset/pull/3903) Fixes default hanlding in Altered slice tag (#3903) (@mistercrunch)
- [#3910](https://github.com/apache/incubator-superset/pull/3910) Workaround pandas bug in datetimes with time zones (#3910) (@bolkedebruin)
- [#3583](https://github.com/apache/incubator-superset/pull/3583) [3541] Augmenting datasources uniqueness constraints (#3583) (@john-bodley)
- [#3895](https://github.com/apache/incubator-superset/pull/3895) [druid] Fixing issue 3894 multi-processing w/ Gunicorn (#3895) (@john-bodley)
- [#3897](https://github.com/apache/incubator-superset/pull/3897) [druid] Catch IOError when fetching Druid datasource time boundary (#3897) (@john-bodley)
- [#3899](https://github.com/apache/incubator-superset/pull/3899) [druid] Renaming refresh_async method (#3899) (@john-bodley)
- [#3884](https://github.com/apache/incubator-superset/pull/3884) Add datasource to the SliceAddView modal (#3884) (#3900) (@alanmcruickshank)
- [#3890](https://github.com/apache/incubator-superset/pull/3890) [dashboard bug]Instant control should take effect instantly (#3890) (@graceguo-supercat)
- [#3879](https://github.com/apache/incubator-superset/pull/3879) Allow users to specify label->color mapping (#3879) (@mistercrunch)
- [#3893](https://github.com/apache/incubator-superset/pull/3893) Only refreshing non instant filters on apply (#3893) (@michellethomas)
### 0.21.0rc1 (2017/11/17 17:33 +00:00)
- [#3896](https://github.com/apache/incubator-superset/pull/3896) [druid] Fix datasource column enumeration (#3896) (@john-bodley)
- [#3852](https://github.com/apache/incubator-superset/pull/3852) fix input height to match with react-select (#3852) (@graceguo-supercat)
- [#3887](https://github.com/apache/incubator-superset/pull/3887) Fixing the build's linting errors (#3887) (@mistercrunch)
- [#3851](https://github.com/apache/incubator-superset/pull/3851) A better looking favicon (#3851) (@mistercrunch)
- [#3876](https://github.com/apache/incubator-superset/pull/3876) Fix slug function (#3876) (@mistercrunch)
- [#3880](https://github.com/apache/incubator-superset/pull/3880) [table] show 'Time' column header instead of '__timestamp' (#3880) (@mistercrunch)
- [#3771](https://github.com/apache/incubator-superset/pull/3771) DECKGL integration - Phase 1 (#3771) (@mistercrunch)
- [#3843](https://github.com/apache/incubator-superset/pull/3843) Further refactoring around dashboards (#3843) (@mistercrunch)
- [#3877](https://github.com/apache/incubator-superset/pull/3877) [dashboard bug] Fix standalone slice (#3877) (@graceguo-supercat)
- [#3872](https://github.com/apache/incubator-superset/pull/3872) Add mailing list and move screenshot at the end of README (#3872) (@xrmx)
- [#3875](https://github.com/apache/incubator-superset/pull/3875) Filter out unavailable databases (#3875) (@dmigo)
### 0.20.6 (2017/11/15 05:26 +00:00)
- [#3865](https://github.com/apache/incubator-superset/pull/3865) [issue] Resolving issue 2530 (#3865) (@john-bodley)
- [#3809](https://github.com/apache/incubator-superset/pull/3809) [cache] Fixing cache key w/ merged extra filters (#3809) (@john-bodley)
- [#3869](https://github.com/apache/incubator-superset/pull/3869) Fixing an issue with stripping filter values (#3869) (@michellethomas)
- [#3862](https://github.com/apache/incubator-superset/pull/3862) [flake8] Updaing CONTRIBUTING.md (#3862) (@john-bodley)
- [#3866](https://github.com/apache/incubator-superset/pull/3866) [Dashboard bug] Fix merged filter param name (#3866) (@graceguo-supercat)
- [#3858](https://github.com/apache/incubator-superset/pull/3858) Fix cachedDttm prop type (#3858) (@graceguo-supercat)
- [#3847](https://github.com/apache/incubator-superset/pull/3847) [flake8] Resolving Q??? errors (#3847) (@john-bodley)
- [#3856](https://github.com/apache/incubator-superset/pull/3856) adding support for getting list of foreign tables for PostgreSQL (#3856) (@mike-schiller)
- [#3834](https://github.com/apache/incubator-superset/pull/3834) [Dashboard bug] Slice doesn't show loading icon when loading (#3834) (@graceguo-supercat)
- [#3857](https://github.com/apache/incubator-superset/pull/3857) [Dashboard bug]Fix userId prop in Explore view Save_Modal (#3857) (@graceguo-supercat)
- [#3850](https://github.com/apache/incubator-superset/pull/3850) [sql lab] minor cosmetic touchups on Run / Save buttons (#3850) (@mistercrunch)
- [#3849](https://github.com/apache/incubator-superset/pull/3849) [sqllab] fix wrong error msg (#3849) (@mistercrunch)
- [#3842](https://github.com/apache/incubator-superset/pull/3842) Add CHANGELOG.md entries for 0.20.0 to 0.20.5 (#3842) (@mistercrunch)
- [#3846](https://github.com/apache/incubator-superset/pull/3846) [flake8] Resolving F5?? errors (#3846) (@john-bodley)
- [#3841](https://github.com/apache/incubator-superset/pull/3841) [Dashboard bug] should reset chartAlert when start new query (#3841) (@graceguo-supercat)
- [#3510](https://github.com/apache/incubator-superset/pull/3510) Update setup.py (#3510) (@joriewong)
- [#3833](https://github.com/apache/incubator-superset/pull/3833) [Dashboard bug] Fix Cache status and dttm information display for each slice (#3833) (@graceguo-supercat)
- [#3837](https://github.com/apache/incubator-superset/pull/3837) [Dashboard bug] should reset chartAlert when start new query (#3837) (@graceguo-supercat)
- [#3836](https://github.com/apache/incubator-superset/pull/3836) run_tests.sh: call coveralls only on CI (#3836) (@xrmx)
- [#3838](https://github.com/apache/incubator-superset/pull/3838) [slice] Removing deprecated argument (#3838) (@john-bodley)
- [#3839](https://github.com/apache/incubator-superset/pull/3839) [viz] Fix payload force logic (#3839) (@john-bodley)
- [#3668](https://github.com/apache/incubator-superset/pull/3668) [Explore] Altered Slice Tag (#3668) (@Mogball)
- [#3813](https://github.com/apache/incubator-superset/pull/3813) [docs] add StatsD setup instructions (#3813) (@mistercrunch)
- [#3814](https://github.com/apache/incubator-superset/pull/3814) [flake8] Resolving E3?? errors (#3814) (@john-bodley)
- [#3831](https://github.com/apache/incubator-superset/pull/3831) Bump celery to 4.1.0 (#3831) (@mistercrunch)
- [#3805](https://github.com/apache/incubator-superset/pull/3805) [flake8] Resolve E1?? errors (#3805) (@john-bodley)
- [#3815](https://github.com/apache/incubator-superset/pull/3815) [docstring] Refining warm_up_cache comment (#3815) (@john-bodley)
- [#3822](https://github.com/apache/incubator-superset/pull/3822) First time fetching chart should not force refresh. (#3822) (@graceguo-supercat)
- [#3740](https://github.com/apache/incubator-superset/pull/3740) Basic German Translation (#3740) (@alanmcruickshank)
- [#3816](https://github.com/apache/incubator-superset/pull/3816) [flake8] Resolving E7?? errors (#3816) (@john-bodley)
- [#3817](https://github.com/apache/incubator-superset/pull/3817) [flake8] Resolving E4?? errors (#3817) (@john-bodley)
- [#3819](https://github.com/apache/incubator-superset/pull/3819) Added /healthcheck endpoint for integrations with envoy (#3819) (@hughhhh)
- [#3818](https://github.com/apache/incubator-superset/pull/3818) Fix typo in installation.rst (#3818) (@pswaminathan)
- [#3825](https://github.com/apache/incubator-superset/pull/3825) Fix misleading SQL Lab timeout error message (#3825) (@mistercrunch)
- [#3823](https://github.com/apache/incubator-superset/pull/3823) fix error message format when long query timeout (#3823) (@graceguo-supercat)
- [#3810](https://github.com/apache/incubator-superset/pull/3810) Make overflow important to allow scrolling on dashboard (#3810) (@michellethomas)
- [#3811](https://github.com/apache/incubator-superset/pull/3811) [flake8] Resolving F4?? errors (#3811) (@john-bodley)
- [#3812](https://github.com/apache/incubator-superset/pull/3812) [flake8] Resolving E2?? errors (#3812) (@john-bodley)
- [#3808](https://github.com/apache/incubator-superset/pull/3808) Making time table viz scrollable (#3808) (@michellethomas)
- [#3581](https://github.com/apache/incubator-superset/pull/3581) Dashboard refactory (#3581) (@graceguo-supercat)
- [#3801](https://github.com/apache/incubator-superset/pull/3801) Stamping version to 0.21.0dev (#3801) (@mistercrunch)
- [#3433](https://github.com/apache/incubator-superset/pull/3433) Allowing Leading and Trailing spaces in connection (#3433) (@ishpreet-singh)
- [#3796](https://github.com/apache/incubator-superset/pull/3796) Fixed single extraction dimension error (#3796) (@Mogball)
- [#3787](https://github.com/apache/incubator-superset/pull/3787) [flake8] Resolving C??? errors (#3787) (@john-bodley)
- [#3716](https://github.com/apache/incubator-superset/pull/3716) Update messages.json (#3716) (@magicansk)
- [#3784](https://github.com/apache/incubator-superset/pull/3784) [flake8] Resolving W??? errors (#3784) (@john-bodley)
- [#3797](https://github.com/apache/incubator-superset/pull/3797) [flake8] Resolve I??? errors (#3797) (@john-bodley)
- [#3789](https://github.com/apache/incubator-superset/pull/3789) Add Lyft and Twitter to list of companies (#3789) (@mistercrunch)
- [#3794](https://github.com/apache/incubator-superset/pull/3794) [time table] use sparkData values in tooltip (#3794) (@williaster)
- [#3793](https://github.com/apache/incubator-superset/pull/3793) Adding back iso and correctly filtering iso from contrib total (#3793) (@michellethomas)
- [#3788](https://github.com/apache/incubator-superset/pull/3788) Removing iso from data (#3788) (@michellethomas)
- [#3778](https://github.com/apache/incubator-superset/pull/3778) [flake8] Resolving F8?? errors (#3778) (@john-bodley)
- [#3785](https://github.com/apache/incubator-superset/pull/3785) Rename files to allow RPM build (#3785) (@SpyderRivera)
- [#3783](https://github.com/apache/incubator-superset/pull/3783) [falke8] Resolving F6?? errors (#3783) (@john-bodley)
- [#3529](https://github.com/apache/incubator-superset/pull/3529) [explore] using verbose_name in 'Time Column' control (#3529) (@mistercrunch)
- [#3654](https://github.com/apache/incubator-superset/pull/3654) [Performance] VirtualizedSelect for SelectControl and FilterBox (#3654) (@Mogball)
- [#3697](https://github.com/apache/incubator-superset/pull/3697) DI-1113. ADDENDUM. Authentication: Enable user impersonation for Superset to HiveServer2 using hive.server2.proxy.user (a.fernandez) (#3697) (@afernandez)
### 0.20.5 (2017/11/06 07:18 +00:00)
- [#3776](https://github.com/apache/incubator-superset/pull/3776) [flake8] Enabling flake8 linting (#3776) (@john-bodley)
- [#3774](https://github.com/apache/incubator-superset/pull/3774) [sql-lab] Fixing Run Query tooltip (#3774) (@john-bodley)
- [#3773](https://github.com/apache/incubator-superset/pull/3773) Fix dashboard export download (#3773) (@michellethomas)
- [#3767](https://github.com/apache/incubator-superset/pull/3767) [time table] add tooltip to sparkline (#3767) (@williaster)
- [#3748](https://github.com/apache/incubator-superset/pull/3748) Update to reflect new version of cryptography (#3748) (@SpyderRivera)
- [#3763](https://github.com/apache/incubator-superset/pull/3763) docs: reword the FAQ regarding table changes (#3763) (@xrmx)
- [#3764](https://github.com/apache/incubator-superset/pull/3764) add stackoverflow tag (#3764) (@dmigo)
- [#3759](https://github.com/apache/incubator-superset/pull/3759) Add dummy file to fix symlink (#3759) (@mistercrunch)
- [#3751](https://github.com/apache/incubator-superset/pull/3751) fix https://github.com/apache/incubator-superset/pull/3726 (#3751) (@graceguo-supercat)
- [#3750](https://github.com/apache/incubator-superset/pull/3750) Consolidate all translation config (#3750) (@alanmcruickshank)
- [#3726](https://github.com/apache/incubator-superset/pull/3726) Bumping react-select to rc10 (#3726) (@mistercrunch)
- [#3741](https://github.com/apache/incubator-superset/pull/3741) Fix has_table method (#3741) (@mxmzdlv)
- [#3736](https://github.com/apache/incubator-superset/pull/3736) Escape columns names for time grains - postgres (#3736) (@Ryanthegiantlion)
- [#3739](https://github.com/apache/incubator-superset/pull/3739) Fix 3657 (#3739) (@baldoalessandro)
- [#3733](https://github.com/apache/incubator-superset/pull/3733) Using indexOf instead of includes for isXAxisString (#3733) (@michellethomas)
- [#3723](https://github.com/apache/incubator-superset/pull/3723) bump react-bootstrap version (#3723) (@graceguo-supercat)
- [#3721](https://github.com/apache/incubator-superset/pull/3721) Add CRUD action to refresh table metadata (#3721) (@mistercrunch)
- [#3720](https://github.com/apache/incubator-superset/pull/3720) Validate JSON in slice's params on save (#3720) (@mistercrunch)
- [#3722](https://github.com/apache/incubator-superset/pull/3722) Fix box_plot NaN issue (#3722) (@mistercrunch)
- [#3715](https://github.com/apache/incubator-superset/pull/3715) Update messages.po (#3715) (@magicansk)
- [#3686](https://github.com/apache/incubator-superset/pull/3686) Missing the data of one province and two regions of China (#3686) (@roganw)
- [#3685](https://github.com/apache/incubator-superset/pull/3685) Fix the ISO code description of region/province/department (#3685) (@roganw)
- [#3662](https://github.com/apache/incubator-superset/pull/3662) Set logging level to debug for DummyStatsLogger (#3662) (@mistercrunch)
- [#3692](https://github.com/apache/incubator-superset/pull/3692) fixes for bugs in #3689 (#3692) (@Mogball)
- [#3703](https://github.com/apache/incubator-superset/pull/3703) add VIPKID to the orgs. (#3703) (@killpanda)
- [#3696](https://github.com/apache/incubator-superset/pull/3696) changed metric heading from h1 to h3 (#3696) (@Mogball)
- [#3713](https://github.com/apache/incubator-superset/pull/3713) [translation] added japanese support (#3713) (@xiaoyugit)
- [#3663](https://github.com/apache/incubator-superset/pull/3663) [minor] fix label showing description in time_table's URL (#3663) (@mistercrunch)
- [#3711](https://github.com/apache/incubator-superset/pull/3711) fix the slice permission issue after user click-edit new slice title (#3711) (@graceguo-supercat)
- [#3701](https://github.com/apache/incubator-superset/pull/3701) [form-data] Quoting form data (#3701) (@john-bodley)
- [#3698](https://github.com/apache/incubator-superset/pull/3698) fixing the datasource inconsistence but in visualize flow (#3698) (@graceguo-supercat)
- [#3683](https://github.com/apache/incubator-superset/pull/3683) [cleanup] removing print() artefacts (#3683) (@mistercrunch)
- [#3702](https://github.com/apache/incubator-superset/pull/3702) Add support for IE 11 for markup slices (#3702) (@jaylindquist)
- [#3693](https://github.com/apache/incubator-superset/pull/3693) defaultSort should be false when no sort is necessary (#3693) (@michellethomas)
- [#3586](https://github.com/apache/incubator-superset/pull/3586) [Feature] Percentage columns in Table Viz (#3586) (@Mogball)
- [#3652](https://github.com/apache/incubator-superset/pull/3652) DI-1113. Authentication: Enable user impersonation for Superset to HiveServer2 using hive.server2.proxy.user (a.fernandez) (#3652) (@afernandez)
- [#3664](https://github.com/apache/incubator-superset/pull/3664) [minor] fix padding in Time Table (#3664) (@mistercrunch)
- [#3678](https://github.com/apache/incubator-superset/pull/3678) unit tests for OptionDescription component (#3678) (@Mogball)
- [#3679](https://github.com/apache/incubator-superset/pull/3679) Avoid dividing by zero for sparkline in time table viz (#3679) (@michellethomas)
- [#3680](https://github.com/apache/incubator-superset/pull/3680) Sqllab error troubleshooting (#3680) (@timifasubaa)
- [#3653](https://github.com/apache/incubator-superset/pull/3653) Add a ColorPickerControl (#3653) (@mistercrunch)
- [#3642](https://github.com/apache/incubator-superset/pull/3642) [New Viz] Partition Diagram (#3642) (@Mogball)
- [#3665](https://github.com/apache/incubator-superset/pull/3665) Add description for running specific test (#3665) (@timifasubaa)
- [#3661](https://github.com/apache/incubator-superset/pull/3661) Making the sort order for metrics pull from fd for time table viz (#3661) (@michellethomas)
- [#3417](https://github.com/apache/incubator-superset/pull/3417) Make columns that return an exception on click unsortable. (#3417) (@aliavni)
- [#3651](https://github.com/apache/incubator-superset/pull/3651) Adding sort time table (#3651) (@michellethomas)
- [#3647](https://github.com/apache/incubator-superset/pull/3647) added aihello as superset user. (#3647) (@ganeshkrishnan1)
- [#3646](https://github.com/apache/incubator-superset/pull/3646) Fix #3612 - reverse sign in difference calculation (#3646) (@mistercrunch)
- [#3648](https://github.com/apache/incubator-superset/pull/3648) Fixing some warnings during tests (#3648) (@dennybiasiolli)
### 0.20.4 (2017/10/12 04:04 +00:00)
- [#3645](https://github.com/apache/incubator-superset/pull/3645) [Translations] Restored lost French translations (#3645) (@Mogball)
- [#3644](https://github.com/apache/incubator-superset/pull/3644) [sql lab] fix impersonation + template issue (#3644) (@mistercrunch)
- [#3641](https://github.com/apache/incubator-superset/pull/3641) Pin moment.js library since 2.19.0 creates problem (#3641) (@mistercrunch)
- [#3600](https://github.com/apache/incubator-superset/pull/3600) [time_table] adding support for URLs / links (#3600) (@mistercrunch)
- [#3626](https://github.com/apache/incubator-superset/pull/3626) Set tooltip to show extent of sparkData (#3626) (@michellethomas)
- [#3631](https://github.com/apache/incubator-superset/pull/3631) add explicit message display for 'Fetching Annotation Layer' error (#3631) (@graceguo-supercat)
- [#3637](https://github.com/apache/incubator-superset/pull/3637) [bugfix] Template rendering failed: '_AppCtxGlobals' object has no attribute 'user' (#3637) (@mistercrunch)
- [#3638](https://github.com/apache/incubator-superset/pull/3638) fix long title text wrapping in editable-title component (#3638) (@graceguo-supercat)
- [#3625](https://github.com/apache/incubator-superset/pull/3625) [minor] proper tooltip on ControlHeader's instant re-render trigger (#3625) (@mistercrunch)
- [#3634](https://github.com/apache/incubator-superset/pull/3634) add annotation option and a linear color map for heatmap viz. (#3634) (@xiaoyugit)
- [#3633](https://github.com/apache/incubator-superset/pull/3633) [bugfix] empty From date filter NoneType error (#3633) (@mistercrunch)
- [#3621](https://github.com/apache/incubator-superset/pull/3621) remove unused imports (#3621) (@xrmx)
- [#3611](https://github.com/apache/incubator-superset/pull/3611) fixing date/time filter keys (#3611) (@Mogball)
### 0.20.2 (2017/10/06 07:46 +00:00)
- [#3606](https://github.com/apache/incubator-superset/pull/3606) [bugfix] #3593 'Chart Options' panel is missing (#3606) (@mistercrunch)
- [#3601](https://github.com/apache/incubator-superset/pull/3601) Removing git artifact (#3601) (@mistercrunch)
- [#3599](https://github.com/apache/incubator-superset/pull/3599) [hotfix] fixing issues around new time_table viz (#3599) (@mistercrunch)
- [#3598](https://github.com/apache/incubator-superset/pull/3598) [hofix] work around circular deps (#3598) (@mistercrunch)
- [#3597](https://github.com/apache/incubator-superset/pull/3597) [time table] fix reversed ratio (#3597) (@mistercrunch)
- [#3508](https://github.com/apache/incubator-superset/pull/3508) [Feature/Bugfix] Datepicker and time granularity options to dashboard filters (#3508) (@Mogball)
- [#3596](https://github.com/apache/incubator-superset/pull/3596) updating react-alert dependency to v2.3.0 (#3596) (@dennybiasiolli)
- [#3577](https://github.com/apache/incubator-superset/pull/3577) [translations] generating missing strings (#3577) (@mistercrunch)
- [#3478](https://github.com/apache/incubator-superset/pull/3478) [Bugfix/Feature] Fixed slice render staggering on dashboard first load (#3478) (@Mogball)
- [#3543](https://github.com/apache/incubator-superset/pull/3543) New "Time Series - Table" visualization (#3543) (@mistercrunch)
- [#3587](https://github.com/apache/incubator-superset/pull/3587) [sql lab] fix numeric sort in data table (#3587) (@mistercrunch)
- [#3594](https://github.com/apache/incubator-superset/pull/3594) Fxing bug in label generation for multiple groupbys (#3594) (@fabianmenges)
- [#3591](https://github.com/apache/incubator-superset/pull/3591) update immutable.js to v3.8.2 (MIT license) (#3591) (@naoyak)
- [#3571](https://github.com/apache/incubator-superset/pull/3571) [Feature] Copy-to-clipboard button in View Query (#3571) (@Mogball)
- [#3585](https://github.com/apache/incubator-superset/pull/3585) Allow users to see query string when query returns no data (#3585) (@Mogball)
- [#3582](https://github.com/apache/incubator-superset/pull/3582) [Bugfix]: Explore view does not respect custom timeout. (#3582) (@fabianmenges)
- [#3584](https://github.com/apache/incubator-superset/pull/3584) Fixed creating new filter options in FilterBox (#3584) (@Mogball)
- [#3562](https://github.com/apache/incubator-superset/pull/3562) Added custom pasteSelect to handle paste events (#3562) (@Mogball)
- [#3569](https://github.com/apache/incubator-superset/pull/3569) Bumping React to 15.6.2 (MIT license) (#3569) (@mistercrunch)
### 0.20.1 (2017/10/03 07:04 +00:00)
- [#3576](https://github.com/apache/incubator-superset/pull/3576) v0.20.1 (#3576) (@mistercrunch)
- [#3572](https://github.com/apache/incubator-superset/pull/3572) After saving slice fixing redirect (#3572) (@michellethomas)
- [#3565](https://github.com/apache/incubator-superset/pull/3565) Added label+percent and label+value display options to pie chart (#3565) (@Mogball)
- [#3567](https://github.com/apache/incubator-superset/pull/3567) Removing yarn warnings during install (#3567) (@dennybiasiolli)
- [#3563](https://github.com/apache/incubator-superset/pull/3563) [nvd3] fix single metric showing up in legend (#3563) (@mistercrunch)
- [#3558](https://github.com/apache/incubator-superset/pull/3558) Add Pronto Tools to user list (#3558) (@zkan)
- [#3553](https://github.com/apache/incubator-superset/pull/3553) Minor documentation fix (#3553) (@gaborhermann)
- [#3545](https://github.com/apache/incubator-superset/pull/3545) CHANGELOG for 0.20.0 (#3545) (@mistercrunch)
- [#3534](https://github.com/apache/incubator-superset/pull/3534) Explore update button labels (#3534) (@timifasubaa)
- [#3547](https://github.com/apache/incubator-superset/pull/3547) Fixing missing messages.json file (#3547) (@mistercrunch)
### 0.20.0 (2017/09/28 04:26 +00:00)
- [#3528](https://github.com/apache/incubator-superset/pull/3528) try to fix problem that chrome window not opening after ajax requrest (#3528) (@graceguo-supercat)
- [#3521](https://github.com/apache/incubator-superset/pull/3521) Time Series Annotation Layers (#3521) (@graceguo-supercat)

84
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,84 @@
# Code of Conduct
## 1. Purpose
A primary goal of Apache Superset is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof).
This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior.
We invite all those who participate in Apache Superset to help us create safe and positive experiences for everyone.
## 2. Open Source Citizenship
A supplemental goal of this Code of Conduct is to increase open source citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community.
Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society.
If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know.
## 3. Expected Behavior
The following behaviors are expected and requested of all community members:
* Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community.
* Exercise consideration and respect in your speech and actions.
* Attempt collaboration before conflict.
* Refrain from demeaning, discriminatory, or harassing behavior and speech.
* Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential.
* Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations.
## 4. Unacceptable Behavior
The following behaviors are considered harassment and are unacceptable within our community:
* Violence, threats of violence or violent language directed against another person.
* Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language.
* Posting or displaying sexually explicit or violent material.
* Posting or threatening to post other peoples personally identifying information ("doxing").
* Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability.
* Inappropriate photography or recording.
* Inappropriate physical contact. You should have someones consent before touching them.
* Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances.
* Deliberate intimidation, stalking or following (online or in person).
* Advocating for, or encouraging, any of the above behavior.
* Sustained disruption of community events, including talks and presentations.
## 5. Consequences of Unacceptable Behavior
Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated.
Anyone asked to stop unacceptable behavior is expected to comply immediately.
If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event).
## 6. Reporting Guidelines
If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. dev@superset.incubator.apache.org .
Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress.
## 7. Addressing Grievances
If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify Apache with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies.
## 8. Scope
We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venuesonline and in-personas well as in all one-on-one communications pertaining to community business.
This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members.
## 9. Contact info
dev@superset.incubator.apache.org
## 10. License and attribution
This Code of Conduct is distributed under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/).
Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy).
Retrieved on November 22, 2016 from [http://citizencodeofconduct.org/](http://citizencodeofconduct.org/)

View File

@@ -49,6 +49,10 @@ If you are proposing a feature:
implement.
- Remember that this is a volunteer-driven project, and that
contributions are welcome :)
### Questions
There is a dedicated [tag](https://stackoverflow.com/questions/tagged/apache-superset) on [stackoverflow](https://stackoverflow.com/). Please use it when asking questions.
## Pull Request Guidelines
@@ -262,9 +266,15 @@ Before running python unit tests, please setup local testing environment:
pip install -r dev-reqs.txt
```
Python tests can be run with:
All python tests can be run with:
./run_tests.sh
Alternatively, you can run a specific test with:
./run_specific_test.sh tests.core_tests:CoreTests.test_function_name
Note that before running specific tests, you have to both setup the local testing environment and run all tests.
We use [Mocha](https://mochajs.org/), [Chai](http://chaijs.com/) and [Enzyme](http://airbnb.io/enzyme/) to test Javascript. Tests can be run with:
@@ -276,9 +286,8 @@ We use [Mocha](https://mochajs.org/), [Chai](http://chaijs.com/) and [Enzyme](ht
Lint the project with:
# for python changes
flake8 changes tests
flake8 changes superset
# for python
flake8
# for javascript
npm run lint
@@ -345,14 +354,15 @@ navigation bar.
}
As per the [Flask AppBuilder documentation] about translation, to create a
new language dictionary, run the following command:
new language dictionary, run the following command (where `es` is replaced with
the language code for your target language):
pybabel init -i ./babel/messages.pot -d superset/translations -l es
pybabel init -i superset/translations/messages.pot -d superset/translations -l es
Then it's a matter of running the statement below to gather all strings that
need translation
fabmanager babel-extract --target superset/translations/ -k _ -k __ -k t -k tn -k tct
fabmanager babel-extract --target superset/translations/ --output superset/translations/messages.pot --config superset/translations/babel.cfg -k _ -k __ -k t -k tn -k tct
You can then translate the strings gathered in files located under
`superset/translation`, where there's one per language. For the translations
@@ -369,6 +379,11 @@ Execute this command to convert the en PO file into a json file:
po2json -d superset -f jed1.x superset/translations/en/LC_MESSAGES/messages.po superset/translations/en/LC_MESSAGES/messages.json
If you get errors running `po2json`, you might be running the ubuntu package with the same
name rather than the nodejs package (they have a different format for the arguments). You
need to be running the nodejs version, and so if there is a conflict you may need to point
directly at `/usr/local/bin/po2json` rather than just `po2json`.
## Adding new datasources
1. Create Models and Views for the datasource, add them under superset folder, like a new my_models.py

View File

@@ -1,8 +1,9 @@
recursive-include superset/templates *
recursive-include superset/data *
recursive-include superset/migrations *
recursive-include superset/static *
recursive-exclude superset/static/docs *
recursive-exclude superset/static/spec *
recursive-exclude superset/static/assets/node_modules *
recursive-include superset/templates *
recursive-include superset/translations *
recursive-exclude tests *
recursive-include superset/data *
recursive-include superset/migrations *

102
README.md
View File

@@ -6,7 +6,7 @@ Superset
[![Coverage Status](https://coveralls.io/repos/apache/incubator-superset/badge.svg?branch=master&service=github)](https://coveralls.io/github/apache/incubator-superset?branch=master)
[![PyPI](https://img.shields.io/pypi/pyversions/superset.svg?maxAge=2592000)](https://pypi.python.org/pypi/superset)
[![Requirements Status](https://requires.io/github/apache/incubator-superset/requirements.svg?branch=master)](https://requires.io/github/apache/incubator-superset/requirements/?branch=master)
[![Join the chat at https://gitter.im/apache/incubator-superset](https://badges.gitter.im/apache/incubator-superset.svg)](https://gitter.im/apache/incubator-superset?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Join the chat at https://gitter.im/airbnb/superset](https://badges.gitter.im/apache/incubator-superset.svg)](https://gitter.im/airbnb/superset?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Documentation](https://img.shields.io/badge/docs-apache.org-blue.svg)](https://superset.incubator.apache.org)
[![dependencies Status](https://david-dm.org/apache/incubator-superset/status.svg?path=superset/assets)](https://david-dm.org/apache/incubator-superset?path=superset/assets)
@@ -127,6 +127,59 @@ 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)
* [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!
- [AiHello](https://www.aihello.com)
- [Airbnb](https://github.com/airbnb)
- [Amino](https://amino.com)
- [Brilliant.org](https://brilliant.org/)
- [Capital Service S.A.](http://capitalservice.pl)
- [Clark.de](http://clark.de/)
- [Digit Game Studios](https://www.digitgaming.com/)
- [Douban](https://www.douban.com/)
- [Endress+Hauser](http://www.endress.com/)
- [FBK - ICT center](http://ict.fbk.eu)
- [Faasos](http://faasos.com/)
- [GfK Data Lab](http://datalab.gfk.com)
- [Konfío](http://konfio.mx)
- [Lyft](https://www.lyft.com/)
- [Maieutical Labs](https://cloudschooling.it)
- [Ona](https://ona.io)
- [Pronto Tools](http://www.prontotools.io)
- [Qunar](https://www.qunar.com/)
- [Shopee](https://shopee.sg)
- [Shopkick](https://www.shopkick.com)
- [Tails.com](https://tails.com)
- [Tobii](http://www.tobii.com/)
- [Tooploox](https://www.tooploox.com/)
- [Twitter](https://twitter.com/)
- [Udemy](https://www.udemy.com/)
- [VIPKID](https://www.vipkid.com.cn/)
- [Yahoo!](https://yahoo.com/)
- [Zalando](https://www.zalando.com)
More screenshots
----------------
@@ -145,50 +198,3 @@ More screenshots
![superset-sql-lab-2](https://cloud.githubusercontent.com/assets/130878/20234713/0f67b856-a835-11e6-9d50-7a52168f66fd.png)
![superset-sql-lab](https://cloud.githubusercontent.com/assets/130878/20234714/0f68f45a-a835-11e6-9467-f47ad0af7e79.png)
Resources
-------------
* [Superset Google Group](https://groups.google.com/forum/#!forum/airbnb_superset)
* [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)
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!
- [Airbnb](https://github.com/airbnb)
- [Amino](https://amino.com)
- [Brilliant.org](https://brilliant.org/)
- [Capital Service S.A.](http://capitalservice.pl)
- [Clark.de](http://clark.de/)
- [Digit Game Studios](https://www.digitgaming.com/)
- [Douban](https://www.douban.com/)
- [Endress+Hauser](http://www.endress.com/)
- [FBK - ICT center](http://ict.fbk.eu)
- [Faasos](http://faasos.com/)
- [GfK Data Lab](http://datalab.gfk.com)
- [Konfío](http://konfio.mx)
- [Maieutical Labs](https://cloudschooling.it)
- [Pronto Tools](http://www.prontotools.io)
- [Qunar](https://www.qunar.com/)
- [Shopee](https://shopee.sg)
- [Shopkick](https://www.shopkick.com)
- [Tails.com](https://tails.com)
- [Tobii](http://www.tobii.com/)
- [Tooploox](https://www.tooploox.com/)
- [Udemy](https://www.udemy.com/)
- [Yahoo!](https://yahoo.com/)
- [Zalando](https://www.zalando.com)

View File

@@ -2,6 +2,7 @@ codeclimate-test-reporter
coveralls
flake8
flask_cors
ipdb
mock
mysqlclient
nose

1
docs/_build/html/README.md vendored Normal file
View File

@@ -0,0 +1 @@
Folder containing the sphinx-generated documentation

View File

@@ -45,6 +45,13 @@ visualizations.
https://github.com/airbnb/superset/issues?q=label%3Aexample+is%3Aclosed
Can I upload and visualize csv data?
------------------------------------
Yes, using the ``Upload a CSV`` button under the ``Sources``
menu item. This brings up a form that allows you specify required information. After creating the table from CSV, it can then be loaded like any other on the ``Sources -> Tables``page.
Why are my queries timing out?
------------------------------
@@ -99,7 +106,7 @@ edit the ``JSON Metadata`` field, more specifically the
never be affected by any dashboard level filtering.
..code::
..code:: json
{
"filter_immune_slices": [324, 65, 92],
@@ -134,7 +141,7 @@ to be refreshed - especially if some data is slow moving, or run heavy queries.
slices from the timed refresh process, add the ``timed_refresh_immune_slices`` key to the dashboard
``JSON Metadata`` field:
..code::
..code:: json
{
"filter_immune_slices": [],
@@ -150,7 +157,7 @@ Slice refresh will also be staggered over the specified period. You can turn off
by setting the ``stagger_refresh`` to ``false`` and modify the stagger period by setting
``stagger_time`` to a value in milliseconds in the ``JSON Metadata`` field:
..code::
..code:: json
{
"stagger_refresh": false,
@@ -170,8 +177,8 @@ You can override this path using the ``SUPERSET_HOME`` environment variable.
Another work around is to change where superset stores the sqlite database by adding ``SQLALCHEMY_DATABASE_URI = 'sqlite:////new/location/superset.db'`` in superset_config.py (create the file if needed), then adding the directory where superset_config.py lives to PYTHONPATH environment variable (e.g. ``export PYTHONPATH=/opt/logs/sandbox/airbnb/``).
How do I add new columns to an existing table
---------------------------------------------
What if the table schema changed?
---------------------------------
Table schemas evolve, and Superset needs to reflect that. It's pretty common
in the life cycle of a dashboard to want to add a new dimension or metric.
@@ -213,3 +220,27 @@ How can I set a default filter on my dashboard?
Easy. Simply apply the filter and save the dashboard while the filter
is active.
How do I get Superset to refresh the schema of my table?
--------------------------------------------------------
When adding columns to a table, you can have Superset detect and merge the
new columns in by using the "Refresh Metadata" action in the
``Source -> Tables`` page. Simply check the box next to the tables
you want the schema refreshed, and click ``Actions -> Refresh Metadata``.
Is there a way to force the use specific colors?
------------------------------------------------
It is possible on a per-dashboard basis by providing a mapping of
labels to colors in the ``JSON Metadata`` attribute using the
``label_colors`` key.
..code:: json
{
"label_colors": {
"Girls": "#FF69B4",
"Boys": "#ADD8E6"
}
}

View File

@@ -0,0 +1,103 @@
Importing and Exporting Datasources
===================================
The superset cli allows you to import and export datasources from and to YAML.
Datasources include both databases and druid clusters. The data is expected to be organized in the following hierarchy: ::
.
├──databases
| ├──database_1
| | ├──table_1
| | | ├──columns
| | | | ├──column_1
| | | | ├──column_2
| | | | └──... (more columns)
| | | └──metrics
| | | ├──metric_1
| | | ├──metric_2
| | | └──... (more metrics)
| | └── ... (more tables)
| └── ... (more databases)
└──druid_clusters
├──cluster_1
| ├──datasource_1
| | ├──columns
| | | ├──column_1
| | | ├──column_2
| | | └──... (more columns)
| | └──metrics
| | ├──metric_1
| | ├──metric_2
| | └──... (more metrics)
| └── ... (more datasources)
└── ... (more clusters)
Exporting Datasources to YAML
-----------------------------
You can print your current datasources to stdout by running: ::
superset export_datasources
To save your datasources to a file run: ::
superset export_datasources -f <filename>
By default, default (null) values will be omitted. Use the ``-d`` flag to include them.
If you want back references to be included (e.g. a column to include the table id
it belongs to) use the ``-b`` flag.
Alternatively you can export datasources using the UI: ::
1. Open **Sources** -> **Databases** to export all tables associated to a single or multiple databases. (**Tables** for one or more tables, **Druid Clusters** for clusters, **Druid Datasources** for datasources)
2. Select the items you would like to export
3. Click **Actions** -> **Export to YAML**
4. If you want to import an item that you exported through the UI, you will need to nest it inside its parent element, e.g. a `database` needs to be nested under `databases` a `table` needs to be nested inside a `database` element.
Exporting the complete supported YAML schema
--------------------------------------------
In order to obtain an exhaustive list of all fields you can import using the YAML import run: ::
superset export_datasource_schema
Again, you can use the ``-b`` flag to include back references.
Importing Datasources from YAML
-------------------------------
In order to import datasources from a YAML file(s), run: ::
superset import_datasources -p <path or filename>
If you supply a path all files ending with ``*.yaml`` or ``*.yml`` will be parsed.
You can apply additional flags e.g.: ::
superset import_datasources -p <path> -r
Will search the supplied path recursively.
The sync flag ``-s`` takes parameters in order to sync the supplied elements with
your file. Be careful this can delete the contents of your meta database. Example:
superset import_datasources -p <path / filename> -s columns,metrics
This will sync all ``metrics`` and ``columns`` for all datasources found in the
``<path / filename>`` in the Superset meta database. This means columns and metrics
not specified in YAML will be deleted. If you would add ``tables`` to ``columns,metrics``
those would be synchronised as well.
If you don't supply the sync flag (``-s``) importing will only add and update (override) fields.
E.g. you can add a ``verbose_name`` to the the column ``ds`` in the table ``random_time_series`` from the example datasets
by saving the following YAML to file and then running the ``import_datasources`` command. ::
databases:
- database_name: main
tables:
- table_name: random_time_series
columns:
- column_name: ds
verbose_name: datetime

View File

@@ -25,10 +25,10 @@ intelligence web application
endorsed by the ASF.
Overview
=======================================
========
Features
---------
--------
- A rich set of data visualizations
- An easy-to-use interface for exploring and visualizing data
@@ -61,7 +61,7 @@ Features
Contents
---------
--------
.. toctree::
:maxdepth: 2

View File

@@ -53,6 +53,12 @@ the required dependencies are installed: ::
sudo apt-get install build-essential libssl-dev libffi-dev python-dev python-pip libsasl2-dev libldap2-dev
**Ubuntu 16.04** If you have python3.5 installed alongside with python2.7, as is default on **Ubuntu 16.04 LTS**, run this command also
sudo apt-get install build-essential libssl-dev libffi-dev python3.5-dev python-pip libsasl2-dev libldap2-dev
otherwhise build for ``cryptography`` fails.
For **Fedora** and **RHEL-derivatives**, the following command will ensure
that the required dependencies are installed: ::
@@ -62,7 +68,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.7.2
env LDFLAGS="-L$(brew --prefix openssl)/lib" CFLAGS="-I$(brew --prefix openssl)/include" pip install cryptography==1.9
**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: ::
@@ -148,7 +154,7 @@ around `gunicorn`, it doesn't expose all the options you may need,
so you'll want to craft your own `gunicorn` command in your production
environment. Here's an **async** setup known to work well: ::
gunicorn \
 gunicorn \
-w 10 \
-k gevent \
--timeout 120 \
@@ -163,10 +169,31 @@ Refer to the
for more information.
Note that *gunicorn* does not
work on Windows so the `superser runserver` command is not expected to work
work on Windows so the `superset runserver` command is not expected to work
in that context. Also note that the development web
server (`superset runserver -d`) is not intended for production use.
Flask-AppBuilder Permissions
----------------------------
By default every time the Flask-AppBuilder (FAB) app is initialized the
permissions and views are added automatically to the backend and associated with
the Admin role. The issue however is when you are running multiple concurrent
workers this creates a lot of contention and race conditions when defining
permissions and views.
To alleviate this issue, the automatic updating of permissions can be disabled
by setting the :envvar:`SUPERSET_UPDATE_PERMS` environment variable to `0`.
The value `1` enables it, `0` disables it. Note if undefined the functionality
is enabled to maintain backwards compatibility.
In a production environment initialization could take on the following form:
export SUPERSET_UPDATE_PERMS=1
superset init
export SUPERSET_UPDATE_PERMS=0
gunicorn -w 10 ... superset:app
Configuration behind a load balancer
------------------------------------
@@ -181,6 +208,11 @@ If the load balancer is inserting X-Forwarded-For/X-Forwarded-Proto headers, you
should set `ENABLE_PROXY_FIX = True` in the superset config file to extract and use
the headers.
In case that the reverse proxy is used for providing ssl encryption,
an explicit definition of the `X-Forwarded-Proto` may be required.
For the Apache webserver this can be set as follows: ::
 RequestHeader set X-Forwarded-Proto "https"
Configuration
-------------
@@ -277,6 +309,8 @@ Here's a list of some of the recommended packages.
| ClickHouse | ``pip install | ``clickhouse://`` |
| | sqlalchemy-clickhouse`` | |
+---------------+-------------------------------------+-------------------------------------------------+
| Kylin | ``pip install kylinpy`` | ``kylin://`` |
+---------------+-------------------------------------+-------------------------------------------------+
Note that many other database are supported, the main criteria being the
existence of a functional SqlAlchemy dialect and Python driver. Googling
@@ -394,7 +428,7 @@ metadata from your Druid cluster(s)
CORS
-----
----
The extra CORS Dependency must be installed:
@@ -498,6 +532,11 @@ look something like:
RESULTS_BACKEND = RedisCache(
host='localhost', port=6379, key_prefix='superset_results')
Note that it's important that all the worker nodes and web servers in
the Superset cluster share a common metadata database.
This means that SQLite will not work in this context since it has
limited support for concurrency and
typically lives on the local file system.
Also note that SQL Lab supports Jinja templating in queries, and that it's
possible to overload
@@ -550,3 +589,20 @@ same server.
return "Ok"
BLUEPRINTS = [simple_page]
StatsD logging
--------------
Superset is instrumented to log events to StatsD if desired. Most endpoints hit
are logged as well as key events like query start and end in SQL Lab.
To setup StatsD logging, it's a matter of configuring the logger in your
``superset_config.py``.
..code ::
from superset.stats_logger import StatsdStatsLogger
STATS_LOGGER = StatsdStatsLogger(host='localhost', port=8125, prefix='superset')
Note that it's also possible to implement you own logger by deriving
``superset.stats_logger.BaseStatsLogger``.

View File

@@ -10,7 +10,7 @@ Provided Roles
--------------
Superset ships with a set of roles that are handled by Superset itself.
You can assume that these roles will stay up-to-date as Superset evolves.
Even though it's possible for ``Admin`` usrs to do so, it is not recommended
Even though it's possible for ``Admin`` users to do so, it is not recommended
that you alter these roles in any way by removing
or adding permissions to them as these roles will be re-synchronized to
their original values as you run your next ``superset init`` command.

View File

@@ -48,17 +48,25 @@ Available macros
We expose certain modules from Python's standard library in
Superset's Jinja context:
- ``time``: ``time``
- ``datetime``: ``datetime.datetime``
- ``uuid``: ``uuid``
- ``random``: ``random``
- ``relativedelta``: ``dateutil.relativedelta.relativedelta``
- more to come!
`Jinja's builtin filters <http://jinja.pocoo.org/docs/dev/templates/>`_ can be also be applied where needed.
.. autoclass:: superset.jinja_context.PrestoTemplateProcessor
:members:
.. autofunction:: superset.jinja_context.url_param
Extending macros
''''''''''''''''
As mentioned in the `Installation & Configuration <https://superset.incubator.apache.org/installation.html#installation-configuration>`_ documentation,
it's possible for administrators to expose more more macros in their
environment using the configuration variable ``JINJA_CONTEXT_ADDONS``.
All objects referenced in this dictionary will become available for users
to integrate in their queries in **SQL Lab**.

View File

@@ -23,7 +23,7 @@ Under the **Sources** menu, select the *Databases* option:
.. image:: _static/img/tutorial/tutorial_01_sources_database.png
:scale: 70%
On the resulting page, click on the green plus sign, near the top left:
On the resulting page, click on the green plus sign, near the top right:
.. image:: _static/img/tutorial/tutorial_02_add_database.png
:scale: 70%

File diff suppressed because it is too large Load Diff

View File

@@ -9,4 +9,6 @@ set -e
superset/bin/superset db upgrade
superset/bin/superset version -v
python setup.py nosetests
coveralls
if [ "$CI" = "true" ] ; then
coveralls
fi

View File

@@ -1,6 +1,7 @@
from superset import sm
from collections import defaultdict
from superset import sm
def cleanup_permissions():
# 1. Clean up duplicates.

View File

@@ -23,6 +23,3 @@ detailed-errors=1
with-coverage=1
nocapture=1
cover-package=superset
[pycodestyle]
max-line-length=90

View File

@@ -1,7 +1,8 @@
import json
import os
import subprocess
import json
from setuptools import setup, find_packages
from setuptools import find_packages, setup
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
PACKAGE_DIR = os.path.join(BASE_DIR, 'superset', 'static', 'assets')
@@ -14,18 +15,19 @@ def get_git_sha():
try:
s = str(subprocess.check_output(['git', 'rev-parse', 'HEAD']))
return s.strip()
except:
return ""
except Exception:
return ''
GIT_SHA = get_git_sha()
version_info = {
'GIT_SHA': GIT_SHA,
'version': version_string,
}
print("-==-" * 15)
print("VERSION: " + version_string)
print("GIT SHA: " + GIT_SHA)
print("-==-" * 15)
print('-==-' * 15)
print('VERSION: ' + version_string)
print('GIT SHA: ' + GIT_SHA)
print('-==-' * 15)
with open(os.path.join(PACKAGE_DIR, 'version_info.json'), 'w') as version_file:
json.dump(version_info, version_file)
@@ -34,45 +36,50 @@ with open(os.path.join(PACKAGE_DIR, 'version_info.json'), 'w') as version_file:
setup(
name='superset',
description=(
"A interactive data visualization platform build on SqlAlchemy "
"and druid.io"),
'A interactive data visualization platform build on SqlAlchemy '
'and druid.io'),
version=version_string,
packages=find_packages(),
include_package_data=True,
zip_safe=False,
scripts=['superset/bin/superset'],
install_requires=[
'boto3==1.4.4',
'celery==3.1.25',
'boto3>=1.4.6',
'celery==4.1.0',
'colorama==0.3.9',
'cryptography==1.9',
'flask==0.12.2',
'flask-appbuilder==1.9.4',
'flask-appbuilder==1.9.6',
'flask-cache==0.13.1',
'flask-migrate==2.0.3',
'flask-script==2.0.5',
'flask-migrate==2.1.1',
'flask-script==2.0.6',
'flask-sqlalchemy==2.1',
'flask-testing==0.6.2',
'flask-testing==0.7.1',
'flask-wtf==0.14.2',
'flower==0.9.1',
'flower==0.9.2',
'future>=0.16.0, <0.17',
'python-geohash==0.8.5',
'humanize==0.5.1',
'gunicorn==19.7.1',
'idna==2.5',
'markdown==2.6.8',
'pandas==0.20.3',
'idna==2.6',
'markdown==2.6.11',
'pandas==0.22.0',
'parsedatetime==2.0.0',
'pydruid==0.3.1',
'pathlib2==2.3.0',
'polyline==1.3.2',
'pydruid==0.4.0',
'PyHive>=0.4.0',
'python-dateutil==2.6.0',
'requests==2.17.3',
'simplejson==3.10.0',
'six==1.10.0',
'sqlalchemy==1.1.9',
'sqlalchemy-utils==0.32.16',
'sqlparse==0.2.3',
'python-dateutil==2.6.1',
'pyyaml>=3.11',
'requests==2.18.4',
'simplejson==3.13.2',
'six==1.11.0',
'sqlalchemy==1.2.2',
'sqlalchemy-utils==0.32.21',
'sqlparse==0.2.4',
'thrift>=0.9.3',
'thrift-sasl>=0.2.1',
'unidecode>=0.04.21',
],
extras_require={
'cors': ['Flask-Cors>=2.0.0'],

View File

@@ -4,14 +4,13 @@ from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import json
import logging
from logging.handlers import TimedRotatingFileHandler
import json
import os
from flask import Flask, redirect
from flask_appbuilder import SQLA, AppBuilder, IndexView
from flask_appbuilder import AppBuilder, IndexView, SQLA
from flask_appbuilder.baseviews import expose
from flask_migrate import Migrate
from flask_wtf.csrf import CSRFProtect
@@ -23,6 +22,9 @@ from superset import utils, config # noqa
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') as f:
frontend_config = json.load(f)
@@ -43,7 +45,7 @@ def parse_manifest_json():
with open(MANIFEST_FILE, 'r') as f:
manifest = json.load(f)
except Exception:
print("no manifest file found at " + MANIFEST_FILE)
pass
def get_manifest_file(filename):
@@ -67,7 +69,7 @@ for bp in conf.get('BLUEPRINTS'):
print("Registering blueprint: '{}'".format(bp.name))
app.register_blueprint(bp)
except Exception as e:
print("blueprint registration failed")
print('blueprint registration failed')
logging.exception(e)
if conf.get('SILENCE_FAB'):
@@ -92,7 +94,7 @@ utils.pessimistic_connection_handling(db.engine)
cache = utils.setup_cache(app, conf.get('CACHE_CONFIG'))
tables_cache = utils.setup_cache(app, conf.get('TABLE_NAMES_CACHE_CONFIG'))
migrate = Migrate(app, db, directory=APP_DIR + "/migrations")
migrate = Migrate(app, db, directory=APP_DIR + '/migrations')
# Logging configuration
logging.basicConfig(format=app.config.get('LOG_FORMAT'))
@@ -150,16 +152,23 @@ appbuilder = AppBuilder(
db.session,
base_template='superset/base.html',
indexview=MyIndexView,
security_manager_class=app.config.get("CUSTOM_SECURITY_MANAGER"))
security_manager_class=app.config.get('CUSTOM_SECURITY_MANAGER'),
update_perms=utils.get_update_perms_flag(),
)
sm = appbuilder.sm
get_session = appbuilder.get_session
results_backend = app.config.get("RESULTS_BACKEND")
results_backend = app.config.get('RESULTS_BACKEND')
# Registering sources
module_datasource_map = app.config.get("DEFAULT_MODULE_DS_MAP")
module_datasource_map.update(app.config.get("ADDITIONAL_MODULE_DS_MAP"))
module_datasource_map = app.config.get('DEFAULT_MODULE_DS_MAP')
module_datasource_map.update(app.config.get('ADDITIONAL_MODULE_DS_MAP'))
ConnectorRegistry.register_sources(module_datasource_map)
# Hook that provides administrators a handle on the Flask APP
# after initialization
flask_app_mutator = app.config.get('FLASK_APP_MUTATOR')
if flask_app_mutator:
flask_app_mutator(app)
from superset import views # noqa

View File

@@ -1,3 +1,3 @@
{
"presets" : ["airbnb", "env", "react"],
"presets" : ["airbnb"],
}

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -20,6 +20,7 @@ export const QUERY_EDITOR_SET_SCHEMA = 'QUERY_EDITOR_SET_SCHEMA';
export const QUERY_EDITOR_SET_TITLE = 'QUERY_EDITOR_SET_TITLE';
export const QUERY_EDITOR_SET_AUTORUN = 'QUERY_EDITOR_SET_AUTORUN';
export const QUERY_EDITOR_SET_SQL = 'QUERY_EDITOR_SET_SQL';
export const QUERY_EDITOR_SET_TEMPLATE_PARAMS = 'QUERY_EDITOR_SET_TEMPLATE_PARAMS';
export const QUERY_EDITOR_SET_SELECTED_TEXT = 'QUERY_EDITOR_SET_SELECTED_TEXT';
export const QUERY_EDITOR_PERSIST_HEIGHT = 'QUERY_EDITOR_PERSIST_HEIGHT';
@@ -132,6 +133,7 @@ export function runQuery(query) {
tab: query.tab,
tmp_table_name: query.tempTableName,
select_as_cta: query.ctas,
templateParams: query.templateParams,
};
const sqlJsonUrl = '/superset/sql_json/' + location.search;
$.ajax({
@@ -153,10 +155,12 @@ export function runQuery(query) {
msg = err.responseText;
}
}
if (textStatus === 'error' && errorThrown === '') {
msg = t('Could not connect to server');
} else if (msg === null) {
msg = `[${textStatus}] ${errorThrown}`;
if (msg === null) {
if (errorThrown) {
msg = `[${textStatus}] ${errorThrown}`;
} else {
msg = t('Unknown error');
}
}
if (msg.indexOf('CSRF token') > 0) {
msg = t('Your session timed out, please refresh your page and try again.');
@@ -246,6 +250,10 @@ export function queryEditorSetSql(queryEditor, sql) {
return { type: QUERY_EDITOR_SET_SQL, queryEditor, sql };
}
export function queryEditorSetTemplateParams(queryEditor, templateParams) {
return { type: QUERY_EDITOR_SET_TEMPLATE_PARAMS, queryEditor, templateParams };
}
export function queryEditorSetSelectedText(queryEditor, sql) {
return { type: QUERY_EDITOR_SET_SELECTED_TEXT, queryEditor, sql };
}

View File

@@ -144,7 +144,8 @@ export default class ResultSet extends React.PureComponent {
}
render() {
const query = this.props.query;
const height = this.props.search ? this.props.height - SEARCH_HEIGHT : this.props.height;
const height = Math.max(0,
(this.props.search ? this.props.height - SEARCH_HEIGHT : this.props.height));
let sql;
if (this.props.showSql) {

View File

@@ -6,7 +6,7 @@ import { t } from '../../locales';
const propTypes = {
allowAsync: PropTypes.bool.isRequired,
dbId: PropTypes.number,
queryState: PropTypes.string.isRequired,
queryState: PropTypes.string,
runQuery: PropTypes.func.isRequired,
selectedText: PropTypes.string,
stopQuery: PropTypes.func.isRequired,
@@ -19,7 +19,6 @@ export default function RunQueryActionButton(props) {
const runBtnText = props.selectedText ? t('Run Selected Query') : t('Run Query');
const btnStyle = props.selectedText ? 'warning' : 'primary';
const shouldShowStopBtn = ['running', 'pending'].indexOf(props.queryState) > -1;
const asyncToolTip = t('Run query asynchronously');
const commonBtnProps = {
bsSize: 'small',
@@ -32,7 +31,7 @@ export default function RunQueryActionButton(props) {
{...commonBtnProps}
onClick={() => props.runQuery(false)}
key="run-btn"
tooltip={asyncToolTip}
tooltip={t('Run query synchronously')}
>
<i className="fa fa-refresh" /> {runBtnText}
</Button>
@@ -43,7 +42,7 @@ export default function RunQueryActionButton(props) {
{...commonBtnProps}
onClick={() => props.runQuery(true)}
key="run-async-btn"
tooltip={asyncToolTip}
tooltip={t('Run query asynchronously')}
>
<i className="fa fa-table" /> {runBtnText}
</Button>
@@ -66,12 +65,7 @@ export default function RunQueryActionButton(props) {
} else {
button = syncBtn;
}
return (
<div className="inline m-r-5 pull-left">
{button}
</div>
);
return button;
}
RunQueryActionButton.propTypes = propTypes;

View File

@@ -1,7 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormControl, FormGroup, Overlay, Popover, Row, Col } from 'react-bootstrap';
import { FormControl, FormGroup, Row, Col } from 'react-bootstrap';
import Button from '../../components/Button';
import ModalTrigger from '../../components/ModalTrigger';
import { t } from '../../locales';
const propTypes = {
@@ -41,10 +43,10 @@ class SaveQuery extends React.PureComponent {
sql: this.props.sql,
};
this.props.onSave(query);
this.setState({ showSave: false });
this.saveModal.close();
}
onCancel() {
this.setState({ showSave: false });
this.saveModal.close();
}
onLabelChange(e) {
this.setState({ label: e.target.value });
@@ -55,73 +57,70 @@ class SaveQuery extends React.PureComponent {
toggleSave(e) {
this.setState({ target: e.target, showSave: !this.state.showSave });
}
renderPopover() {
renderModalBody() {
return (
<Popover id="embed-code-popover">
<FormGroup bsSize="small" style={{ width: '350px' }}>
<Row>
<Col md={12}>
<small>
<label className="control-label" htmlFor="embed-height">
{t('Label')}
</label>
</small>
<FormControl
type="text"
placeholder={t('Label for your query')}
value={this.state.label}
onChange={this.onLabelChange}
/>
</Col>
</Row>
<br />
<Row>
<Col md={12}>
<small>
<label className="control-label" htmlFor="embed-height">{t('Description')}</label>
</small>
<FormControl
componentClass="textarea"
placeholder={t('Write a description for your query')}
value={this.state.description}
onChange={this.onDescriptionChange}
/>
</Col>
</Row>
<br />
<Row>
<Col md={12}>
<Button
bsStyle="primary"
onClick={this.onSave}
className="m-r-3"
>
{t('Save')}
</Button>
<Button onClick={this.onCancel} className="cancelQuery">
{t('Cancel')}
</Button>
</Col>
</Row>
</FormGroup>
</Popover>
<FormGroup bsSize="small">
<Row>
<Col md={12}>
<small>
<label className="control-label" htmlFor="embed-height">
{t('Label')}
</label>
</small>
<FormControl
type="text"
placeholder={t('Label for your query')}
value={this.state.label}
onChange={this.onLabelChange}
/>
</Col>
</Row>
<br />
<Row>
<Col md={12}>
<small>
<label className="control-label" htmlFor="embed-height">{t('Description')}</label>
</small>
<FormControl
componentClass="textarea"
placeholder={t('Write a description for your query')}
value={this.state.description}
onChange={this.onDescriptionChange}
/>
</Col>
</Row>
<br />
<Row>
<Col md={12}>
<Button
bsStyle="primary"
onClick={this.onSave}
className="m-r-3"
>
{t('Save')}
</Button>
<Button onClick={this.onCancel} className="cancelQuery">
{t('Cancel')}
</Button>
</Col>
</Row>
</FormGroup>
);
}
render() {
return (
<span className="SaveQuery">
<Overlay
trigger="click"
target={this.state.target}
show={this.state.showSave}
placement="bottom"
animation={this.props.animation}
>
{this.renderPopover()}
</Overlay>
<Button bsSize="small" className="toggleSave" onClick={this.toggleSave}>
<i className="fa fa-save" /> {t('Save Query')}
</Button>
<ModalTrigger
ref={(ref) => { this.saveModal = ref; }}
modalTitle={t('Save Query')}
modalBody={this.renderModalBody()}
triggerNode={
<Button bsSize="small" className="toggleSave" onClick={this.toggleSave}>
<i className="fa fa-save" /> {t('Save Query')}
</Button>
}
bsSize="small"
/>
</span>
);
}

View File

@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import throttle from 'lodash.throttle';
import {
Col,
FormGroup,
@@ -15,6 +16,7 @@ import {
import SplitPane from 'react-split-pane';
import Button from '../../components/Button';
import TemplateParamsEditor from './TemplateParamsEditor';
import SouthPane from './SouthPane';
import SaveQuery from './SaveQuery';
import Timer from '../../components/Timer';
@@ -24,6 +26,7 @@ import { STATE_BSSTYLE_MAP } from '../constants';
import RunQueryActionButton from './RunQueryActionButton';
import { t } from '../../locales';
const propTypes = {
actions: PropTypes.object.isRequired,
height: PropTypes.string.isRequired,
@@ -50,6 +53,9 @@ class SqlEditor extends React.PureComponent {
autorun: props.queryEditor.autorun,
ctas: '',
};
this.onResize = this.onResize.bind(this);
this.throttledResize = throttle(this.onResize, 250);
}
componentWillMount() {
if (this.state.autorun) {
@@ -60,12 +66,16 @@ class SqlEditor extends React.PureComponent {
}
componentDidMount() {
this.onResize();
window.addEventListener('resize', this.throttledResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.throttledResize);
}
onResize() {
const height = this.sqlEditorHeight();
this.setState({
editorPaneHeight: this.refs.ace.clientHeight,
southPaneHeight: height - this.refs.ace.clientHeight,
editorPaneHeight: this.props.queryEditor.height,
southPaneHeight: height - this.props.queryEditor.height,
height,
});
@@ -95,6 +105,7 @@ class SqlEditor extends React.PureComponent {
tab: qe.title,
schema: qe.schema,
tempTableName: ctas ? this.state.ctas : '',
templateParams: qe.templateParams,
runAsync,
ctas,
};
@@ -165,25 +176,37 @@ class SqlEditor extends React.PureComponent {
<div className="sql-toolbar clearfix" id="js-sql-toolbar">
<div className="pull-left">
<Form inline>
<RunQueryActionButton
allowAsync={this.props.database ? this.props.database.allow_run_async : false}
dbId={qe.dbId}
queryState={this.props.latestQuery && this.props.latestQuery.state}
runQuery={this.runQuery.bind(this)}
selectedText={qe.selectedText}
stopQuery={this.stopQuery.bind(this)}
/>
<SaveQuery
defaultLabel={qe.title}
sql={qe.sql}
onSave={this.props.actions.saveQuery}
schema={qe.schema}
dbId={qe.dbId}
/>
<span className="m-r-5">
<RunQueryActionButton
allowAsync={this.props.database ? this.props.database.allow_run_async : false}
dbId={qe.dbId}
queryState={this.props.latestQuery && this.props.latestQuery.state}
runQuery={this.runQuery.bind(this)}
selectedText={qe.selectedText}
stopQuery={this.stopQuery.bind(this)}
/>
</span>
<span className="m-r-5">
<SaveQuery
defaultLabel={qe.title}
sql={qe.sql}
className="m-r-5"
onSave={this.props.actions.saveQuery}
schema={qe.schema}
dbId={qe.dbId}
/>
</span>
{ctasControls}
</Form>
</div>
<div className="pull-right">
<TemplateParamsEditor
language="json"
onChange={(params) => {
this.props.actions.queryEditorSetTemplateParams(qe, params);
}}
code={qe.templateParams}
/>
{limitWarning}
{this.props.latestQuery &&
<Timer
@@ -237,7 +260,7 @@ class SqlEditor extends React.PureComponent {
split="horizontal"
defaultSize={defaultNorthHeight}
minSize={100}
onChange={this.onResize.bind(this)}
onChange={this.onResize}
>
<div ref="ace" style={{ width: '100%' }}>
<div>
@@ -258,7 +281,7 @@ class SqlEditor extends React.PureComponent {
editorQueries={this.props.editorQueries}
dataPreviewQueries={this.props.dataPreviewQueries}
actions={this.props.actions}
height={this.state.southPaneHeight}
height={this.state.southPaneHeight || 0}
/>
</div>
</SplitPane>

View File

@@ -0,0 +1,129 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Badge } from 'react-bootstrap';
import AceEditor from 'react-ace';
import 'brace/mode/sql';
import 'brace/mode/json';
import 'brace/mode/html';
import 'brace/mode/markdown';
import 'brace/theme/textmate';
import ModalTrigger from '../../components/ModalTrigger';
import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
import Button from '../../components/Button';
import { t } from '../../locales';
const propTypes = {
onChange: PropTypes.func,
code: PropTypes.string,
language: PropTypes.oneOf(['yaml', 'json']),
};
const defaultProps = {
label: null,
description: null,
onChange: () => {},
code: '{}',
};
export default class TemplateParamsEditor extends React.Component {
constructor(props) {
super(props);
const codeText = props.code || '{}';
this.state = {
codeText,
parsedJSON: null,
isValid: true,
};
this.onChange = this.onChange.bind(this);
}
componentDidMount() {
this.onChange(this.state.codeText);
}
onChange(value) {
const codeText = value;
let isValid;
let parsedJSON = {};
try {
parsedJSON = JSON.parse(value);
isValid = true;
} catch (e) {
isValid = false;
}
this.setState({ parsedJSON, isValid, codeText });
if (isValid) {
this.props.onChange(codeText);
} else {
this.props.onChange('{}');
}
}
renderDoc() {
return (
<p>
Assign a set of parameters as <code>JSON</code> below
(example: <code>{'{"my_table": "foo"}'}</code>),
and they become available
in your SQL (example: <code>SELECT * FROM {'{{ my_table }}'} </code>)
by using
<a
href="http://superset.apache.org/sqllab.html#templating-with-jinja"
target="_blank"
rel="noopener noreferrer"
>
Jinja templating
</a> syntax.
</p>
);
}
renderModalBody() {
return (
<div>
{this.renderDoc()}
<AceEditor
mode={this.props.language}
theme="textmate"
style={{ border: '1px solid #CCC' }}
minLines={25}
maxLines={50}
onChange={this.onChange}
width="100%"
editorProps={{ $blockScrolling: true }}
enableLiveAutocompletion
value={this.state.codeText}
/>
</div>
);
}
render() {
const paramCount = this.state.parsedJSON ? Object.keys(this.state.parsedJSON).length : 0;
return (
<ModalTrigger
modalTitle={t('Template Parameters')}
triggerNode={
<Button
className="m-r-5"
tooltip={t('Edit template parameters')}
>
{`${t('parameters')} `}
{paramCount > 0 &&
<Badge>{paramCount}</Badge>
}
{!this.state.isValid &&
<InfoTooltipWithTrigger
icon="exclamation-triangle"
bsStyle="danger"
tooltip={t('Invalid JSON')}
label="invalid-json"
/>
}
</Button>
}
modalBody={this.renderModalBody(true)}
/>
);
}
}
TemplateParamsEditor.propTypes = propTypes;
TemplateParamsEditor.defaultProps = defaultProps;

View File

@@ -151,11 +151,12 @@ class VisualizeModal extends React.PureComponent {
}
visualize() {
this.props.actions.createDatasource(this.buildVizOptions(), this)
.done(() => {
.done((resp) => {
const columns = Object.keys(this.state.columns).map(k => this.state.columns[k]);
const data = JSON.parse(resp);
const mainGroupBy = columns.filter(d => d.is_dim)[0];
const formData = {
datasource: this.props.datasource,
datasource: `${data.table_id}__table`,
viz_type: this.state.chartType.value,
since: '100 years ago',
limit: '0',
@@ -293,7 +294,7 @@ function mapStateToProps(state) {
return {
datasource: state.datasource,
errorMessage: state.errorMessage,
timeout: state.common ? state.common.SUPERSET_WEBSERVER_TIMEOUT : null,
timeout: state.common ? state.common.conf.SUPERSET_WEBSERVER_TIMEOUT : null,
};
}

View File

@@ -289,8 +289,14 @@ a.Link {
.tooltip-inner {
max-width: 500px;
}
.SplitPane.horizontal {
padding-right: 4px;
}
.SouthPane {
margin-top: 10px;
position: absolute;
width: 100%;
overflow: scroll;
}
.search-date-filter-container {
display: flex;

View File

@@ -211,6 +211,9 @@ export const sqlLabReducer = function (state, action) {
[actions.QUERY_EDITOR_SET_SQL]() {
return alterInArr(state, 'queryEditors', action.queryEditor, { sql: action.sql });
},
[actions.QUERY_EDITOR_SET_TEMPLATE_PARAMS]() {
return alterInArr(state, 'queryEditors', action.queryEditor, { templateParams: action.templateParams });
},
[actions.QUERY_EDITOR_SET_SELECTED_TEXT]() {
return alterInArr(state, 'queryEditors', action.queryEditor, { selectedText: action.sql });
},

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { Button, Panel, Grid, Row, Col } from 'react-bootstrap';
import Select from 'react-virtualized-select';
import visTypes from '../explore/stores/visTypes';
import { t } from '../locales';
const propTypes = {
datasources: PropTypes.arrayOf(PropTypes.shape({
@@ -50,30 +51,30 @@ export default class AddSliceContainer extends React.PureComponent {
render() {
return (
<div className="container">
<Panel header={<h3>{('Create a new slice')}</h3>}>
<Panel header={<h3>{t('Create a new slice')}</h3>}>
<Grid>
<Row>
<Col xs={12} sm={6}>
<div>
<p>{('Choose a datasource')}</p>
<p>{t('Choose a datasource')}</p>
<Select
clearable={false}
name="select-datasource"
onChange={this.changeDatasource.bind(this)}
options={this.props.datasources}
placeholder={('Choose a datasource')}
placeholder={t('Choose a datasource')}
value={this.state.datasourceValue}
/>
</div>
<br />
<div>
<p>{('Choose a visualization type')}</p>
<p>{t('Choose a visualization type')}</p>
<Select
clearable={false}
name="select-vis-type"
onChange={this.changeVisType.bind(this)}
options={this.vizTypeOptions}
placeholder={('Choose a visualization type')}
placeholder={t('Choose a visualization type')}
value={this.state.visType}
/>
</div>
@@ -83,7 +84,7 @@ export default class AddSliceContainer extends React.PureComponent {
disabled={this.isBtnDisabled()}
onClick={this.gotoSlice.bind(this)}
>
{('Create new slice')}
{t('Create new slice')}
</Button>
<br /><br />
</Col>

View File

@@ -0,0 +1,229 @@
/* eslint camelcase: 0 */
import React from 'react';
import PropTypes from 'prop-types';
import Mustache from 'mustache';
import { Tooltip } from 'react-bootstrap';
import { d3format } from '../modules/utils';
import ChartBody from './ChartBody';
import Loading from '../components/Loading';
import { Logger, LOG_ACTIONS_RENDER_EVENT } from '../logger';
import StackTraceMessage from '../components/StackTraceMessage';
import visMap from '../../visualizations/main';
import sandboxedEval from '../modules/sandbox';
import './chart.css';
const propTypes = {
annotationData: PropTypes.object,
actions: PropTypes.object,
chartKey: PropTypes.string.isRequired,
containerId: PropTypes.string.isRequired,
datasource: PropTypes.object.isRequired,
formData: PropTypes.object.isRequired,
height: PropTypes.number,
width: PropTypes.number,
setControlValue: PropTypes.func,
timeout: PropTypes.number,
vizType: PropTypes.string.isRequired,
// state
chartAlert: PropTypes.string,
chartStatus: PropTypes.string,
chartUpdateEndTime: PropTypes.number,
chartUpdateStartTime: PropTypes.number,
latestQueryFormData: PropTypes.object,
queryRequest: PropTypes.object,
queryResponse: PropTypes.object,
lastRendered: PropTypes.number,
triggerQuery: PropTypes.bool,
// dashboard callbacks
addFilter: PropTypes.func,
getFilters: PropTypes.func,
clearFilter: PropTypes.func,
removeFilter: PropTypes.func,
};
const defaultProps = {
addFilter: () => ({}),
getFilters: () => ({}),
clearFilter: () => ({}),
removeFilter: () => ({}),
};
class Chart extends React.PureComponent {
constructor(props) {
super(props);
this.state = {};
// these properties are used by visualizations
this.annotationData = props.annotationData;
this.containerId = props.containerId;
this.selector = `#${this.containerId}`;
this.formData = props.formData;
this.datasource = props.datasource;
this.addFilter = this.addFilter.bind(this);
this.getFilters = this.getFilters.bind(this);
this.clearFilter = this.clearFilter.bind(this);
this.removeFilter = this.removeFilter.bind(this);
this.height = this.height.bind(this);
this.width = this.width.bind(this);
}
componentDidMount() {
if (this.props.triggerQuery) {
this.props.actions.runQuery(this.props.formData, false,
this.props.timeout,
this.props.chartKey,
);
}
}
componentWillReceiveProps(nextProps) {
this.annotationData = nextProps.annotationData;
this.containerId = nextProps.containerId;
this.selector = `#${this.containerId}`;
this.formData = nextProps.formData;
this.datasource = nextProps.datasource;
}
componentDidUpdate(prevProps) {
if (
this.props.queryResponse &&
['success', 'rendered'].indexOf(this.props.chartStatus) > -1 &&
!this.props.queryResponse.error && (
prevProps.annotationData !== this.props.annotationData ||
prevProps.queryResponse !== this.props.queryResponse ||
prevProps.height !== this.props.height ||
prevProps.width !== this.props.width ||
prevProps.lastRendered !== this.props.lastRendered)
) {
this.renderViz();
}
}
getFilters() {
return this.props.getFilters();
}
setTooltip(tooltip) {
this.setState({ tooltip });
}
addFilter(col, vals, merge = true, refresh = true) {
this.props.addFilter(col, vals, merge, refresh);
}
clearFilter() {
this.props.clearFilter();
}
removeFilter(col, vals, refresh = true) {
this.props.removeFilter(col, vals, refresh);
}
clearError() {
this.setState({
errorMsg: null,
});
}
width() {
return this.props.width || this.container.el.offsetWidth;
}
height() {
return this.props.height || this.container.el.offsetHeight;
}
d3format(col, number) {
const { datasource } = this.props;
const format = (datasource.column_formats && datasource.column_formats[col]) || '0.3s';
return d3format(format, number);
}
render_template(s) {
const context = {
width: this.width(),
height: this.height(),
};
return Mustache.render(s, context);
}
renderTooltip() {
if (this.state.tooltip) {
/* eslint-disable react/no-danger */
return (
<Tooltip
className="chart-tooltip"
id="chart-tooltip"
placement="right"
positionTop={this.state.tooltip.y - 10}
positionLeft={this.state.tooltip.x + 30}
arrowOffsetTop={10}
>
<div dangerouslySetInnerHTML={{ __html: this.state.tooltip.content }} />
</Tooltip>
);
/* eslint-enable react/no-danger */
}
return null;
}
renderViz() {
const viz = visMap[this.props.vizType];
const fd = this.props.formData;
const qr = this.props.queryResponse;
const renderStart = Logger.getTimestamp();
try {
// Executing user-defined data mutator function
if (fd.js_data) {
qr.data = sandboxedEval(fd.js_data)(qr.data);
}
// [re]rendering the visualization
viz(this, qr, this.props.setControlValue);
Logger.append(LOG_ACTIONS_RENDER_EVENT, {
label: this.props.chartKey,
vis_type: this.props.vizType,
start_offset: renderStart,
duration: Logger.getTimestamp() - renderStart,
});
this.props.actions.chartRenderingSucceeded(this.props.chartKey);
} catch (e) {
this.props.actions.chartRenderingFailed(e, this.props.chartKey);
}
}
render() {
const isLoading = this.props.chartStatus === 'loading';
return (
<div className={`token col-md-12 ${isLoading ? 'is-loading' : ''}`}>
{this.renderTooltip()}
{isLoading &&
<Loading size={25} />
}
{this.props.chartAlert &&
<StackTraceMessage
message={this.props.chartAlert}
queryResponse={this.props.queryResponse}
/>
}
{!isLoading && !this.props.chartAlert &&
<ChartBody
containerId={this.containerId}
vizType={this.props.vizType}
height={this.height}
width={this.width}
ref={(inner) => {
this.container = inner;
}}
/>
}
</div>
);
}
}
Chart.propTypes = propTypes;
Chart.defaultProps = defaultProps;
export default Chart;

View File

@@ -0,0 +1,54 @@
import React from 'react';
import PropTypes from 'prop-types';
import $ from 'jquery';
const propTypes = {
containerId: PropTypes.string.isRequired,
vizType: PropTypes.string.isRequired,
height: PropTypes.func.isRequired,
width: PropTypes.func.isRequired,
};
class ChartBody extends React.PureComponent {
html(data) {
this.el.innerHTML = data;
}
css(property, value) {
this.el.style[property] = value;
}
get(n) {
return $(this.el).get(n);
}
find(classname) {
return $(this.el).find(classname);
}
show() {
return $(this.el).show();
}
height() {
return this.props.height();
}
width() {
return this.props.width();
}
render() {
return (
<div
id={this.props.containerId}
className={`slice_container ${this.props.vizType}`}
ref={(el) => { this.el = el; }}
/>
);
}
}
ChartBody.propTypes = propTypes;
export default ChartBody;

View File

@@ -0,0 +1,29 @@
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as Actions from './chartAction';
import Chart from './Chart';
function mapStateToProps({ charts }, ownProps) {
const chart = charts[ownProps.chartKey];
return {
annotationData: chart.annotationData,
chartAlert: chart.chartAlert,
chartStatus: chart.chartStatus,
chartUpdateEndTime: chart.chartUpdateEndTime,
chartUpdateStartTime: chart.chartUpdateStartTime,
latestQueryFormData: chart.latestQueryFormData,
lastRendered: chart.lastRendered,
queryResponse: chart.queryResponse,
queryRequest: chart.queryRequest,
triggerQuery: chart.triggerQuery,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Chart);

View File

@@ -0,0 +1,4 @@
.chart-tooltip {
opacity: 0.75;
font-size: 12px;
}

View File

@@ -0,0 +1,172 @@
import { getExploreUrl, getAnnotationJsonUrl } from '../explore/exploreUtils';
import { requiresQuery, ANNOTATION_SOURCE_TYPES } from '../modules/AnnotationTypes';
import { Logger, LOG_ACTIONS_LOAD_EVENT } from '../logger';
const $ = window.$ = require('jquery');
export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
export function chartUpdateStarted(queryRequest, key) {
return { type: CHART_UPDATE_STARTED, queryRequest, key };
}
export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED';
export function chartUpdateSucceeded(queryResponse, key) {
return { type: CHART_UPDATE_SUCCEEDED, queryResponse, key };
}
export const CHART_UPDATE_STOPPED = 'CHART_UPDATE_STOPPED';
export function chartUpdateStopped(key) {
return { type: CHART_UPDATE_STOPPED, key };
}
export const CHART_UPDATE_TIMEOUT = 'CHART_UPDATE_TIMEOUT';
export function chartUpdateTimeout(statusText, timeout, key) {
return { type: CHART_UPDATE_TIMEOUT, statusText, timeout, key };
}
export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED';
export function chartUpdateFailed(queryResponse, key) {
return { type: CHART_UPDATE_FAILED, queryResponse, key };
}
export const CHART_RENDERING_FAILED = 'CHART_RENDERING_FAILED';
export function chartRenderingFailed(error, key) {
return { type: CHART_RENDERING_FAILED, error, key };
}
export const CHART_RENDERING_SUCCEEDED = 'CHART_RENDERING_SUCCEEDED';
export function chartRenderingSucceeded(key) {
return { type: CHART_RENDERING_SUCCEEDED, key };
}
export const REMOVE_CHART = 'REMOVE_CHART';
export function removeChart(key) {
return { type: REMOVE_CHART, key };
}
export const ANNOTATION_QUERY_SUCCESS = 'ANNOTATION_QUERY_SUCCESS';
export function annotationQuerySuccess(annotation, queryResponse, key) {
return { type: ANNOTATION_QUERY_SUCCESS, annotation, queryResponse, key };
}
export const ANNOTATION_QUERY_STARTED = 'ANNOTATION_QUERY_STARTED';
export function annotationQueryStarted(annotation, queryRequest, key) {
return { type: ANNOTATION_QUERY_STARTED, annotation, queryRequest, key };
}
export const ANNOTATION_QUERY_FAILED = 'ANNOTATION_QUERY_FAILED';
export function annotationQueryFailed(annotation, queryResponse, key) {
return { type: ANNOTATION_QUERY_FAILED, annotation, queryResponse, key };
}
export function runAnnotationQuery(annotation, timeout = 60, formData = null, key) {
return function (dispatch, getState) {
const sliceKey = key || Object.keys(getState().charts)[0];
const fd = formData || getState().charts[sliceKey].latestQueryFormData;
if (!requiresQuery(annotation.sourceType)) {
return Promise.resolve();
}
const sliceFormData = Object.keys(annotation.overrides)
.reduce((d, k) => ({
...d,
[k]: annotation.overrides[k] || fd[k],
}), {});
const isNative = annotation.sourceType === ANNOTATION_SOURCE_TYPES.NATIVE;
const url = getAnnotationJsonUrl(annotation.value, sliceFormData, isNative);
const queryRequest = $.ajax({
url,
dataType: 'json',
timeout: timeout * 1000,
});
dispatch(annotationQueryStarted(annotation, queryRequest, sliceKey));
return queryRequest
.then(queryResponse => dispatch(annotationQuerySuccess(annotation, queryResponse, sliceKey)))
.catch((err) => {
if (err.statusText === 'timeout') {
dispatch(annotationQueryFailed(annotation, { error: 'Query Timeout' }, sliceKey));
} else if ((err.responseJSON.error || '').toLowerCase().startsWith('no data')) {
dispatch(annotationQuerySuccess(annotation, err, sliceKey));
} else if (err.statusText !== 'abort') {
dispatch(annotationQueryFailed(annotation, err.responseJSON, sliceKey));
}
});
};
}
export const TRIGGER_QUERY = 'TRIGGER_QUERY';
export function triggerQuery(value = true, key) {
return { type: TRIGGER_QUERY, value, key };
}
// this action is used for forced re-render without fetch data
export const RENDER_TRIGGERED = 'RENDER_TRIGGERED';
export function renderTriggered(value, key) {
return { type: RENDER_TRIGGERED, value, key };
}
export const RUN_QUERY = 'RUN_QUERY';
export function runQuery(formData, force = false, timeout = 60, key) {
return (dispatch) => {
const url = getExploreUrl(formData, 'json', force);
let logStart;
const queryRequest = $.ajax({
url,
dataType: 'json',
timeout: timeout * 1000,
beforeSend: () => {
logStart = Logger.getTimestamp();
},
});
const queryPromise = Promise.resolve(dispatch(chartUpdateStarted(queryRequest, key)))
.then(() => queryRequest)
.then((queryResponse) => {
Logger.append(LOG_ACTIONS_LOAD_EVENT, {
label: key,
is_cached: queryResponse.is_cached,
row_count: queryResponse.rowcount,
datasource: formData.datasource,
start_offset: logStart,
duration: Logger.getTimestamp() - logStart,
});
return dispatch(chartUpdateSucceeded(queryResponse, key));
})
.catch((err) => {
Logger.append(LOG_ACTIONS_LOAD_EVENT, {
label: key,
has_err: true,
datasource: formData.datasource,
start_offset: logStart,
duration: Logger.getTimestamp() - logStart,
});
if (err.statusText === 'timeout') {
dispatch(chartUpdateTimeout(err.statusText, timeout, key));
} else if (err.statusText === 'abort') {
dispatch(chartUpdateStopped(key));
} else {
let errObject;
if (err.responseJSON) {
errObject = err.responseJSON;
} else if (err.stack) {
errObject = {
error: 'Unexpected error: ' + err.description,
stacktrace: err.stack,
};
} else {
errObject = {
error: 'Unexpected error.',
};
}
dispatch(chartUpdateFailed(errObject, key));
}
});
const annotationLayers = formData.annotation_layers || [];
return Promise.all([
queryPromise,
dispatch(triggerQuery(false, key)),
...annotationLayers.map(x => dispatch(runAnnotationQuery(x, timeout, formData, key))),
]);
};
}

View File

@@ -0,0 +1,155 @@
/* eslint camelcase: 0 */
import PropTypes from 'prop-types';
import { now } from '../modules/dates';
import * as actions from './chartAction';
import { t } from '../locales';
export const chartPropType = {
chartKey: PropTypes.string.isRequired,
chartAlert: PropTypes.string,
chartStatus: PropTypes.string,
chartUpdateEndTime: PropTypes.number,
chartUpdateStartTime: PropTypes.number,
latestQueryFormData: PropTypes.object,
queryRequest: PropTypes.object,
queryResponse: PropTypes.object,
triggerQuery: PropTypes.bool,
lastRendered: PropTypes.number,
};
export const chart = {
chartKey: '',
chartAlert: null,
chartStatus: 'loading',
chartUpdateEndTime: null,
chartUpdateStartTime: now(),
latestQueryFormData: null,
queryRequest: null,
queryResponse: null,
triggerQuery: true,
lastRendered: 0,
};
export default function chartReducer(charts = {}, action) {
const actionHandlers = {
[actions.CHART_UPDATE_SUCCEEDED](state) {
return { ...state,
chartStatus: 'success',
queryResponse: action.queryResponse,
chartUpdateEndTime: now(),
};
},
[actions.CHART_UPDATE_STARTED](state) {
return { ...state,
chartStatus: 'loading',
chartAlert: null,
chartUpdateEndTime: null,
chartUpdateStartTime: now(),
queryRequest: action.queryRequest,
};
},
[actions.CHART_UPDATE_STOPPED](state) {
return { ...state,
chartStatus: 'stopped',
chartAlert: t('Updating chart was stopped'),
};
},
[actions.CHART_RENDERING_SUCCEEDED](state) {
return { ...state,
chartStatus: 'rendered',
};
},
[actions.CHART_RENDERING_FAILED](state) {
return { ...state,
chartStatus: 'failed',
chartAlert: t('An error occurred while rendering the visualization: %s', action.error),
};
},
[actions.CHART_UPDATE_TIMEOUT](state) {
return { ...state,
chartStatus: 'failed',
chartAlert: (
`<strong>${t('Query timeout')}</strong> - ` +
t(`visualization queries are set to timeout at ${action.timeout} seconds. `) +
t('Perhaps your data has grown, your database is under unusual load, ' +
'or you are simply querying a data source that is too large ' +
'to be processed within the timeout range. ' +
'If that is the case, we recommend that you summarize your data further.')),
};
},
[actions.CHART_UPDATE_FAILED](state) {
return { ...state,
chartStatus: 'failed',
chartAlert: action.queryResponse ? action.queryResponse.error : t('Network error.'),
chartUpdateEndTime: now(),
queryResponse: action.queryResponse,
};
},
[actions.TRIGGER_QUERY](state) {
return { ...state, triggerQuery: action.value };
},
[actions.RENDER_TRIGGERED](state) {
return { ...state, lastRendered: action.value };
},
[actions.ANNOTATION_QUERY_STARTED](state) {
if (state.annotationQuery &&
state.annotationQuery[action.annotation.name]) {
state.annotationQuery[action.annotation.name].abort();
}
const annotationQuery = {
...state.annotationQuery,
[action.annotation.name]: action.queryRequest,
};
return {
...state,
annotationQuery,
};
},
[actions.ANNOTATION_QUERY_SUCCESS](state) {
const annotationData = {
...state.annotationData,
[action.annotation.name]: action.queryResponse.data,
};
const annotationError = { ...state.annotationError };
delete annotationError[action.annotation.name];
const annotationQuery = { ...state.annotationQuery };
delete annotationQuery[action.annotation.name];
return {
...state,
annotationData,
annotationError,
annotationQuery,
};
},
[actions.ANNOTATION_QUERY_FAILED](state) {
const annotationData = { ...state.annotationData };
delete annotationData[action.annotation.name];
const annotationError = {
...state.annotationError,
[action.annotation.name]: action.queryResponse ?
action.queryResponse.error : t('Network error.'),
};
const annotationQuery = { ...state.annotationQuery };
delete annotationQuery[action.annotation.name];
return {
...state,
annotationData,
annotationError,
annotationQuery,
};
},
};
/* eslint-disable no-param-reassign */
if (action.type === actions.REMOVE_CHART) {
delete charts[action.key];
return charts;
}
if (action.type in actionHandlers) {
return { ...charts, [action.key]: actionHandlers[action.type](charts[action.key], action) };
}
return charts;
}

View File

@@ -0,0 +1,145 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Table, Tr, Td, Thead, Th } from 'reactable';
import { isEqual, isEmpty } from 'underscore';
import TooltipWrapper from './TooltipWrapper';
import { controls } from '../explore/stores/controls';
import ModalTrigger from './ModalTrigger';
import { t } from '../locales';
const propTypes = {
origFormData: PropTypes.object.isRequired,
currentFormData: PropTypes.object.isRequired,
};
export default class AlteredSliceTag extends React.Component {
constructor(props) {
super(props);
const diffs = this.getDiffs(props);
this.state = { diffs, hasDiffs: !isEmpty(diffs) };
}
componentWillReceiveProps(newProps) {
// Update differences if need be
if (isEqual(this.props, newProps)) {
return;
}
const diffs = this.getDiffs(newProps);
this.setState({ diffs, hasDiffs: !isEmpty(diffs) });
}
getDiffs(props) {
// Returns all properties that differ in the
// current form data and the saved form data
const ofd = props.origFormData;
const cfd = props.currentFormData;
const fdKeys = Object.keys(cfd);
const diffs = {};
for (const fdKey of fdKeys) {
// Ignore values that are undefined/nonexisting in either
if (!ofd[fdKey] && !cfd[fdKey]) {
continue;
}
if (!isEqual(ofd[fdKey], cfd[fdKey])) {
diffs[fdKey] = { before: ofd[fdKey], after: cfd[fdKey] };
}
}
return diffs;
}
formatValue(value, key) {
// Format display value based on the control type
// or the value type
if (value === undefined) {
return 'N/A';
} else if (value === null) {
return 'null';
} else if (controls[key] && controls[key].type === 'FilterControl') {
if (!value.length) {
return '[]';
}
return value.map((v) => {
const filterVal = v.val.constructor === Array ? `[${v.val.join(', ')}]` : v.val;
return `${v.col} ${v.op} ${filterVal}`;
}).join(', ');
} else if (controls[key] && controls[key].type === 'BoundsControl') {
return `Min: ${value[0]}, Max: ${value[1]}`;
} else if (controls[key] && controls[key].type === 'CollectionControl') {
return value.map(v => JSON.stringify(v)).join(', ');
} else if (typeof value === 'boolean') {
return value ? 'true' : 'false';
} else if (value.constructor === Array) {
return value.length ? value.join(', ') : '[]';
} else if (typeof value === 'string' || typeof value === 'number') {
return value;
}
return JSON.stringify(value);
}
renderRows() {
const diffs = this.state.diffs;
const rows = [];
for (const key in diffs) {
rows.push(
<Tr key={key}>
<Td column="control" data={(controls[key] && controls[key].label) || key} />
<Td column="before">{this.formatValue(diffs[key].before, key)}</Td>
<Td column="after">{this.formatValue(diffs[key].after, key)}</Td>
</Tr>,
);
}
return rows;
}
renderModalBody() {
return (
<Table className="table" sortable>
<Thead>
<Th column="control">Control</Th>
<Th column="before">Before</Th>
<Th column="after">After</Th>
</Thead>
{this.renderRows()}
</Table>
);
}
renderTriggerNode() {
return (
<TooltipWrapper
label="difference"
tooltip={t('Click to see difference')}
>
<span
className="label label-warning m-l-5"
style={{ fontSize: '12px' }}
>
{t('Altered')}
</span>
</TooltipWrapper>
);
}
render() {
// Return nothing if there are no differences
if (!this.state.hasDiffs) {
return null;
}
// Render the label-warning 'Altered' tag which the user may
// click to open a modal containing a table summarizing the
// differences in the slice
return (
<ModalTrigger
animation
triggerNode={this.renderTriggerNode()}
modalTitle={t('Slice changes')}
bsSize="large"
modalBody={this.renderModalBody()}
/>
);
}
}
AlteredSliceTag.propTypes = propTypes;

View File

@@ -42,19 +42,20 @@ class AsyncSelect extends React.PureComponent {
fetchOptions() {
this.setState({ isLoading: true });
const mutator = this.props.mutator;
$.get(this.props.dataEndpoint, (data) => {
this.setState({ options: mutator ? mutator(data) : data, isLoading: false });
$.get(this.props.dataEndpoint)
.done((data) => {
this.setState({ options: mutator ? mutator(data) : data, isLoading: false });
if (!this.props.value && this.props.autoSelect && this.state.options.length) {
this.onChange(this.state.options[0]);
}
})
.fail(() => {
this.props.onAsyncError();
})
.always(() => {
this.setState({ isLoading: false });
});
if (!this.props.value && this.props.autoSelect && this.state.options.length) {
this.onChange(this.state.options[0]);
}
})
.fail((xhr) => {
this.props.onAsyncError(xhr.responseText);
})
.always(() => {
this.setState({ isLoading: false });
});
}
render() {
return (

View File

@@ -5,9 +5,13 @@ import InfoTooltipWithTrigger from './InfoTooltipWithTrigger';
const propTypes = {
column: PropTypes.object.isRequired,
showType: PropTypes.bool,
};
const defaultProps = {
showType: false,
};
export default function ColumnOption({ column }) {
export default function ColumnOption({ column, showType }) {
return (
<span>
<span className="m-r-5 option-label">
@@ -29,6 +33,10 @@ export default function ColumnOption({ column }) {
label={`expr-${column.column_name}`}
/>
}
{showType &&
<span className="text-muted">{column.type}</span>
}
</span>);
}
ColumnOption.propTypes = propTypes;
ColumnOption.defaultProps = defaultProps;

View File

@@ -56,14 +56,16 @@ export default class CopyToClipboard extends React.Component {
selection.removeAllRanges();
document.activeElement.blur();
const range = document.createRange();
const textArea = document.createElement('textarea');
const span = document.createElement('span');
span.textContent = textToCopy;
span.style.all = 'unset';
span.style.position = 'fixed';
span.style.top = 0;
span.style.clip = 'rect(0, 0, 0, 0)';
span.style.whiteSpace = 'pre';
textArea.style.position = 'fixed';
textArea.style.left = '-1000px';
textArea.value = textToCopy;
document.body.appendChild(textArea);
range.selectNode(textArea);
document.body.appendChild(span);
range.selectNode(span);
selection.addRange(range);
try {
if (!document.execCommand('copy')) {
@@ -73,7 +75,7 @@ export default class CopyToClipboard extends React.Component {
window.alert(t('Sorry, your browser does not support copying. Use Ctrl / Cmd + C!')); // eslint-disable-line
}
document.body.removeChild(textArea);
document.body.removeChild(span);
if (selection.removeRange) {
selection.removeRange(range);
} else {

View File

@@ -8,10 +8,12 @@ const propTypes = {
canEdit: PropTypes.bool,
onSaveTitle: PropTypes.func,
noPermitTooltip: PropTypes.string,
showTooltip: PropTypes.bool,
};
const defaultProps = {
title: t('Title'),
canEdit: false,
showTooltip: true,
};
class EditableTitle extends React.PureComponent {
@@ -85,24 +87,30 @@ class EditableTitle extends React.PureComponent {
}
}
render() {
return (
<span className="editable-title">
let input = (
<input
required
type={this.state.isEditing ? 'text' : 'button'}
value={this.state.title}
onChange={this.handleChange}
onBlur={this.handleBlur}
onClick={this.handleClick}
onKeyPress={this.handleKeyPress}
/>
);
if (this.props.showTooltip) {
input = (
<TooltipWrapper
label="title"
tooltip={this.props.canEdit ? t('click to edit title') :
this.props.noPermitTooltip || t('You don\'t have the rights to alter this title.')}
>
<input
required
type={this.state.isEditing ? 'text' : 'button'}
value={this.state.title}
onChange={this.handleChange}
onBlur={this.handleBlur}
onClick={this.handleClick}
onKeyPress={this.handleKeyPress}
/>
{input}
</TooltipWrapper>
</span>
);
}
return (
<span className="editable-title">{input}</span>
);
}
}

View File

@@ -5,19 +5,20 @@ import TooltipWrapper from './TooltipWrapper';
import { t } from '../locales';
const propTypes = {
sliceId: PropTypes.number.isRequired,
actions: PropTypes.object.isRequired,
itemId: PropTypes.number.isRequired,
fetchFaveStar: PropTypes.func,
saveFaveStar: PropTypes.func,
isStarred: PropTypes.bool.isRequired,
};
export default class FaveStar extends React.Component {
componentDidMount() {
this.props.actions.fetchFaveStar(this.props.sliceId);
this.props.fetchFaveStar(this.props.itemId);
}
onClick(e) {
e.preventDefault();
this.props.actions.saveFaveStar(this.props.sliceId, this.props.isStarred);
this.props.saveFaveStar(this.props.itemId, this.props.isStarred);
}
render() {

View File

@@ -1,8 +1,12 @@
.ReactVirtualized__Grid__innerScrollContainer {
border: 1px solid #ccc;
}
.ReactVirtualized__Table__headerRow {
font-weight: 700;
display: flex;
flex-direction: row;
align-items: center;
border: 1px solid #ccc;
}
.ReactVirtualized__Table__row {
display: flex;
@@ -50,11 +54,6 @@
}
.even-row { background: #f2f2f2; }
.odd-row { background: #ffffff; }
.even-row,
.odd-row {
border: none;
}
.filterable-table-container {
overflow: auto;
border: 1px solid #ccc;
}

View File

@@ -21,7 +21,7 @@ const tooltipStyle = { wordWrap: 'break-word' };
export default function InfoTooltipWithTrigger({
label, tooltip, icon, className, onClick, placement, bsStyle }) {
const iconClass = `fa fa-${icon} ${className} ${bsStyle ? 'text-' + bsStyle : ''}`;
const iconClass = `fa fa-${icon} ${className} ${bsStyle ? `text-${bsStyle}` : ''}`;
const iconEl = (
<i
className={iconClass}

View File

@@ -19,6 +19,7 @@ export default function Loading(props) {
height: props.size,
padding: 0,
margin: 0,
position: 'absolute',
}}
/>
);

View File

@@ -5,18 +5,20 @@ import InfoTooltipWithTrigger from './InfoTooltipWithTrigger';
const propTypes = {
metric: PropTypes.object.isRequired,
openInNewWindow: PropTypes.bool,
showFormula: PropTypes.bool,
url: PropTypes.string,
};
const defaultProps = {
showFormula: true,
};
export default function MetricOption({ metric, showFormula }) {
export default function MetricOption({ metric, openInNewWindow, showFormula, url }) {
const verbose = metric.verbose_name || metric.metric_name;
const link = url ? <a href={url} target={openInNewWindow ? '_blank' : null}>{verbose}</a> : verbose;
return (
<div>
<span className="m-r-5 option-label">
{metric.verbose_name || metric.metric_name}
</span>
<span className="m-r-5 option-label">{link}</span>
{metric.description &&
<InfoTooltipWithTrigger
className="m-r-5 text-muted"

View File

@@ -1,7 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Modal } from 'react-bootstrap';
import { Modal, MenuItem } from 'react-bootstrap';
import cx from 'classnames';
import Button from './Button';
const propTypes = {
@@ -13,6 +14,7 @@ const propTypes = {
beforeOpen: PropTypes.func,
onExit: PropTypes.func,
isButton: PropTypes.bool,
isMenuItem: PropTypes.bool,
bsSize: PropTypes.string,
className: PropTypes.string,
tooltip: PropTypes.string,
@@ -23,6 +25,7 @@ const defaultProps = {
beforeOpen: () => {},
onExit: () => {},
isButton: false,
isMenuItem: false,
bsSize: null,
className: '',
};
@@ -86,6 +89,13 @@ export default class ModalTrigger extends React.Component {
{this.renderModal()}
</Button>
);
} else if (this.props.isMenuItem) {
return (
<MenuItem onClick={this.open}>
{this.props.triggerNode}
{this.renderModal()}
</MenuItem>
);
}
/* eslint-disable jsx-a11y/interactive-supports-focus */
return (

View File

@@ -0,0 +1,87 @@
import React from 'react';
import PropTypes from 'prop-types';
import Select from 'react-select';
export default class OnPasteSelect extends React.Component {
onPaste(evt) {
if (!this.props.multi) {
return;
}
evt.preventDefault();
const clipboard = evt.clipboardData.getData('Text');
if (!clipboard) {
return;
}
const regex = `[${this.props.separator}]+`;
const values = clipboard.split(new RegExp(regex)).map(v => v.trim());
const validator = this.props.isValidNewOption;
const selected = this.props.value || [];
const existingOptions = {};
const existing = {};
this.props.options.forEach((v) => {
existingOptions[v[this.props.valueKey]] = 1;
});
let options = [];
selected.forEach((v) => {
options.push({ [this.props.labelKey]: v, [this.props.valueKey]: v });
existing[v] = 1;
});
options = options.concat(values
.filter((v) => {
const notExists = !existing[v];
existing[v] = 1;
return notExists && (validator ? validator({ [this.props.labelKey]: v }) : !!v);
})
.map((v) => {
const opt = { [this.props.labelKey]: v, [this.props.valueKey]: v };
if (!existingOptions[v]) {
this.props.options.unshift(opt);
}
return opt;
}),
);
if (options.length) {
if (this.props.onChange) {
this.props.onChange(options);
}
}
}
render() {
const SelectComponent = this.props.selectWrap;
const refFunc = (ref) => {
if (this.props.ref) {
this.props.ref(ref);
}
this.pasteInput = ref;
};
const inputProps = { onPaste: this.onPaste.bind(this) };
return (
<SelectComponent
{...this.props}
ref={refFunc}
inputProps={inputProps}
/>
);
}
}
OnPasteSelect.propTypes = {
separator: PropTypes.string.isRequired,
selectWrap: PropTypes.func.isRequired,
ref: PropTypes.func,
onChange: PropTypes.func.isRequired,
valueKey: PropTypes.string.isRequired,
labelKey: PropTypes.string.isRequired,
options: PropTypes.array,
multi: PropTypes.bool.isRequired,
value: PropTypes.any,
isValidNewOption: PropTypes.func,
};
OnPasteSelect.defaultProps = {
separator: ',',
selectWrap: Select,
valueKey: 'value',
labelKey: 'label',
options: [],
multi: false,
};

View File

@@ -0,0 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import InfoTooltipWithTrigger from './InfoTooltipWithTrigger';
const propTypes = {
option: PropTypes.object.isRequired,
};
// This component provides a general tooltip for options
// in a SelectControl
export default function OptionDescription({ option }) {
return (
<span>
<span className="m-r-5 option-label">
{option.label}
</span>
{option.description &&
<InfoTooltipWithTrigger
className="m-r-5 text-muted"
icon="question-circle-o"
tooltip={option.description}
label={`descr-${option.label}`}
/>
}
</span>);
}
OptionDescription.propTypes = propTypes;

View File

@@ -23,7 +23,7 @@ export default function PopoverSection({ title, isSelected, children, onSelect,
&nbsp;
<i className={isSelected ? 'fa fa-check text-primary' : ''} />
</div>
<div>
<div className="m-t-5 m-l-5">
{children}
</div>
</div>);

View File

@@ -0,0 +1,52 @@
/* eslint-disable react/no-danger */
import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Collapse } from 'react-bootstrap';
const propTypes = {
message: PropTypes.string,
queryResponse: PropTypes.object,
showStackTrace: PropTypes.bool,
};
const defaultProps = {
showStackTrace: false,
};
class StackTraceMessage extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
showStackTrace: props.showStackTrace,
};
}
hasTrace() {
return this.props.queryResponse && this.props.queryResponse.stacktrace;
}
render() {
return (
<div className={`stack-trace-container${this.hasTrace() ? ' has-trace' : ''}`}>
<Alert
bsStyle="warning"
onClick={() => this.setState({ showStackTrace: !this.state.showStackTrace })}
>
{this.props.message}
</Alert>
{this.hasTrace() &&
<Collapse in={this.state.showStackTrace}>
<pre>
{this.props.queryResponse.stacktrace}
</pre>
</Collapse>
}
</div>
);
}
}
StackTraceMessage.propTypes = propTypes;
StackTraceMessage.defaultProps = defaultProps;
export default StackTraceMessage;

View File

@@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
export default function VirtualizedRendererWrap(renderer) {
function WrapperRenderer({
focusedOption,
focusOption,
key,
option,
selectValue,
style,
valueArray,
}) {
if (!option) {
return null;
}
const className = ['VirtualizedSelectOption'];
if (option === focusedOption) {
className.push('VirtualizedSelectFocusedOption');
}
if (option.disabled) {
className.push('VirtualizedSelectDisabledOption');
}
if (valueArray && valueArray.indexOf(option) >= 0) {
className.push('VirtualizedSelectSelectedOption');
}
if (option.className) {
className.push(option.className);
}
const events = option.disabled ? {} : {
onClick: () => selectValue(option),
onMouseEnter: () => focusOption(option),
};
return (
<div
className={className.join(' ')}
key={key}
style={Object.assign(option.style || {}, style)}
title={option.title}
{...events}
>
{renderer(option)}
</div>
);
}
WrapperRenderer.propTypes = {
focusedOption: PropTypes.object.isRequired,
focusOption: PropTypes.func.isRequired,
key: PropTypes.string,
option: PropTypes.object,
selectValue: PropTypes.func.isRequired,
style: PropTypes.object,
valueArray: PropTypes.array,
};
return WrapperRenderer;
}

View File

@@ -1,381 +0,0 @@
import React from 'react';
import { render } from 'react-dom';
import d3 from 'd3';
import { Alert } from 'react-bootstrap';
import moment from 'moment';
import GridLayout from './components/GridLayout';
import Header from './components/Header';
import { appSetup } from '../common';
import AlertsWrapper from '../components/AlertsWrapper';
import { t } from '../locales';
import '../../stylesheets/dashboard.css';
const superset = require('../modules/superset');
const urlLib = require('url');
const utils = require('../modules/utils');
let px;
appSetup();
export function getInitialState(boostrapData) {
const dashboard = Object.assign(
{},
utils.controllerInterface,
boostrapData.dashboard_data,
{ common: boostrapData.common });
dashboard.firstLoad = true;
dashboard.posDict = {};
if (dashboard.position_json) {
dashboard.position_json.forEach((position) => {
dashboard.posDict[position.slice_id] = position;
});
}
dashboard.refreshTimer = null;
const state = Object.assign({}, boostrapData, { dashboard });
return state;
}
function unload() {
const message = t('You have unsaved changes.');
window.event.returnValue = message; // Gecko + IE
return message; // Gecko + Webkit, Safari, Chrome etc.
}
function onBeforeUnload(hasChanged) {
if (hasChanged) {
window.addEventListener('beforeunload', unload);
} else {
window.removeEventListener('beforeunload', unload);
}
}
function renderAlert() {
render(
<div className="container-fluid">
<Alert bsStyle="warning">
<strong>{t('You have unsaved changes.')}</strong> {t('Click the')} &nbsp;
<i className="fa fa-save" />&nbsp;
{t('button on the top right to save your changes.')}
</Alert>
</div>,
document.getElementById('alert-container'),
);
}
function initDashboardView(dashboard) {
render(
<div>
<AlertsWrapper initMessages={dashboard.common.flash_messages} />
<Header dashboard={dashboard} />
</div>,
document.getElementById('dashboard-header'),
);
// eslint-disable-next-line no-param-reassign
dashboard.reactGridLayout = render(
<GridLayout dashboard={dashboard} />,
document.getElementById('grid-container'),
);
// Displaying widget controls on hover
$('.react-grid-item').hover(
function () {
$(this).find('.chart-controls').fadeIn(300);
},
function () {
$(this).find('.chart-controls').fadeOut(300);
},
);
$('div.grid-container').css('visibility', 'visible');
$('div.widget').click(function (e) {
const $this = $(this);
const $target = $(e.target);
if ($target.hasClass('slice_info')) {
$this.find('.slice_description').slideToggle(0, function () {
$this.find('.refresh').click();
});
} else if ($target.hasClass('controls-toggle')) {
$this.find('.chart-controls').toggle();
}
});
px.initFavStars();
$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
}
export function dashboardContainer(dashboard, datasources, userid) {
return Object.assign({}, dashboard, {
type: 'dashboard',
filters: {},
curUserId: userid,
init() {
this.sliceObjects = [];
dashboard.slices.forEach((data) => {
if (data.error) {
const html = `<div class="alert alert-danger">${data.error}</div>`;
$(`#slice_${data.slice_id}`).find('.token').html(html);
} else {
const slice = px.Slice(data, datasources[data.form_data.datasource], this);
$(`#slice_${data.slice_id}`).find('a.refresh').click(() => {
slice.render(true);
});
this.sliceObjects.push(slice);
}
});
this.loadPreSelectFilters();
this.renderSlices(this.sliceObjects);
this.firstLoad = false;
this.bindResizeToWindowResize();
},
onChange() {
onBeforeUnload(true);
renderAlert();
},
onSave() {
onBeforeUnload(false);
$('#alert-container').html('');
},
loadPreSelectFilters() {
try {
const filters = JSON.parse(px.getParam('preselect_filters') || '{}');
for (const sliceId in filters) {
for (const col in filters[sliceId]) {
this.setFilter(sliceId, col, filters[sliceId][col], false, false);
}
}
} catch (e) {
// console.error(e);
}
},
setFilter(sliceId, col, vals, refresh) {
this.addFilter(sliceId, col, vals, false, refresh);
},
done(slice) {
const refresh = slice.getWidgetHeader().find('.refresh');
const data = slice.data;
const cachedWhen = moment.utc(data.cached_dttm).fromNow();
if (data !== undefined && data.is_cached) {
refresh
.addClass('danger')
.attr(
'title',
t('Served from data cached %s . Click to force refresh.', cachedWhen))
.tooltip('fixTitle');
} else {
refresh
.removeClass('danger')
.attr('title', t('Click to force refresh'))
.tooltip('fixTitle');
}
},
effectiveExtraFilters(sliceId) {
const f = [];
const immuneSlices = this.metadata.filter_immune_slices || [];
if (sliceId && immuneSlices.includes(sliceId)) {
// The slice is immune to dashboard filters
return f;
}
// Building a list of fields the slice is immune to filters on
let immuneToFields = [];
if (
sliceId &&
this.metadata.filter_immune_slice_fields &&
this.metadata.filter_immune_slice_fields[sliceId]) {
immuneToFields = this.metadata.filter_immune_slice_fields[sliceId];
}
for (const filteringSliceId in this.filters) {
if (filteringSliceId === sliceId.toString()) {
// Filters applied by the slice don't apply to itself
continue;
}
for (const field in this.filters[filteringSliceId]) {
if (!immuneToFields.includes(field)) {
f.push({
col: field,
op: 'in',
val: this.filters[filteringSliceId][field],
});
}
}
}
return f;
},
addFilter(sliceId, col, vals, merge = true, refresh = true) {
if (
this.getSlice(sliceId) && (
['__from', '__to', '__time_col', '__time_grain', '__time_origin', '__granularity']
.indexOf(col) >= 0 ||
this.getSlice(sliceId).formData.groupby.indexOf(col) !== -1
)
) {
if (!(sliceId in this.filters)) {
this.filters[sliceId] = {};
}
if (!(col in this.filters[sliceId]) || !merge) {
this.filters[sliceId][col] = vals;
// d3.merge pass in array of arrays while some value form filter components
// from and to filter box require string to be process and return
} else if (this.filters[sliceId][col] instanceof Array) {
this.filters[sliceId][col] = d3.merge([this.filters[sliceId][col], vals]);
} else {
this.filters[sliceId][col] = d3.merge([[this.filters[sliceId][col]], vals])[0] || '';
}
if (refresh) {
this.refreshExcept(sliceId);
}
}
this.updateFilterParamsInUrl();
},
readFilters() {
// Returns a list of human readable active filters
return JSON.stringify(this.filters, null, ' ');
},
updateFilterParamsInUrl() {
const urlObj = urlLib.parse(location.href, true);
urlObj.query = urlObj.query || {};
urlObj.query.preselect_filters = this.readFilters();
urlObj.search = null;
history.pushState(urlObj.query, window.title, urlLib.format(urlObj));
},
bindResizeToWindowResize() {
let resizeTimer;
const dash = this;
$(window).on('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
dash.sliceObjects.forEach((slice) => {
slice.resize();
});
}, 500);
});
},
stopPeriodicRender() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
},
renderSlices(slices, force = false, interval = 0) {
if (!interval) {
slices.forEach(slice => slice.render(force));
return;
}
const meta = this.metadata;
const refreshTime = Math.max(interval, meta.stagger_time || 5000); // default 5 seconds
if (typeof meta.stagger_refresh !== 'boolean') {
meta.stagger_refresh = meta.stagger_refresh === undefined ?
true : meta.stagger_refresh === 'true';
}
const delay = meta.stagger_refresh ? refreshTime / (slices.length - 1) : 0;
slices.forEach((slice, i) => {
setTimeout(() => slice.render(force), delay * i);
});
},
startPeriodicRender(interval) {
this.stopPeriodicRender();
const dash = this;
const immune = this.metadata.timed_refresh_immune_slices || [];
const refreshAll = () => {
const slices = dash.sliceObjects
.filter(slice => immune.indexOf(slice.data.slice_id) === -1);
dash.renderSlices(slices, true, interval * 0.2);
};
const fetchAndRender = function () {
refreshAll();
if (interval > 0) {
dash.refreshTimer = setTimeout(function () {
fetchAndRender();
}, interval);
}
};
fetchAndRender();
},
refreshExcept(sliceId) {
const immune = this.metadata.filter_immune_slices || [];
const slices = this.sliceObjects.filter(slice =>
slice.data.slice_id !== sliceId && immune.indexOf(slice.data.slice_id) === -1);
this.renderSlices(slices);
},
clearFilters(sliceId) {
delete this.filters[sliceId];
this.refreshExcept(sliceId);
this.updateFilterParamsInUrl();
},
removeFilter(sliceId, col, vals) {
if (sliceId in this.filters) {
if (col in this.filters[sliceId]) {
const a = [];
this.filters[sliceId][col].forEach(function (v) {
if (vals.indexOf(v) < 0) {
a.push(v);
}
});
this.filters[sliceId][col] = a;
}
}
this.refreshExcept(sliceId);
this.updateFilterParamsInUrl();
},
getSlice(sliceId) {
const id = parseInt(sliceId, 10);
let i = 0;
let slice = null;
while (i < this.sliceObjects.length) {
// when the slice is found, assign to slice and break;
if (this.sliceObjects[i].data.slice_id === id) {
slice = this.sliceObjects[i];
break;
}
i++;
}
return slice;
},
getAjaxErrorMsg(error) {
const respJSON = error.responseJSON;
return (respJSON && respJSON.message) ? respJSON.message :
error.responseText;
},
addSlicesToDashboard(sliceIds) {
const getAjaxErrorMsg = this.getAjaxErrorMsg;
$.ajax({
type: 'POST',
url: `/superset/add_slices/${dashboard.id}/`,
data: {
data: JSON.stringify({ slice_ids: sliceIds }),
},
success() {
// Refresh page to allow for slices to re-render
window.location.reload();
},
error(error) {
const errorMsg = getAjaxErrorMsg(error);
utils.showModal({
title: t('Error'),
body: t('Sorry, there was an error adding slices to this dashboard: %s', errorMsg),
});
},
});
},
updateDashboardTitle(title) {
this.dashboard_title = title;
this.onChange();
},
});
}
$(document).ready(() => {
// Getting bootstrapped data from the DOM
utils.initJQueryAjax();
const dashboardData = $('.dashboard').data('bootstrap');
const state = getInitialState(dashboardData);
px = superset(state);
const dashboard = dashboardContainer(state.dashboard, state.datasources, state.user_id);
initDashboardView(dashboard);
dashboard.init();
});

View File

@@ -0,0 +1,117 @@
/* global notify */
import $ from 'jquery';
import { getExploreUrl } from '../explore/exploreUtils';
export const ADD_FILTER = 'ADD_FILTER';
export function addFilter(sliceId, col, vals, merge = true, refresh = true) {
return { type: ADD_FILTER, sliceId, col, vals, merge, refresh };
}
export const CLEAR_FILTER = 'CLEAR_FILTER';
export function clearFilter(sliceId) {
return { type: CLEAR_FILTER, sliceId };
}
export const REMOVE_FILTER = 'REMOVE_FILTER';
export function removeFilter(sliceId, col, vals, refresh = true) {
return { type: REMOVE_FILTER, sliceId, col, vals, refresh };
}
export const UPDATE_DASHBOARD_LAYOUT = 'UPDATE_DASHBOARD_LAYOUT';
export function updateDashboardLayout(layout) {
return { type: UPDATE_DASHBOARD_LAYOUT, layout };
}
export const UPDATE_DASHBOARD_TITLE = 'UPDATE_DASHBOARD_TITLE';
export function updateDashboardTitle(title) {
return { type: UPDATE_DASHBOARD_TITLE, title };
}
export function addSlicesToDashboard(dashboardId, sliceIds) {
return () => (
$.ajax({
type: 'POST',
url: `/superset/add_slices/${dashboardId}/`,
data: {
data: JSON.stringify({ slice_ids: sliceIds }),
},
})
.done(() => {
// Refresh page to allow for slices to re-render
window.location.reload();
})
);
}
export const REMOVE_SLICE = 'REMOVE_SLICE';
export function removeSlice(slice) {
return { type: REMOVE_SLICE, slice };
}
export const UPDATE_SLICE_NAME = 'UPDATE_SLICE_NAME';
export function updateSliceName(slice, sliceName) {
return { type: UPDATE_SLICE_NAME, slice, sliceName };
}
export function saveSlice(slice, sliceName) {
const oldName = slice.slice_name;
return (dispatch) => {
const sliceParams = {};
sliceParams.slice_id = slice.slice_id;
sliceParams.action = 'overwrite';
sliceParams.slice_name = sliceName;
const saveUrl = getExploreUrl(slice.form_data, 'base', false, null, sliceParams);
return $.ajax({
url: saveUrl,
type: 'GET',
success: () => {
dispatch(updateSliceName(slice, sliceName));
notify.success('This slice name was saved successfully.');
},
error: () => {
// if server-side reject the overwrite action,
// revert to old state
dispatch(updateSliceName(slice, oldName));
notify.error("You don't have the rights to alter this slice");
},
});
};
}
const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR';
export function toggleFaveStar(isStarred) {
return { type: TOGGLE_FAVE_STAR, isStarred };
}
export const FETCH_FAVE_STAR = 'FETCH_FAVE_STAR';
export function fetchFaveStar(id) {
return function (dispatch) {
const url = `${FAVESTAR_BASE_URL}/${id}/count`;
return $.get(url)
.done((data) => {
if (data.count > 0) {
dispatch(toggleFaveStar(true));
}
});
};
}
export const SAVE_FAVE_STAR = 'SAVE_FAVE_STAR';
export function saveFaveStar(id, isStarred) {
return function (dispatch) {
const urlSuffix = isStarred ? 'unselect' : 'select';
const url = `${FAVESTAR_BASE_URL}/${id}/${urlSuffix}/`;
$.get(url);
dispatch(toggleFaveStar(!isStarred));
};
}
export const TOGGLE_EXPAND_SLICE = 'TOGGLE_EXPAND_SLICE';
export function toggleExpandSlice(slice, isExpanded) {
return { type: TOGGLE_EXPAND_SLICE, slice, isExpanded };
}
export const SET_EDIT_MODE = 'SET_EDIT_MODE';
export function setEditMode(editMode) {
return { type: SET_EDIT_MODE, editMode };
}

View File

@@ -21,7 +21,7 @@ export default class CodeModal extends React.PureComponent {
}
beforeOpen() {
let code = this.props.code;
if (this.props.codeCallback) {
if (!code && this.props.codeCallback) {
code = this.props.codeCallback();
}
this.setState({ code });

View File

@@ -1,19 +1,59 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ButtonGroup } from 'react-bootstrap';
import { DropdownButton, MenuItem } from 'react-bootstrap';
import Button from '../../components/Button';
import CssEditor from './CssEditor';
import RefreshIntervalModal from './RefreshIntervalModal';
import SaveModal from './SaveModal';
import CodeModal from './CodeModal';
import SliceAdder from './SliceAdder';
import { t } from '../../locales';
import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
const $ = window.$ = require('jquery');
const propTypes = {
dashboard: PropTypes.object.isRequired,
filters: PropTypes.object.isRequired,
slices: PropTypes.array,
userId: PropTypes.string.isRequired,
addSlicesToDashboard: PropTypes.func,
onSave: PropTypes.func,
onChange: PropTypes.func,
renderSlices: PropTypes.func,
serialize: PropTypes.func,
startPeriodicRender: PropTypes.func,
editMode: PropTypes.bool,
};
function MenuItemContent({ faIcon, text, tooltip, children }) {
return (
<span>
<i className={`fa fa-${faIcon}`} /> {text} {''}
<InfoTooltipWithTrigger
tooltip={tooltip}
label={`dash-${faIcon}`}
placement="top"
/>
{children}
</span>
);
}
MenuItemContent.propTypes = {
faIcon: PropTypes.string.isRequired,
text: PropTypes.string,
tooltip: PropTypes.string,
children: PropTypes.node,
};
function ActionMenuItem(props) {
return (
<MenuItem onClick={props.onClick}>
<MenuItemContent {...props} />
</MenuItem>
);
}
ActionMenuItem.propTypes = {
onClick: PropTypes.func,
};
class Controls extends React.PureComponent {
@@ -23,8 +63,13 @@ class Controls extends React.PureComponent {
css: props.dashboard.css || '',
cssTemplates: [],
};
this.refresh = this.refresh.bind(this);
this.toggleModal = this.toggleModal.bind(this);
this.updateDom = this.updateDom.bind(this);
}
componentWillMount() {
this.updateDom(this.state.css);
$.get('/csstemplateasyncmodelview/api/read', (data) => {
const cssTemplates = data.result.map(row => ({
value: row.template_name,
@@ -36,74 +81,129 @@ class Controls extends React.PureComponent {
}
refresh() {
// Force refresh all slices
this.props.dashboard.renderSlices(this.props.dashboard.sliceObjects, true);
this.props.renderSlices(true);
}
toggleModal(modal) {
let currentModal;
if (modal !== this.state.currentModal) {
currentModal = modal;
}
this.setState({ currentModal });
}
changeCss(css) {
this.setState({ css });
this.props.dashboard.onChange();
this.setState({ css }, () => {
this.updateDom(css);
});
this.props.onChange();
}
updateDom(css) {
const className = 'CssEditor-css';
const head = document.head || document.getElementsByTagName('head')[0];
let style = document.querySelector('.' + className);
if (!style) {
style = document.createElement('style');
style.className = className;
style.type = 'text/css';
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.innerHTML = css;
}
}
render() {
const dashboard = this.props.dashboard;
const { dashboard, userId, filters,
addSlicesToDashboard, startPeriodicRender,
serialize, onSave, editMode } = this.props;
const emailBody = t('Checkout this dashboard: %s', window.location.href);
const emailLink = 'mailto:?Subject=Superset%20Dashboard%20'
+ `${dashboard.dashboard_title}&Body=${emailBody}`;
let saveText = t('Save as');
if (editMode) {
saveText = t('Save');
}
return (
<ButtonGroup>
<Button
tooltip={t('Force refresh the whole dashboard')}
onClick={this.refresh.bind(this)}
>
<i className="fa fa-refresh" />
</Button>
<SliceAdder
dashboard={dashboard}
triggerNode={
<i className="fa fa-plus" />
<span>
<DropdownButton title="Actions" bsSize="small" id="bg-nested-dropdown" pullRight>
<ActionMenuItem
text={t('Force Refresh')}
tooltip={t('Force refresh the whole dashboard')}
faIcon="refresh"
onClick={this.refresh}
/>
<RefreshIntervalModal
onChange={refreshInterval => startPeriodicRender(refreshInterval * 1000)}
triggerNode={
<MenuItemContent
text={t('Set autorefresh')}
tooltip={t('Set the auto-refresh interval for this session')}
faIcon="clock-o"
/>
}
/>
<SaveModal
dashboard={dashboard}
filters={filters}
serialize={serialize}
onSave={onSave}
css={this.state.css}
triggerNode={
<MenuItemContent
text={saveText}
tooltip={t('Save the dashboard')}
faIcon="save"
/>
}
/>
{editMode &&
<ActionMenuItem
text={t('Edit properties')}
tooltip={t("Edit the dashboards's properties")}
faIcon="edit"
onClick={() => { window.location = `/dashboardmodelview/edit/${dashboard.id}`; }}
/>
}
/>
<RefreshIntervalModal
onChange={refreshInterval => dashboard.startPeriodicRender(refreshInterval * 1000)}
triggerNode={
<i className="fa fa-clock-o" />
{editMode &&
<ActionMenuItem
text={t('Email')}
tooltip={t('Email a link to this dashbaord')}
onClick={() => { window.location = emailLink; }}
faIcon="envelope"
/>
}
/>
<CodeModal
codeCallback={dashboard.readFilters.bind(dashboard)}
triggerNode={<i className="fa fa-filter" />}
/>
<CssEditor
dashboard={dashboard}
triggerNode={
<i className="fa fa-css3" />
{editMode &&
<SliceAdder
dashboard={dashboard}
addSlicesToDashboard={addSlicesToDashboard}
userId={userId}
triggerNode={
<MenuItemContent
text={t('Add Slices')}
tooltip={t('Add some slices to this dashbaord')}
faIcon="plus"
/>
}
/>
}
initialCss={dashboard.css}
templates={this.state.cssTemplates}
onChange={this.changeCss.bind(this)}
/>
<Button
onClick={() => { window.location = emailLink; }}
>
<i className="fa fa-envelope" />
</Button>
<Button
disabled={!dashboard.dash_edit_perm}
onClick={() => {
window.location = `/dashboardmodelview/edit/${dashboard.id}`;
}}
tooltip={t('Edit this dashboard\'s properties')}
>
<i className="fa fa-edit" />
</Button>
<SaveModal
dashboard={dashboard}
css={this.state.css}
triggerNode={
<Button disabled={!dashboard.dash_save_perm}>
<i className="fa fa-save" />
</Button>
{editMode &&
<CssEditor
dashboard={dashboard}
triggerNode={
<MenuItemContent
text={t('Edit CSS')}
tooltip={t('Change the style of the dashboard using CSS code')}
faIcon="css3"
/>
}
initialCss={this.state.css}
templates={this.state.cssTemplates}
onChange={this.changeCss.bind(this)}
/>
}
/>
</ButtonGroup>
</DropdownButton>
</span>
);
}
}

View File

@@ -30,30 +30,10 @@ class CssEditor extends React.PureComponent {
cssTemplateOptions: [],
};
}
componentWillMount() {
this.updateDom();
}
changeCss(css) {
this.setState({ css }, this.updateDom);
this.props.onChange(css);
}
updateDom() {
const css = this.state.css;
const className = 'CssEditor-css';
const head = document.head || document.getElementsByTagName('head')[0];
let style = document.querySelector('.' + className);
if (!style) {
style = document.createElement('style');
style.className = className;
style.type = 'text/css';
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.innerHTML = css;
}
this.setState({ css }, () => {
this.props.onChange(css);
});
}
changeCssTemplate(opt) {
this.changeCss(opt.css);
@@ -78,7 +58,7 @@ class CssEditor extends React.PureComponent {
<ModalTrigger
triggerNode={this.props.triggerNode}
modalTitle={t('CSS')}
isButton
isMenuItem
modalBody={
<div>
{this.renderTemplateSelector()}

View File

@@ -0,0 +1,340 @@
import React from 'react';
import PropTypes from 'prop-types';
import AlertsWrapper from '../../components/AlertsWrapper';
import GridLayout from './GridLayout';
import Header from './Header';
import { areObjectsEqual } from '../../reduxUtils';
import { Logger, ActionLog, LOG_ACTIONS_PAGE_LOAD,
LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT } from '../../logger';
import { t } from '../../locales';
import '../../../stylesheets/dashboard.css';
const propTypes = {
actions: PropTypes.object,
initMessages: PropTypes.array,
dashboard: PropTypes.object.isRequired,
slices: PropTypes.object,
datasources: PropTypes.object,
filters: PropTypes.object,
refresh: PropTypes.bool,
timeout: PropTypes.number,
userId: PropTypes.string,
isStarred: PropTypes.bool,
editMode: PropTypes.bool,
impressionId: PropTypes.string,
};
const defaultProps = {
initMessages: [],
dashboard: {},
slices: {},
datasources: {},
filters: {},
refresh: false,
timeout: 60,
userId: '',
isStarred: false,
editMode: false,
};
class Dashboard extends React.PureComponent {
constructor(props) {
super(props);
this.refreshTimer = null;
this.firstLoad = true;
this.loadingLog = new ActionLog({
impressionId: props.impressionId,
actionType: LOG_ACTIONS_PAGE_LOAD,
source: 'dashboard',
sourceId: props.dashboard.id,
eventNames: [LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT],
});
Logger.start(this.loadingLog);
// alert for unsaved changes
this.state = { unsavedChanges: false };
this.rerenderCharts = this.rerenderCharts.bind(this);
this.updateDashboardTitle = this.updateDashboardTitle.bind(this);
this.onSave = this.onSave.bind(this);
this.onChange = this.onChange.bind(this);
this.serialize = this.serialize.bind(this);
this.fetchAllSlices = this.fetchSlices.bind(this, this.getAllSlices());
this.startPeriodicRender = this.startPeriodicRender.bind(this);
this.addSlicesToDashboard = this.addSlicesToDashboard.bind(this);
this.fetchSlice = this.fetchSlice.bind(this);
this.getFormDataExtra = this.getFormDataExtra.bind(this);
this.props.actions.fetchFaveStar = this.props.actions.fetchFaveStar.bind(this);
this.props.actions.saveFaveStar = this.props.actions.saveFaveStar.bind(this);
this.props.actions.saveSlice = this.props.actions.saveSlice.bind(this);
this.props.actions.removeSlice = this.props.actions.removeSlice.bind(this);
this.props.actions.removeChart = this.props.actions.removeChart.bind(this);
this.props.actions.updateDashboardLayout = this.props.actions.updateDashboardLayout.bind(this);
this.props.actions.toggleExpandSlice = this.props.actions.toggleExpandSlice.bind(this);
this.props.actions.addFilter = this.props.actions.addFilter.bind(this);
this.props.actions.clearFilter = this.props.actions.clearFilter.bind(this);
this.props.actions.removeFilter = this.props.actions.removeFilter.bind(this);
}
componentDidMount() {
window.addEventListener('resize', this.rerenderCharts);
}
componentWillReceiveProps(nextProps) {
if (this.firstLoad &&
Object.values(nextProps.slices)
.every(slice => (['rendered', 'failed', 'stopped'].indexOf(slice.chartStatus) > -1))
) {
Logger.end(this.loadingLog);
this.firstLoad = false;
}
}
componentDidUpdate(prevProps) {
if (this.props.refresh) {
let changedFilterKey;
const prevFiltersKeySet = new Set(Object.keys(prevProps.filters));
Object.keys(this.props.filters).some((key) => {
prevFiltersKeySet.delete(key);
if (prevProps.filters[key] === undefined ||
!areObjectsEqual(prevProps.filters[key], this.props.filters[key])) {
changedFilterKey = key;
return true;
}
return false;
});
// has changed filter or removed a filter?
if (!!changedFilterKey || prevFiltersKeySet.size) {
this.refreshExcept(changedFilterKey);
}
}
}
componentWillUnmount() {
window.removeEventListener('resize', this.rerenderCharts);
}
onBeforeUnload(hasChanged) {
if (hasChanged) {
window.addEventListener('beforeunload', this.unload);
} else {
window.removeEventListener('beforeunload', this.unload);
}
}
onChange() {
this.onBeforeUnload(true);
this.setState({ unsavedChanges: true });
}
onSave() {
this.onBeforeUnload(false);
this.setState({ unsavedChanges: false });
}
// return charts in array
getAllSlices() {
return Object.values(this.props.slices);
}
getFormDataExtra(slice) {
const formDataExtra = Object.assign({}, slice.formData);
const extraFilters = this.effectiveExtraFilters(slice.slice_id);
formDataExtra.extra_filters = formDataExtra.filters.concat(extraFilters);
return formDataExtra;
}
getFilters(sliceId) {
return this.props.filters[sliceId];
}
unload() {
const message = t('You have unsaved changes.');
window.event.returnValue = message; // Gecko + IE
return message; // Gecko + Webkit, Safari, Chrome etc.
}
effectiveExtraFilters(sliceId) {
const metadata = this.props.dashboard.metadata;
const filters = this.props.filters;
const f = [];
const immuneSlices = metadata.filter_immune_slices || [];
if (sliceId && immuneSlices.includes(sliceId)) {
// The slice is immune to dashboard filters
return f;
}
// Building a list of fields the slice is immune to filters on
let immuneToFields = [];
if (
sliceId &&
metadata.filter_immune_slice_fields &&
metadata.filter_immune_slice_fields[sliceId]) {
immuneToFields = metadata.filter_immune_slice_fields[sliceId];
}
for (const filteringSliceId in filters) {
if (filteringSliceId === sliceId.toString()) {
// Filters applied by the slice don't apply to itself
continue;
}
for (const field in filters[filteringSliceId]) {
if (!immuneToFields.includes(field)) {
f.push({
col: field,
op: 'in',
val: filters[filteringSliceId][field],
});
}
}
}
return f;
}
refreshExcept(filterKey) {
const immune = this.props.dashboard.metadata.filter_immune_slices || [];
let slices = this.getAllSlices();
if (filterKey) {
slices = slices.filter(slice => (
String(slice.slice_id) !== filterKey &&
immune.indexOf(slice.slice_id) === -1
));
}
this.fetchSlices(slices);
}
stopPeriodicRender() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
}
startPeriodicRender(interval) {
this.stopPeriodicRender();
const immune = this.props.dashboard.metadata.timed_refresh_immune_slices || [];
const refreshAll = () => {
const affectedSlices = this.getAllSlices()
.filter(slice => immune.indexOf(slice.slice_id) === -1);
this.fetchSlices(affectedSlices, true, interval * 0.2);
};
const fetchAndRender = () => {
refreshAll();
if (interval > 0) {
this.refreshTimer = setTimeout(fetchAndRender, interval);
}
};
fetchAndRender();
}
updateDashboardTitle(title) {
this.props.actions.updateDashboardTitle(title);
this.onChange();
}
serialize() {
return this.props.dashboard.layout.map(reactPos => ({
slice_id: reactPos.i,
col: reactPos.x + 1,
row: reactPos.y,
size_x: reactPos.w,
size_y: reactPos.h,
}));
}
addSlicesToDashboard(sliceIds) {
return this.props.actions.addSlicesToDashboard(this.props.dashboard.id, sliceIds);
}
fetchSlice(slice, force = false) {
return this.props.actions.runQuery(
this.getFormDataExtra(slice), force, this.props.timeout, slice.chartKey,
);
}
// fetch and render an list of slices
fetchSlices(slc, force = false, interval = 0) {
const slices = slc || this.getAllSlices();
if (!interval) {
slices.forEach((slice) => { this.fetchSlice(slice, force); });
return;
}
const meta = this.props.dashboard.metadata;
const refreshTime = Math.max(interval, meta.stagger_time || 5000); // default 5 seconds
if (typeof meta.stagger_refresh !== 'boolean') {
meta.stagger_refresh = meta.stagger_refresh === undefined ?
true : meta.stagger_refresh === 'true';
}
const delay = meta.stagger_refresh ? refreshTime / (slices.length - 1) : 0;
slices.forEach((slice, i) => {
setTimeout(() => { this.fetchSlice(slice, force); }, delay * i);
});
}
// re-render chart without fetch
rerenderCharts() {
this.getAllSlices().forEach((slice) => {
setTimeout(() => {
this.props.actions.renderTriggered(new Date().getTime(), slice.chartKey);
}, 50);
});
}
render() {
return (
<div id="dashboard-container">
<div id="dashboard-header">
<AlertsWrapper initMessages={this.props.initMessages} />
<Header
dashboard={this.props.dashboard}
unsavedChanges={this.state.unsavedChanges}
filters={this.props.filters}
userId={this.props.userId}
isStarred={this.props.isStarred}
updateDashboardTitle={this.updateDashboardTitle}
onSave={this.onSave}
onChange={this.onChange}
serialize={this.serialize}
fetchFaveStar={this.props.actions.fetchFaveStar}
saveFaveStar={this.props.actions.saveFaveStar}
renderSlices={this.fetchAllSlices}
startPeriodicRender={this.startPeriodicRender}
addSlicesToDashboard={this.addSlicesToDashboard}
editMode={this.props.editMode}
setEditMode={this.props.actions.setEditMode}
/>
</div>
<div id="grid-container" className="slice-grid gridster">
<GridLayout
dashboard={this.props.dashboard}
datasources={this.props.datasources}
filters={this.props.filters}
charts={this.props.slices}
timeout={this.props.timeout}
onChange={this.onChange}
getFormDataExtra={this.getFormDataExtra}
fetchSlice={this.fetchSlice}
saveSlice={this.props.actions.saveSlice}
removeSlice={this.props.actions.removeSlice}
removeChart={this.props.actions.removeChart}
updateDashboardLayout={this.props.actions.updateDashboardLayout}
toggleExpandSlice={this.props.actions.toggleExpandSlice}
addFilter={this.props.actions.addFilter}
getFilters={this.getFilters}
clearFilter={this.props.actions.clearFilter}
removeFilter={this.props.actions.removeFilter}
editMode={this.props.editMode}
/>
</div>
</div>
);
}
}
Dashboard.propTypes = propTypes;
Dashboard.defaultProps = defaultProps;
export default Dashboard;

View File

@@ -0,0 +1,31 @@
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as dashboardActions from '../actions';
import * as chartActions from '../../chart/chartAction';
import Dashboard from './Dashboard';
function mapStateToProps({ charts, dashboard, impressionId }) {
return {
initMessages: dashboard.common.flash_messages,
timeout: dashboard.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
dashboard: dashboard.dashboard,
slices: charts,
datasources: dashboard.datasources,
filters: dashboard.filters,
refresh: !!dashboard.refresh,
userId: dashboard.userId,
isStarred: !!dashboard.isStarred,
editMode: dashboard.editMode,
impressionId,
};
}
function mapDispatchToProps(dispatch) {
const actions = { ...chartActions, ...dashboardActions };
return {
actions: bindActionCreators(actions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);

View File

@@ -0,0 +1,142 @@
/* eslint-disable react/no-danger */
import React from 'react';
import PropTypes from 'prop-types';
import SliceHeader from './SliceHeader';
import ChartContainer from '../../chart/ChartContainer';
import '../../../stylesheets/dashboard.css';
const propTypes = {
timeout: PropTypes.number,
datasource: PropTypes.object,
isLoading: PropTypes.bool,
isCached: PropTypes.bool,
cachedDttm: PropTypes.string,
isExpanded: PropTypes.bool,
widgetHeight: PropTypes.number,
widgetWidth: PropTypes.number,
exploreChartUrl: PropTypes.string,
exportCSVUrl: PropTypes.string,
slice: PropTypes.object,
chartKey: PropTypes.string,
formData: PropTypes.object,
filters: PropTypes.object,
forceRefresh: PropTypes.func,
removeSlice: PropTypes.func,
updateSliceName: PropTypes.func,
toggleExpandSlice: PropTypes.func,
addFilter: PropTypes.func,
getFilters: PropTypes.func,
clearFilter: PropTypes.func,
removeFilter: PropTypes.func,
editMode: PropTypes.bool,
annotationQuery: PropTypes.object,
};
const defaultProps = {
forceRefresh: () => ({}),
removeSlice: () => ({}),
updateSliceName: () => ({}),
toggleExpandSlice: () => ({}),
addFilter: () => ({}),
getFilters: () => ({}),
clearFilter: () => ({}),
removeFilter: () => ({}),
editMode: false,
};
class GridCell extends React.PureComponent {
constructor(props) {
super(props);
const sliceId = this.props.slice.slice_id;
this.addFilter = this.props.addFilter.bind(this, sliceId);
this.getFilters = this.props.getFilters.bind(this, sliceId);
this.clearFilter = this.props.clearFilter.bind(this, sliceId);
this.removeFilter = this.props.removeFilter.bind(this, sliceId);
}
getDescriptionId(slice) {
return 'description_' + slice.slice_id;
}
getHeaderId(slice) {
return 'header_' + slice.slice_id;
}
width() {
return this.props.widgetWidth - 10;
}
height(slice) {
const widgetHeight = this.props.widgetHeight;
const headerId = this.getHeaderId(slice);
const descriptionId = this.getDescriptionId(slice);
const headerHeight = this.refs[headerId] ? this.refs[headerId].offsetHeight : 30;
let descriptionHeight = 0;
if (this.props.isExpanded && this.refs[descriptionId]) {
descriptionHeight = this.refs[descriptionId].offsetHeight + 10;
}
return widgetHeight - headerHeight - descriptionHeight;
}
render() {
const {
exploreChartUrl, exportCSVUrl, isExpanded, isLoading, isCached, cachedDttm,
removeSlice, updateSliceName, toggleExpandSlice, forceRefresh,
chartKey, slice, datasource, formData, timeout, annotationQuery,
} = this.props;
return (
<div
className={isLoading ? 'slice-cell-highlight' : 'slice-cell'}
id={`${slice.slice_id}-cell`}
>
<div ref={this.getHeaderId(slice)}>
<SliceHeader
slice={slice}
exploreChartUrl={exploreChartUrl}
exportCSVUrl={exportCSVUrl}
isExpanded={isExpanded}
isCached={isCached}
cachedDttm={cachedDttm}
removeSlice={removeSlice}
updateSliceName={updateSliceName}
toggleExpandSlice={toggleExpandSlice}
forceRefresh={forceRefresh}
editMode={this.props.editMode}
annotationQuery={annotationQuery}
/>
</div>
<div
className="slice_description bs-callout bs-callout-default"
style={isExpanded ? {} : { display: 'none' }}
ref={this.getDescriptionId(slice)}
dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
/>
<div className="row chart-container">
<input type="hidden" value="false" />
<ChartContainer
containerId={`slice-container-${slice.slice_id}`}
chartKey={chartKey}
datasource={datasource}
formData={formData}
height={this.height(slice)}
width={this.width()}
timeout={timeout}
vizType={slice.formData.viz_type}
addFilter={this.addFilter}
getFilters={this.getFilters}
clearFilter={this.clearFilter}
removeFilter={this.removeFilter}
/>
</div>
</div>
);
}
}
GridCell.propTypes = propTypes;
GridCell.defaultProps = defaultProps;
export default GridCell;

View File

@@ -1,10 +1,8 @@
/* global notify */
import React from 'react';
import PropTypes from 'prop-types';
import { Responsive, WidthProvider } from 'react-grid-layout';
import $ from 'jquery';
import SliceCell from './SliceCell';
import GridCell from './GridCell';
import { getExploreUrl } from '../../explore/exploreUtils';
require('react-grid-layout/css/styles.css');
@@ -14,119 +12,170 @@ const ResponsiveReactGridLayout = WidthProvider(Responsive);
const propTypes = {
dashboard: PropTypes.object.isRequired,
datasources: PropTypes.object,
charts: PropTypes.object.isRequired,
filters: PropTypes.object,
timeout: PropTypes.number,
onChange: PropTypes.func,
getFormDataExtra: PropTypes.func,
fetchSlice: PropTypes.func,
saveSlice: PropTypes.func,
removeSlice: PropTypes.func,
removeChart: PropTypes.func,
updateDashboardLayout: PropTypes.func,
toggleExpandSlice: PropTypes.func,
addFilter: PropTypes.func,
getFilters: PropTypes.func,
clearFilter: PropTypes.func,
removeFilter: PropTypes.func,
editMode: PropTypes.bool.isRequired,
};
const defaultProps = {
onChange: () => ({}),
getFormDataExtra: () => ({}),
fetchSlice: () => ({}),
saveSlice: () => ({}),
removeSlice: () => ({}),
removeChart: () => ({}),
updateDashboardLayout: () => ({}),
toggleExpandSlice: () => ({}),
addFilter: () => ({}),
getFilters: () => ({}),
clearFilter: () => ({}),
removeFilter: () => ({}),
};
class GridLayout extends React.Component {
componentWillMount() {
const layout = [];
constructor(props) {
super(props);
this.props.dashboard.slices.forEach((slice, index) => {
const sliceId = slice.slice_id;
let pos = this.props.dashboard.posDict[sliceId];
if (!pos) {
pos = {
col: (index * 4 + 1) % 12,
row: Math.floor((index) / 3) * 4,
size_x: 4,
size_y: 4,
};
}
layout.push({
i: String(sliceId),
x: pos.col - 1,
y: pos.row,
w: pos.size_x,
minW: 2,
h: pos.size_y,
});
});
this.setState({
layout,
slices: this.props.dashboard.slices,
});
this.onResizeStop = this.onResizeStop.bind(this);
this.onDragStop = this.onDragStop.bind(this);
this.forceRefresh = this.forceRefresh.bind(this);
this.removeSlice = this.removeSlice.bind(this);
this.updateSliceName = this.props.dashboard.dash_edit_perm ?
this.updateSliceName.bind(this) : null;
}
onResizeStop(layout, oldItem, newItem) {
const newSlice = this.props.dashboard.getSlice(newItem.i);
if (oldItem.w !== newItem.w || oldItem.h !== newItem.h) {
this.setState({ layout }, () => newSlice.resize());
}
this.props.dashboard.onChange();
onResizeStop(layout) {
this.props.updateDashboardLayout(layout);
this.props.onChange();
}
onDragStop(layout) {
this.setState({ layout });
this.props.dashboard.onChange();
this.props.updateDashboardLayout(layout);
this.props.onChange();
}
removeSlice(sliceId) {
$('[data-toggle=tooltip]').tooltip('hide');
this.setState({
layout: this.state.layout.filter(function (reactPos) {
return reactPos.i !== String(sliceId);
}),
slices: this.state.slices.filter(function (slice) {
return slice.slice_id !== sliceId;
}),
});
this.props.dashboard.onChange();
getWidgetId(slice) {
return 'widget_' + slice.slice_id;
}
getWidgetHeight(slice) {
const widgetId = this.getWidgetId(slice);
if (!widgetId || !this.refs[widgetId]) {
return 400;
}
return this.refs[widgetId].offsetHeight;
}
getWidgetWidth(slice) {
const widgetId = this.getWidgetId(slice);
if (!widgetId || !this.refs[widgetId]) {
return 400;
}
return this.refs[widgetId].offsetWidth;
}
findSliceIndexById(sliceId) {
return this.props.dashboard.slices
.map(slice => (slice.slice_id)).indexOf(sliceId);
}
forceRefresh(sliceId) {
return this.props.fetchSlice(this.props.charts['slice_' + sliceId], true);
}
removeSlice(slice) {
if (!slice) {
return;
}
// remove slice dashbaord and charts
this.props.removeSlice(slice);
this.props.removeChart(this.props.charts['slice_' + slice.slice_id].chartKey);
this.props.onChange();
}
updateSliceName(sliceId, sliceName) {
const index = this.state.slices.map(slice => (slice.slice_id)).indexOf(sliceId);
const index = this.findSliceIndexById(sliceId);
if (index === -1) {
return;
}
// update slice_name first
const oldSlices = this.state.slices;
const currentSlice = this.state.slices[index];
const updated = Object.assign({},
this.state.slices[index], { slice_name: sliceName });
const updatedSlices = this.state.slices.slice();
updatedSlices[index] = updated;
this.setState({ slices: updatedSlices });
const currentSlice = this.props.dashboard.slices[index];
if (currentSlice.slice_name === sliceName) {
return;
}
const sliceParams = {};
sliceParams.slice_id = currentSlice.slice_id;
sliceParams.action = 'overwrite';
sliceParams.slice_name = sliceName;
const saveUrl = getExploreUrl(currentSlice.form_data, 'base', false, null, sliceParams);
$.ajax({
url: saveUrl,
type: 'GET',
success: () => {
notify.success('This slice name was saved successfully.');
},
error: () => {
// if server-side reject the overwrite action,
// revert to old state
this.setState({ slices: oldSlices });
notify.error('You don\'t have the rights to alter this slice');
},
});
this.props.saveSlice(currentSlice, sliceName);
}
serialize() {
return this.state.layout.map(reactPos => ({
slice_id: reactPos.i,
col: reactPos.x + 1,
row: reactPos.y,
size_x: reactPos.w,
size_y: reactPos.h,
}));
isExpanded(slice) {
return this.props.dashboard.metadata.expanded_slices &&
this.props.dashboard.metadata.expanded_slices[slice.slice_id];
}
render() {
const cells = this.props.dashboard.slices.map((slice) => {
const chartKey = `slice_${slice.slice_id}`;
const currentChart = this.props.charts[chartKey];
const queryResponse = currentChart.queryResponse || {};
return (
<div
id={'slice_' + slice.slice_id}
key={slice.slice_id}
data-slice-id={slice.slice_id}
className={`widget ${slice.form_data.viz_type}`}
ref={this.getWidgetId(slice)}
>
<GridCell
slice={slice}
chartKey={chartKey}
datasource={this.props.datasources[slice.form_data.datasource]}
filters={this.props.filters}
formData={this.props.getFormDataExtra(slice)}
timeout={this.props.timeout}
widgetHeight={this.getWidgetHeight(slice)}
widgetWidth={this.getWidgetWidth(slice)}
exploreChartUrl={getExploreUrl(this.props.getFormDataExtra(slice))}
exportCSVUrl={getExploreUrl(this.props.getFormDataExtra(slice), 'csv')}
isExpanded={!!this.isExpanded(slice)}
isLoading={currentChart.chartStatus === 'loading'}
isCached={queryResponse.is_cached}
cachedDttm={queryResponse.cached_dttm}
toggleExpandSlice={this.props.toggleExpandSlice}
forceRefresh={this.forceRefresh}
removeSlice={this.removeSlice}
updateSliceName={this.updateSliceName}
addFilter={this.props.addFilter}
getFilters={this.props.getFilters}
clearFilter={this.props.clearFilter}
removeFilter={this.props.removeFilter}
editMode={this.props.editMode}
annotationQuery={currentChart.annotationQuery}
annotationError={currentChart.annotationError}
/>
</div>);
});
return (
<ResponsiveReactGridLayout
className="layout"
layouts={{ lg: this.state.layout }}
onResizeStop={this.onResizeStop.bind(this)}
onDragStop={this.onDragStop.bind(this)}
layouts={{ lg: this.props.dashboard.layout }}
onResizeStop={this.onResizeStop}
onDragStop={this.onDragStop}
cols={{ lg: 12, md: 12, sm: 10, xs: 8, xxs: 6 }}
rowHeight={100}
autoSize
@@ -134,27 +183,13 @@ class GridLayout extends React.Component {
useCSSTransforms
draggableHandle=".drag"
>
{this.state.slices.map(slice => (
<div
id={'slice_' + slice.slice_id}
key={slice.slice_id}
data-slice-id={slice.slice_id}
className={`widget ${slice.form_data.viz_type}`}
>
<SliceCell
slice={slice}
removeSlice={this.removeSlice.bind(this, slice.slice_id)}
expandedSlices={this.props.dashboard.metadata.expanded_slices}
updateSliceName={this.props.dashboard.dash_edit_perm ?
this.updateSliceName.bind(this) : null}
/>
</div>
))}
{cells}
</ResponsiveReactGridLayout>
);
}
}
GridLayout.propTypes = propTypes;
GridLayout.defaultProps = defaultProps;
export default GridLayout;

View File

@@ -3,42 +3,108 @@ import PropTypes from 'prop-types';
import Controls from './Controls';
import EditableTitle from '../../components/EditableTitle';
import Button from '../../components/Button';
import FaveStar from '../../components/FaveStar';
import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
import { t } from '../../locales';
const propTypes = {
dashboard: PropTypes.object,
};
const defaultProps = {
dashboard: PropTypes.object.isRequired,
filters: PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
isStarred: PropTypes.bool,
addSlicesToDashboard: PropTypes.func,
onSave: PropTypes.func,
onChange: PropTypes.func,
fetchFaveStar: PropTypes.func,
renderSlices: PropTypes.func,
saveFaveStar: PropTypes.func,
serialize: PropTypes.func,
startPeriodicRender: PropTypes.func,
updateDashboardTitle: PropTypes.func,
editMode: PropTypes.bool.isRequired,
setEditMode: PropTypes.func.isRequired,
unsavedChanges: PropTypes.bool.isRequired,
};
class Header extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
};
this.handleSaveTitle = this.handleSaveTitle.bind(this);
this.toggleEditMode = this.toggleEditMode.bind(this);
}
handleSaveTitle(title) {
this.props.dashboard.updateDashboardTitle(title);
this.props.updateDashboardTitle(title);
}
toggleEditMode() {
this.props.setEditMode(!this.props.editMode);
}
renderUnsaved() {
if (!this.props.unsavedChanges) {
return null;
}
return (
<InfoTooltipWithTrigger
label="unsaved"
tooltip={t('Unsaved changes')}
icon="exclamation-triangle"
className="text-danger m-r-5"
placement="top"
/>
);
}
renderEditButton() {
if (!this.props.dashboard.dash_save_perm) {
return null;
}
const btnText = this.props.editMode ? 'Switch to View Mode' : 'Edit Dashboard';
return (
<Button
bsStyle="default"
className="m-r-5"
style={{ width: '150px' }}
onClick={this.toggleEditMode}
>
{btnText}
</Button>);
}
render() {
const dashboard = this.props.dashboard;
return (
<div className="title">
<div className="pull-left">
<h1 className="outer-container">
<h1 className="outer-container pull-left">
<EditableTitle
title={dashboard.dashboard_title}
canEdit={dashboard.dash_save_perm}
canEdit={dashboard.dash_save_perm && this.props.editMode}
onSaveTitle={this.handleSaveTitle}
noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
showTooltip={this.props.editMode}
/>
<span is class="favstar" class_name="Dashboard" obj_id={dashboard.id} />
<span className="favstar m-r-5">
<FaveStar
itemId={dashboard.id}
fetchFaveStar={this.props.fetchFaveStar}
saveFaveStar={this.props.saveFaveStar}
isStarred={this.props.isStarred}
/>
</span>
{this.renderUnsaved()}
</h1>
</div>
<div className="pull-right" style={{ marginTop: '35px' }}>
{!this.props.dashboard.standalone_mode &&
<Controls dashboard={dashboard} />
}
{this.renderEditButton()}
<Controls
dashboard={dashboard}
filters={this.props.filters}
userId={this.props.userId}
addSlicesToDashboard={this.props.addSlicesToDashboard}
onSave={this.props.onSave}
onChange={this.props.onChange}
renderSlices={this.props.renderSlices}
serialize={this.props.serialize}
startPeriodicRender={this.props.startPeriodicRender}
editMode={this.props.editMode}
/>
</div>
<div className="clearfix" />
</div>
@@ -46,6 +112,5 @@ class Header extends React.PureComponent {
}
}
Header.propTypes = propTypes;
Header.defaultProps = defaultProps;
export default Header;

View File

@@ -34,7 +34,7 @@ class RefreshIntervalModal extends React.PureComponent {
return (
<ModalTrigger
triggerNode={this.props.triggerNode}
isButton
isMenuItem
modalTitle={t('Refresh Interval')}
modalBody={
<div>

View File

@@ -13,6 +13,9 @@ const propTypes = {
css: PropTypes.string,
dashboard: PropTypes.object.isRequired,
triggerNode: PropTypes.node.isRequired,
filters: PropTypes.object.isRequired,
serialize: PropTypes.func,
onSave: PropTypes.func,
};
class SaveModal extends React.PureComponent {
@@ -45,8 +48,8 @@ class SaveModal extends React.PureComponent {
});
}
saveDashboardRequest(data, url, saveType) {
const dashboard = this.props.dashboard;
const saveModal = this.modal;
const onSaveDashboard = this.props.onSave;
Object.assign(data, { css: this.props.css });
$.ajax({
type: 'POST',
@@ -56,7 +59,7 @@ class SaveModal extends React.PureComponent {
},
success(resp) {
saveModal.close();
dashboard.onSave();
onSaveDashboard();
if (saveType === 'newDashboard') {
window.location = `/superset/dashboard/${resp.id}/`;
} else {
@@ -72,21 +75,13 @@ class SaveModal extends React.PureComponent {
}
saveDashboard(saveType, newDashboardTitle) {
const dashboard = this.props.dashboard;
const expandedSlices = {};
$.each($('.slice_info'), function () {
const widget = $(this).parents('.widget');
const sliceDescription = widget.find('.slice_description');
if (sliceDescription.is(':visible')) {
expandedSlices[$(widget).attr('data-slice-id')] = true;
}
});
const positions = dashboard.reactGridLayout.serialize();
const positions = this.props.serialize();
const data = {
positions,
css: this.state.css,
expanded_slices: expandedSlices,
expanded_slices: dashboard.metadata.expanded_slices || {},
dashboard_title: dashboard.dashboard_title,
default_filters: dashboard.readFilters(),
default_filters: JSON.stringify(this.props.filters),
duplicate_slices: this.state.duplicateSlices,
};
let url = null;
@@ -111,6 +106,7 @@ class SaveModal extends React.PureComponent {
return (
<ModalTrigger
ref={(modal) => { this.modal = modal; }}
isMenuItem
triggerNode={this.props.triggerNode}
modalTitle={t('Save Dashboard')}
modalBody={

View File

@@ -11,6 +11,8 @@ require('react-bootstrap-table/css/react-bootstrap-table.css');
const propTypes = {
dashboard: PropTypes.object.isRequired,
triggerNode: PropTypes.node.isRequired,
userId: PropTypes.string.isRequired,
addSlicesToDashboard: PropTypes.func,
};
class SliceAdder extends React.Component {
@@ -39,11 +41,13 @@ class SliceAdder extends React.Component {
}
componentWillUnmount() {
this.slicesRequest.abort();
if (this.slicesRequest) {
this.slicesRequest.abort();
}
}
onEnterModal() {
const uri = '/sliceaddview/api/read?_flt_0_created_by=' + this.props.dashboard.curUserId;
const uri = `/sliceaddview/api/read?_flt_0_created_by=${this.props.userId}`;
this.slicesRequest = $.ajax({
url: uri,
type: 'GET',
@@ -53,6 +57,7 @@ class SliceAdder extends React.Component {
id: slice.id,
sliceName: slice.slice_name,
vizType: slice.viz_type,
datasourceLink: slice.datasource_link,
modified: slice.modified,
}));
@@ -65,14 +70,30 @@ class SliceAdder extends React.Component {
error: (error) => {
this.errored = true;
this.setState({
errorMsg: this.props.dashboard.getAjaxErrorMsg(error),
errorMsg: t('Sorry, there was an error fetching slices to this dashboard: ') +
this.getAjaxErrorMsg(error),
});
},
});
}
getAjaxErrorMsg(error) {
const respJSON = error.responseJSON;
return (respJSON && respJSON.message) ? respJSON.message :
error.responseText;
}
addSlices() {
this.props.dashboard.addSlicesToDashboard(Object.keys(this.state.selectionMap));
const adder = this;
this.props.addSlicesToDashboard(Object.keys(this.state.selectionMap))
// if successful, page will be reloaded.
.fail((error) => {
adder.errored = true;
adder.setState({
errorMsg: t('Sorry, there was an error adding slices to this dashboard: ') +
this.getAjaxErrorMsg(error),
});
});
}
toggleSlice(slice) {
@@ -147,6 +168,14 @@ class SliceAdder extends React.Component {
>
{t('Viz')}
</TableHeaderColumn>
<TableHeaderColumn
dataField="datasourceLink"
dataSort
// Will cause react-bootstrap-table to interpret the HTML returned
dataFormat={datasourceLink => datasourceLink}
>
{t('Datasource')}
</TableHeaderColumn>
<TableHeaderColumn
dataField="modified"
dataSort
@@ -175,9 +204,10 @@ class SliceAdder extends React.Component {
triggerNode={this.props.triggerNode}
tooltip={t('Add a new slice to the dashboard')}
beforeOpen={this.onEnterModal.bind(this)}
isButton
isMenuItem
modalBody={modalContent}
bsSize="large"
setModalAsTriggerChildren
modalTitle={t('Add Slices to Dashboard')}
/>
);

View File

@@ -1,117 +0,0 @@
/* eslint-disable react/no-danger */
import React from 'react';
import PropTypes from 'prop-types';
import { t } from '../../locales';
import { getExploreUrl } from '../../explore/exploreUtils';
import EditableTitle from '../../components/EditableTitle';
const propTypes = {
slice: PropTypes.object.isRequired,
removeSlice: PropTypes.func.isRequired,
updateSliceName: PropTypes.func,
expandedSlices: PropTypes.object,
};
const SliceCell = ({ expandedSlices, removeSlice, slice, updateSliceName }) => {
const onSaveTitle = (newTitle) => {
if (updateSliceName) {
updateSliceName(slice.slice_id, newTitle);
}
};
return (
<div className="slice-cell" id={`${slice.slice_id}-cell`}>
<div className="row chart-header">
<div className="col-md-12">
<div className="header">
<EditableTitle
title={slice.slice_name}
canEdit={!!updateSliceName}
onSaveTitle={onSaveTitle}
noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
/>
</div>
<div className="chart-controls">
<div id={'controls_' + slice.slice_id} className="pull-right">
<a title={t('Move chart')} data-toggle="tooltip">
<i className="fa fa-arrows drag" />
</a>
<a className="refresh" title={t('Force refresh data')} data-toggle="tooltip">
<i className="fa fa-repeat" />
</a>
{slice.description &&
<a title={t('Toggle chart description')}>
<i
className="fa fa-info-circle slice_info"
title={slice.description}
data-toggle="tooltip"
/>
</a>
}
<a
href={slice.edit_url}
title={t('Edit chart')}
data-toggle="tooltip"
>
<i className="fa fa-pencil" />
</a>
<a
className="exportCSV"
href={getExploreUrl(slice.form_data, 'csv')}
title={t('Export CSV')}
data-toggle="tooltip"
>
<i className="fa fa-table" />
</a>
<a
className="exploreChart"
href={getExploreUrl(slice.form_data)}
title={t('Explore chart')}
data-toggle="tooltip"
>
<i className="fa fa-share" />
</a>
<a
className="remove-chart"
title={t('Remove chart from dashboard')}
data-toggle="tooltip"
>
<i
className="fa fa-close"
onClick={() => { removeSlice(slice.slice_id); }}
/>
</a>
</div>
</div>
</div>
</div>
<div
className="slice_description bs-callout bs-callout-default"
style={
expandedSlices &&
expandedSlices[String(slice.slice_id)] ? {} : { display: 'none' }
}
dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
/>
<div className="row chart-container">
<input type="hidden" value="false" />
<div id={'token_' + slice.slice_id} className="token col-md-12">
<img
src="/static/assets/images/loading.gif"
className="loading"
alt="loading"
/>
<div
id={'con_' + slice.slice_id}
className={`slice_container ${slice.form_data.viz_type}`}
/>
</div>
</div>
</div>
);
};
SliceCell.propTypes = propTypes;
export default SliceCell;

View File

@@ -0,0 +1,172 @@
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { t } from '../../locales';
import EditableTitle from '../../components/EditableTitle';
import TooltipWrapper from '../../components/TooltipWrapper';
const propTypes = {
slice: PropTypes.object.isRequired,
exploreChartUrl: PropTypes.string,
exportCSVUrl: PropTypes.string,
isExpanded: PropTypes.bool,
isCached: PropTypes.bool,
cachedDttm: PropTypes.string,
formDataExtra: PropTypes.object,
removeSlice: PropTypes.func,
updateSliceName: PropTypes.func,
toggleExpandSlice: PropTypes.func,
forceRefresh: PropTypes.func,
editMode: PropTypes.bool,
annotationQuery: PropTypes.object,
annotationError: PropTypes.object,
};
const defaultProps = {
forceRefresh: () => ({}),
removeSlice: () => ({}),
updateSliceName: () => ({}),
toggleExpandSlice: () => ({}),
editMode: false,
};
class SliceHeader extends React.PureComponent {
constructor(props) {
super(props);
this.onSaveTitle = this.onSaveTitle.bind(this);
}
onSaveTitle(newTitle) {
if (this.props.updateSliceName) {
this.props.updateSliceName(this.props.slice.slice_id, newTitle);
}
}
render() {
const slice = this.props.slice;
const isCached = this.props.isCached;
const isExpanded = !!this.props.isExpanded;
const cachedWhen = moment.utc(this.props.cachedDttm).fromNow();
const refreshTooltip = isCached ?
t('Served from data cached %s . Click to force refresh.', cachedWhen) :
t('Force refresh data');
const annoationsLoading = t('Annotation layers are still loading.');
const annoationsError = t('One ore more annotation layers failed loading.');
return (
<div className="row chart-header">
<div className="col-md-12">
<div className="header">
<EditableTitle
title={slice.slice_name}
canEdit={!!this.props.updateSliceName && this.props.editMode}
onSaveTitle={this.onSaveTitle}
noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
/>
{!!Object.values(this.props.annotationQuery || {}).length &&
<TooltipWrapper
label="annotations-loading"
placement="top"
tooltip={annoationsLoading}
>
<i className="fa fa-refresh warning" />
</TooltipWrapper>
}
{!!Object.values(this.props.annotationError || {}).length &&
<TooltipWrapper
label="annoation-errors"
placement="top"
tooltip={annoationsError}
>
<i className="fa fa-exclamation-circle danger" />
</TooltipWrapper>
}
</div>
<div className="chart-controls">
<div id={'controls_' + slice.slice_id} className="pull-right">
{this.props.editMode &&
<a>
<TooltipWrapper
placement="top"
label="move"
tooltip={t('Move chart')}
>
<i className="fa fa-arrows drag" />
</TooltipWrapper>
</a>
}
<a
className={`refresh ${isCached ? 'danger' : ''}`}
onClick={() => (this.props.forceRefresh(slice.slice_id))}
>
<TooltipWrapper
placement="top"
label="refresh"
tooltip={refreshTooltip}
>
<i className="fa fa-repeat" />
</TooltipWrapper>
</a>
{slice.description &&
<a onClick={() => this.props.toggleExpandSlice(slice, !isExpanded)}>
<TooltipWrapper
placement="top"
label="description"
tooltip={t('Toggle chart description')}
>
<i className="fa fa-info-circle slice_info" />
</TooltipWrapper>
</a>
}
<a href={slice.edit_url} target="_blank">
<TooltipWrapper
placement="top"
label="edit"
tooltip={t('Edit chart')}
>
<i className="fa fa-pencil" />
</TooltipWrapper>
</a>
<a className="exportCSV" href={this.props.exportCSVUrl}>
<TooltipWrapper
placement="top"
label="exportCSV"
tooltip={t('Export CSV')}
>
<i className="fa fa-table" />
</TooltipWrapper>
</a>
<a className="exploreChart" href={this.props.exploreChartUrl} target="_blank">
<TooltipWrapper
placement="top"
label="exploreChart"
tooltip={t('Explore chart')}
>
<i className="fa fa-share" />
</TooltipWrapper>
</a>
{this.props.editMode &&
<a className="remove-chart" onClick={() => (this.props.removeSlice(slice))}>
<TooltipWrapper
placement="top"
label="close"
tooltip={t('Remove chart from dashboard')}
>
<i className="fa fa-close" />
</TooltipWrapper>
</a>
}
</div>
</div>
</div>
</div>
);
}
}
SliceHeader.propTypes = propTypes;
SliceHeader.defaultProps = defaultProps;
export default SliceHeader;

View File

@@ -0,0 +1,29 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware, compose } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { initEnhancer } from '../reduxUtils';
import { appSetup } from '../common';
import { initJQueryAjax } from '../modules/utils';
import DashboardContainer from './components/DashboardContainer';
import rootReducer, { getInitialState } from './reducers';
appSetup();
initJQueryAjax();
const appContainer = document.getElementById('app');
const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
const initState = Object.assign({}, getInitialState(bootstrapData));
const store = createStore(
rootReducer, initState, compose(applyMiddleware(thunk), initEnhancer(false)));
ReactDOM.render(
<Provider store={store}>
<DashboardContainer />
</Provider>,
appContainer,
);

View File

@@ -0,0 +1,205 @@
import { combineReducers } from 'redux';
import d3 from 'd3';
import shortid from 'shortid';
import charts, { chart } from '../chart/chartReducer';
import * as actions from './actions';
import { getParam } from '../modules/utils';
import { alterInArr, removeFromArr } from '../reduxUtils';
import { applyDefaultFormData } from '../explore/stores/store';
import { getColorFromScheme } from '../modules/colors';
export function getInitialState(bootstrapData) {
const { user_id, datasources, common } = bootstrapData;
delete common.locale;
delete common.language_pack;
const dashboard = { ...bootstrapData.dashboard_data };
let filters = {};
try {
// allow request parameter overwrite dashboard metadata
filters = JSON.parse(getParam('preselect_filters') || dashboard.metadata.default_filters);
} catch (e) {
//
}
// Priming the color palette with user's label-color mapping provided in
// the dashboard's JSON metadata
if (dashboard.metadata && dashboard.metadata.label_colors) {
const colorMap = dashboard.metadata.label_colors;
for (const label in colorMap) {
getColorFromScheme(label, null, colorMap[label]);
}
}
dashboard.posDict = {};
dashboard.layout = [];
if (dashboard.position_json) {
dashboard.position_json.forEach((position) => {
dashboard.posDict[position.slice_id] = position;
});
}
dashboard.slices.forEach((slice, index) => {
const sliceId = slice.slice_id;
let pos = dashboard.posDict[sliceId];
if (!pos) {
pos = {
col: (index * 4 + 1) % 12,
row: Math.floor((index) / 3) * 4,
size_x: 4,
size_y: 4,
};
}
dashboard.layout.push({
i: String(sliceId),
x: pos.col - 1,
y: pos.row,
w: pos.size_x,
minW: 2,
h: pos.size_y,
});
});
// will use charts action/reducers to handle chart render
const initCharts = {};
dashboard.slices.forEach((slice) => {
const chartKey = 'slice_' + slice.slice_id;
initCharts[chartKey] = { ...chart,
chartKey,
slice_id: slice.slice_id,
form_data: slice.form_data,
formData: applyDefaultFormData(slice.form_data),
};
});
// also need to add formData for dashboard.slices
dashboard.slices = dashboard.slices.map(slice =>
({ ...slice, formData: applyDefaultFormData(slice.form_data) }),
);
return {
charts: initCharts,
dashboard: { filters, dashboard, userId: user_id, datasources, common, editMode: false },
};
}
export const dashboard = function (state = {}, action) {
const actionHandlers = {
[actions.UPDATE_DASHBOARD_TITLE]() {
const newDashboard = { ...state.dashboard, dashboard_title: action.title };
return { ...state, dashboard: newDashboard };
},
[actions.UPDATE_DASHBOARD_LAYOUT]() {
const newDashboard = { ...state.dashboard, layout: action.layout };
return { ...state, dashboard: newDashboard };
},
[actions.REMOVE_SLICE]() {
const key = String(action.slice.slice_id);
const newLayout = state.dashboard.layout.filter(reactPos => (reactPos.i !== key));
const newDashboard = removeFromArr(state.dashboard, 'slices', action.slice, 'slice_id');
// if this slice is a filter
const newFilter = { ...state.filters };
let refresh = false;
if (state.filters[key]) {
delete newFilter[key];
refresh = true;
}
return {
...state,
dashboard: { ...newDashboard, layout: newLayout },
filters: newFilter,
refresh,
};
},
[actions.TOGGLE_FAVE_STAR]() {
return { ...state, isStarred: action.isStarred };
},
[actions.SET_EDIT_MODE]() {
return { ...state, editMode: action.editMode };
},
[actions.TOGGLE_EXPAND_SLICE]() {
const updatedExpandedSlices = { ...state.dashboard.metadata.expanded_slices };
const sliceId = action.slice.slice_id;
if (action.isExpanded) {
updatedExpandedSlices[sliceId] = true;
} else {
delete updatedExpandedSlices[sliceId];
}
const metadata = { ...state.dashboard.metadata, expanded_slices: updatedExpandedSlices };
const newDashboard = { ...state.dashboard, metadata };
return { ...state, dashboard: newDashboard };
},
// filters
[actions.ADD_FILTER]() {
const selectedSlice = state.dashboard.slices
.find(slice => (slice.slice_id === action.sliceId));
if (!selectedSlice) {
return state;
}
let filters = state.filters;
const { sliceId, col, vals, merge, refresh } = action;
const filterKeys = ['__from', '__to', '__time_col',
'__time_grain', '__time_origin', '__granularity'];
if (filterKeys.indexOf(col) >= 0 ||
selectedSlice.formData.groupby.indexOf(col) !== -1) {
let newFilter = {};
if (!(sliceId in filters)) {
// Straight up set the filters if none existed for the slice
newFilter = { [col]: vals };
} else if (filters[sliceId] && !(col in filters[sliceId]) || !merge) {
newFilter = { ...filters[sliceId], [col]: vals };
// d3.merge pass in array of arrays while some value form filter components
// from and to filter box require string to be process and return
} else if (filters[sliceId][col] instanceof Array) {
newFilter[col] = d3.merge([filters[sliceId][col], vals]);
} else {
newFilter[col] = d3.merge([[filters[sliceId][col]], vals])[0] || '';
}
filters = { ...filters, [sliceId]: newFilter };
}
return { ...state, filters, refresh };
},
[actions.CLEAR_FILTER]() {
const newFilters = { ...state.filters };
delete newFilters[action.sliceId];
return { ...state, filter: newFilters, refresh: true };
},
[actions.REMOVE_FILTER]() {
const { sliceId, col, vals, refresh } = action;
const excluded = new Set(vals);
const valFilter = val => !excluded.has(val);
let filters = state.filters;
// Have to be careful not to modify the dashboard state so that
// the render actually triggers
if (sliceId in state.filters && col in state.filters[sliceId]) {
const newFilter = filters[sliceId][col].filter(valFilter);
filters = { ...filters, [sliceId]: newFilter };
}
return { ...state, filters, refresh };
},
// slice reducer
[actions.UPDATE_SLICE_NAME]() {
const newDashboard = alterInArr(
state.dashboard, 'slices',
action.slice, { slice_name: action.sliceName },
'slice_id');
return { ...state, dashboard: newDashboard };
},
};
if (action.type in actionHandlers) {
return actionHandlers[action.type]();
}
return state;
};
export default combineReducers({
charts,
dashboard,
impressionId: () => (shortid.generate()),
});

View File

@@ -1,74 +0,0 @@
import { getExploreUrl } from '../exploreUtils';
import { getFormDataFromControls } from '../stores/store';
import { triggerQuery } from './exploreActions';
const $ = window.$ = require('jquery');
export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
export function chartUpdateStarted(queryRequest, latestQueryFormData) {
return { type: CHART_UPDATE_STARTED, queryRequest, latestQueryFormData };
}
export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED';
export function chartUpdateSucceeded(queryResponse) {
return { type: CHART_UPDATE_SUCCEEDED, queryResponse };
}
export const CHART_UPDATE_STOPPED = 'CHART_UPDATE_STOPPED';
export function chartUpdateStopped(queryRequest) {
if (queryRequest) {
queryRequest.abort();
}
return { type: CHART_UPDATE_STOPPED };
}
export const CHART_UPDATE_TIMEOUT = 'CHART_UPDATE_TIMEOUT';
export function chartUpdateTimeout(statusText, timeout) {
return { type: CHART_UPDATE_TIMEOUT, statusText, timeout };
}
export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED';
export function chartUpdateFailed(queryResponse) {
return { type: CHART_UPDATE_FAILED, queryResponse };
}
export const UPDATE_CHART_STATUS = 'UPDATE_CHART_STATUS';
export function updateChartStatus(status) {
return { type: UPDATE_CHART_STATUS, status };
}
export const CHART_RENDERING_FAILED = 'CHART_RENDERING_FAILED';
export function chartRenderingFailed(error) {
return { type: CHART_RENDERING_FAILED, error };
}
export const REMOVE_CHART_ALERT = 'REMOVE_CHART_ALERT';
export function removeChartAlert() {
return { type: REMOVE_CHART_ALERT };
}
export const RUN_QUERY = 'RUN_QUERY';
export function runQuery(formData, force = false, timeout = 60) {
return function (dispatch, getState) {
const { explore } = getState();
const lastQueryFormData = getFormDataFromControls(explore.controls);
const url = getExploreUrl(formData, 'json', force);
const queryRequest = $.ajax({
url,
dataType: 'json',
success(queryResponse) {
dispatch(chartUpdateSucceeded(queryResponse));
},
error(err) {
if (err.statusText === 'timeout') {
dispatch(chartUpdateTimeout(err.statusText, timeout));
} else if (err.statusText !== 'abort') {
dispatch(chartUpdateFailed(err.responseJSON));
}
},
timeout: timeout * 1000,
});
dispatch(chartUpdateStarted(queryRequest, lastQueryFormData));
dispatch(triggerQuery(false));
};
}

View File

@@ -1,4 +1,5 @@
/* eslint camelcase: 0 */
import { triggerQuery } from '../../chart/chartAction';
const $ = window.$ = require('jquery');
@@ -54,11 +55,6 @@ export function resetControls() {
return { type: RESET_FIELDS };
}
export const TRIGGER_QUERY = 'TRIGGER_QUERY';
export function triggerQuery(value = true) {
return { type: TRIGGER_QUERY, value };
}
export function fetchDatasourceMetadata(datasourceKey, alsoTriggerQuery = false) {
return function (dispatch) {
dispatch(fetchDatasourceStarted());
@@ -146,11 +142,6 @@ export function updateChartTitle(slice_name) {
return { type: UPDATE_CHART_TITLE, slice_name };
}
export const RENDER_TRIGGERED = 'RENDER_TRIGGERED';
export function renderTriggered() {
return { type: RENDER_TRIGGERED };
}
export const CREATE_NEW_SLICE = 'CREATE_NEW_SLICE';
export function createNewSlice(can_add, can_download, can_overwrite, slice, form_data) {
return { type: CREATE_NEW_SLICE, can_add, can_download, can_overwrite, slice, form_data };

View File

@@ -1,362 +0,0 @@
import $ from 'jquery';
import React from 'react';
import PropTypes from 'prop-types';
import Mustache from 'mustache';
import { connect } from 'react-redux';
import { Alert, Collapse, Panel } from 'react-bootstrap';
import visMap from '../../../visualizations/main';
import { d3format } from '../../modules/utils';
import ExploreActionButtons from './ExploreActionButtons';
import EditableTitle from '../../components/EditableTitle';
import FaveStar from '../../components/FaveStar';
import TooltipWrapper from '../../components/TooltipWrapper';
import Timer from '../../components/Timer';
import { getExploreUrl } from '../exploreUtils';
import { getFormDataFromControls } from '../stores/store';
import CachedLabel from '../../components/CachedLabel';
import { t } from '../../locales';
const CHART_STATUS_MAP = {
failed: 'danger',
loading: 'warning',
success: 'success',
};
const propTypes = {
actions: PropTypes.object.isRequired,
alert: PropTypes.string,
can_overwrite: PropTypes.bool.isRequired,
can_download: PropTypes.bool.isRequired,
chartStatus: PropTypes.string,
chartUpdateEndTime: PropTypes.number,
chartUpdateStartTime: PropTypes.number.isRequired,
column_formats: PropTypes.object,
containerId: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
width: PropTypes.string.isRequired,
isStarred: PropTypes.bool.isRequired,
slice: PropTypes.object,
table_name: PropTypes.string,
viz_type: PropTypes.string.isRequired,
formData: PropTypes.object,
latestQueryFormData: PropTypes.object,
queryResponse: PropTypes.object,
triggerRender: PropTypes.bool,
standalone: PropTypes.bool,
datasourceType: PropTypes.string,
datasourceId: PropTypes.number,
timeout: PropTypes.number,
};
class ChartContainer extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
selector: `#${props.containerId}`,
showStackTrace: false,
};
}
componentDidUpdate(prevProps) {
if (
this.props.queryResponse &&
(
prevProps.queryResponse !== this.props.queryResponse ||
prevProps.height !== this.props.height ||
prevProps.width !== this.props.width ||
this.props.triggerRender
) && !this.props.queryResponse.error
&& this.props.chartStatus !== 'failed'
&& this.props.chartStatus !== 'stopped'
&& this.props.chartStatus !== 'loading'
) {
this.renderViz();
}
}
getMockedSliceObject() {
const props = this.props;
const getHeight = () => {
const headerHeight = props.standalone ? 0 : 100;
return parseInt(props.height, 10) - headerHeight;
};
return {
viewSqlQuery: props.queryResponse.query,
containerId: props.containerId,
datasource: props.datasource,
selector: this.state.selector,
formData: props.formData,
container: {
html: (data) => {
// this should be a callback to clear the contents of the slice container
$(this.state.selector).html(data);
},
css: (property, value) => {
$(this.state.selector).css(property, value);
},
height: getHeight,
show: () => { },
get: n => ($(this.state.selector).get(n)),
find: classname => ($(this.state.selector).find(classname)),
},
width: () => this.chartContainerRef.getBoundingClientRect().width,
height: getHeight,
render_template: (s) => {
const context = {
width: this.width,
height: this.height,
};
return Mustache.render(s, context);
},
setFilter: () => {},
getFilters: () => (
// return filter objects from viz.formData
{}
),
addFilter: () => {},
removeFilter: () => {},
done: () => {},
clearError: () => {
// no need to do anything here since Alert is closable
// query button will also remove Alert
},
error() {},
d3format: (col, number) => {
// mock d3format function in Slice object in superset.js
const format = props.column_formats[col];
return d3format(format, number);
},
data: {
csv_endpoint: getExploreUrl(props.formData, 'csv'),
json_endpoint: getExploreUrl(props.formData, 'json'),
standalone_endpoint: getExploreUrl(props.formData, 'standalone'),
},
};
}
removeAlert() {
this.props.actions.removeChartAlert();
}
runQuery() {
this.props.actions.runQuery(this.props.formData, true, this.props.timeout);
}
updateChartTitleOrSaveSlice(newTitle) {
const isNewSlice = !this.props.slice;
const params = {
slice_name: newTitle,
action: isNewSlice ? 'saveas' : 'overwrite',
};
const saveUrl = getExploreUrl(this.props.formData, 'base', false, null, params);
this.props.actions.saveSlice(saveUrl)
.then((data) => {
if (isNewSlice) {
this.props.actions.createNewSlice(
data.can_add, data.can_download, data.can_overwrite,
data.slice, data.form_data);
} else {
this.props.actions.updateChartTitle(newTitle);
}
});
}
renderChartTitle() {
let title;
if (this.props.slice) {
title = this.props.slice.slice_name;
} else {
title = t('%s - untitled', this.props.table_name);
}
return title;
}
renderViz() {
this.props.actions.renderTriggered();
const mockSlice = this.getMockedSliceObject();
this.setState({ mockSlice });
const viz = visMap[this.props.viz_type];
try {
viz(mockSlice, this.props.queryResponse, this.props.actions.setControlValue);
} catch (e) {
this.props.actions.chartRenderingFailed(e);
}
}
renderAlert() {
/* eslint-disable react/no-danger */
const msg = (
<div>
<i
className="fa fa-close pull-right"
onClick={this.removeAlert.bind(this)}
style={{ cursor: 'pointer' }}
/>
<p
dangerouslySetInnerHTML={{ __html: this.props.alert }}
/>
</div>);
return (
<div>
<Alert
bsStyle="warning"
onClick={() => this.setState({ showStackTrace: !this.state.showStackTrace })}
>
{msg}
</Alert>
{this.props.queryResponse && this.props.queryResponse.stacktrace &&
<Collapse in={this.state.showStackTrace}>
<pre>
{this.props.queryResponse.stacktrace}
</pre>
</Collapse>
}
</div>);
}
renderChart() {
if (this.props.alert) {
return this.renderAlert();
}
const loading = this.props.chartStatus === 'loading';
return (
<div>
{loading &&
<img
alt="loading"
width="25"
src="/static/assets/images/loading.gif"
style={{ position: 'absolute' }}
/>
}
<div
id={this.props.containerId}
ref={(ref) => { this.chartContainerRef = ref; }}
className={this.props.viz_type}
style={{
opacity: loading ? '0.25' : '1',
}}
/>
</div>
);
}
render() {
if (this.props.standalone) {
// dom manipulation hack to get rid of the boostrap theme's body background
$('body').addClass('background-transparent');
return this.renderChart();
}
const queryResponse = this.props.queryResponse;
return (
<div className="chart-container">
<Panel
style={{ height: this.props.height }}
header={
<div
id="slice-header"
className="clearfix panel-title-large"
>
<EditableTitle
title={this.renderChartTitle()}
canEdit={!this.props.slice || this.props.can_overwrite}
onSaveTitle={this.updateChartTitleOrSaveSlice.bind(this)}
/>
{this.props.slice &&
<span>
<FaveStar
sliceId={this.props.slice.slice_id}
actions={this.props.actions}
isStarred={this.props.isStarred}
/>
<TooltipWrapper
label="edit-desc"
tooltip={t('Edit slice properties')}
>
<a
className="edit-desc-icon"
href={`/slicemodelview/edit/${this.props.slice.slice_id}`}
>
<i className="fa fa-edit" />
</a>
</TooltipWrapper>
</span>
}
<div className="pull-right">
{this.props.chartStatus === 'success' &&
this.props.queryResponse &&
this.props.queryResponse.is_cached &&
<CachedLabel
onClick={this.runQuery.bind(this)}
cachedTimestamp={queryResponse.cached_dttm}
/>
}
<Timer
startTime={this.props.chartUpdateStartTime}
endTime={this.props.chartUpdateEndTime}
isRunning={this.props.chartStatus === 'loading'}
status={CHART_STATUS_MAP[this.props.chartStatus]}
style={{ fontSize: '10px', marginRight: '5px' }}
/>
<ExploreActionButtons
slice={this.state.mockSlice}
canDownload={this.props.can_download}
chartStatus={this.props.chartStatus}
queryResponse={queryResponse}
queryEndpoint={getExploreUrl(this.props.latestQueryFormData, 'query')}
/>
</div>
</div>
}
>
{this.renderChart()}
</Panel>
</div>
);
}
}
ChartContainer.propTypes = propTypes;
function mapStateToProps({ explore, chart }) {
const formData = getFormDataFromControls(explore.controls);
return {
alert: chart.chartAlert,
can_overwrite: !!explore.can_overwrite,
can_download: !!explore.can_download,
datasource: explore.datasource,
column_formats: explore.datasource ? explore.datasource.column_formats : null,
containerId: explore.slice ? `slice-container-${explore.slice.slice_id}` : 'slice-container',
formData,
isStarred: explore.isStarred,
slice: explore.slice,
standalone: explore.standalone,
table_name: formData.datasource_name,
viz_type: formData.viz_type,
triggerRender: explore.triggerRender,
datasourceType: explore.datasource.type,
datasourceId: explore.datasource_id,
chartStatus: chart.chartStatus,
chartUpdateEndTime: chart.chartUpdateEndTime,
chartUpdateStartTime: chart.chartUpdateStartTime,
latestQueryFormData: chart.latestQueryFormData,
queryResponse: chart.queryResponse,
timeout: explore.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
};
}
export default connect(mapStateToProps, () => ({}))(ChartContainer);

View File

@@ -13,6 +13,7 @@ const propTypes = {
label: PropTypes.string.isRequired,
choices: PropTypes.arrayOf(PropTypes.array),
description: PropTypes.string,
tooltipOnClick: PropTypes.func,
places: PropTypes.number,
validators: PropTypes.array,
validationErrors: PropTypes.array,
@@ -21,8 +22,10 @@ const propTypes = {
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
PropTypes.bool,
PropTypes.array]),
PropTypes.array,
PropTypes.func]),
};
const defaultProps = {

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