Compare commits
93 Commits
0.20.5
...
0.21.0.rc3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea3a0814fc | ||
|
|
680e1cbb42 | ||
|
|
2d37dec5ff | ||
|
|
3f4c306bd6 | ||
|
|
ac432495d7 | ||
|
|
12fb7c1a62 | ||
|
|
feb15a30a2 | ||
|
|
eb0f3970cf | ||
|
|
b82d15af76 | ||
|
|
32b38ee2d6 | ||
|
|
1d702f2142 | ||
|
|
4ae77ba8af | ||
|
|
3c72e1f8fb | ||
|
|
4bfe08d7c3 | ||
|
|
3a7ed8d194 | ||
|
|
4d204b3b36 | ||
|
|
39ee33aeff | ||
|
|
831cd21737 | ||
|
|
f92a172c7f | ||
|
|
a82bb588f4 | ||
|
|
a84bd5225c | ||
|
|
f2b9f3d5c8 | ||
|
|
7faf38c976 | ||
|
|
357b25e5ae | ||
|
|
8e307a3e4d | ||
|
|
f0acc11249 | ||
|
|
fa35d7d2f4 | ||
|
|
e65aba3c46 | ||
|
|
fab7b1083b | ||
|
|
d9161fb76a | ||
|
|
85b18ff5e7 | ||
|
|
3a8af5d0b0 | ||
|
|
1c545d3a2d | ||
|
|
120a5d08f9 | ||
|
|
b586cb0ba7 | ||
|
|
ae2205aeb5 | ||
|
|
2e25fc4161 | ||
|
|
ba89b2d091 | ||
|
|
aee8438924 | ||
|
|
a6ba841e57 | ||
|
|
8643228b51 | ||
|
|
de869973c7 | ||
|
|
ac57780607 | ||
|
|
630604bc6b | ||
|
|
eb5d220b5e | ||
|
|
3f076b00cd | ||
|
|
514f9452f3 | ||
|
|
068c343be0 | ||
|
|
500455fc72 | ||
|
|
1b4f128f55 | ||
|
|
1a3a8daf49 | ||
|
|
7fce8eab3a | ||
|
|
b4c9402737 | ||
|
|
8459347bdc | ||
|
|
f7bf17290c | ||
|
|
d908e48d61 | ||
|
|
a3a4687ebf | ||
|
|
4d48d5d854 | ||
|
|
83e6807fa0 | ||
|
|
ba96984048 | ||
|
|
591e5ec32e | ||
|
|
690de862e8 | ||
|
|
35810ce2bf | ||
|
|
6c52f2ff72 | ||
|
|
d663bea5e6 | ||
|
|
1ea4521d0c | ||
|
|
c4153c0bbe | ||
|
|
ae8b249dc2 | ||
|
|
9500f0aae3 | ||
|
|
be3da6396f | ||
|
|
330926c167 | ||
|
|
cbcc00c929 | ||
|
|
d03b74f754 | ||
|
|
ec21d5af21 | ||
|
|
70c7315ae0 | ||
|
|
4fa1f0ab17 | ||
|
|
39e502faae | ||
|
|
0280bc52e0 | ||
|
|
dee47864c4 | ||
|
|
17623f71d4 | ||
|
|
7453131858 | ||
|
|
e822fb50d8 | ||
|
|
e2bca47421 | ||
|
|
7987cb794b | ||
|
|
7483e2c942 | ||
|
|
e6129eb492 | ||
|
|
b10aca2de1 | ||
|
|
02cbad59de | ||
|
|
ccb87d337c | ||
|
|
63a49983eb | ||
|
|
81dd622fdb | ||
|
|
9a49b1c41d | ||
|
|
b059506afa |
@@ -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.
|
||||
|
||||
99
CHANGELOG.md
@@ -1,5 +1,104 @@
|
||||
## Change Log
|
||||
|
||||
### 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)
|
||||
|
||||
@@ -286,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
|
||||
|
||||
44
README.md
@@ -127,33 +127,14 @@ Installation & Configuration
|
||||
[See in the documentation](https://superset.incubator.apache.org/installation.html)
|
||||
|
||||
|
||||
More screenshots
|
||||
----------------
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
Resources
|
||||
-------------
|
||||
* [Superset Google Group](https://groups.google.com/forum/#!forum/airbnb_superset)
|
||||
* [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
|
||||
@@ -182,6 +163,7 @@ the world know they are using Superset. Join our growing community!
|
||||
- [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)
|
||||
- [Pronto Tools](http://www.prontotools.io)
|
||||
- [Qunar](https://www.qunar.com/)
|
||||
@@ -190,8 +172,28 @@ the world know they are using Superset. Join our growing community!
|
||||
- [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
|
||||
----------------
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
16
docs/faq.rst
@@ -221,3 +221,19 @@ 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::
|
||||
|
||||
{
|
||||
"label_colors": {
|
||||
"Girls": "#FF69B4",
|
||||
"Boys": "#ADD8E6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ 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.
|
||||
|
||||
@@ -550,3 +550,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``.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from superset import sm
|
||||
from collections import defaultdict
|
||||
|
||||
from superset import sm
|
||||
|
||||
|
||||
def cleanup_permissions():
|
||||
# 1. Clean up duplicates.
|
||||
|
||||
28
setup.py
@@ -1,10 +1,11 @@
|
||||
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')
|
||||
PACKAGE_DIR = os.path.join(BASE_DIR, 'superset', 'assets')
|
||||
PACKAGE_FILE = os.path.join(PACKAGE_DIR, 'package.json')
|
||||
with open(PACKAGE_FILE) as package_file:
|
||||
version_string = json.load(package_file)['version']
|
||||
@@ -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,16 +36,16 @@ 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',
|
||||
|
||||
@@ -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
|
||||
@@ -43,7 +42,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)
|
||||
print('no manifest file found at ' + MANIFEST_FILE)
|
||||
|
||||
|
||||
def get_manifest_file(filename):
|
||||
@@ -67,7 +66,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 +91,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,15 +149,15 @@ 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'))
|
||||
|
||||
sm = appbuilder.sm
|
||||
|
||||
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)
|
||||
|
||||
from superset import views # noqa
|
||||
|
||||
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 6.8 KiB |
BIN
superset/assets/images/viz_thumbnails/deck_grid.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
superset/assets/images/viz_thumbnails/deck_hex.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
superset/assets/images/viz_thumbnails/deck_scatter.png
Normal file
|
After Width: | Height: | Size: 777 KiB |
BIN
superset/assets/images/viz_thumbnails/deck_screengrid.png
Normal file
|
After Width: | Height: | Size: 578 KiB |
@@ -153,10 +153,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.');
|
||||
|
||||
@@ -65,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;
|
||||
|
||||
@@ -165,21 +165,26 @@ 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>
|
||||
|
||||
182
superset/assets/javascripts/chart/Chart.jsx
Normal file
@@ -0,0 +1,182 @@
|
||||
/* eslint camelcase: 0 */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Mustache from 'mustache';
|
||||
|
||||
import { d3format } from '../modules/utils';
|
||||
import ChartBody from './ChartBody';
|
||||
import Loading from '../components/Loading';
|
||||
import StackTraceMessage from '../components/StackTraceMessage';
|
||||
import visMap from '../../visualizations/main';
|
||||
|
||||
const propTypes = {
|
||||
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);
|
||||
|
||||
// these properties are used by visualizations
|
||||
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.containerId = nextProps.containerId;
|
||||
this.selector = `#${this.containerId}`;
|
||||
this.formData = nextProps.formData;
|
||||
this.datasource = nextProps.datasource;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
this.props.queryResponse &&
|
||||
this.props.chartStatus === 'success' &&
|
||||
!this.props.queryResponse.error && (
|
||||
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();
|
||||
}
|
||||
|
||||
addFilter(col, vals, merge = true, refresh = true) {
|
||||
this.props.addFilter(col, vals, merge, refresh);
|
||||
}
|
||||
|
||||
clearFilter() {
|
||||
this.props.clearFilter();
|
||||
}
|
||||
|
||||
removeFilter(col, vals) {
|
||||
this.props.removeFilter(col, vals);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
renderViz() {
|
||||
const viz = visMap[this.props.vizType];
|
||||
try {
|
||||
viz(this, this.props.queryResponse, this.props.actions.setControlValue);
|
||||
} 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' : ''}`}>
|
||||
{isLoading &&
|
||||
<Loading size={25} />
|
||||
}
|
||||
|
||||
{this.props.chartAlert &&
|
||||
<StackTraceMessage
|
||||
message={this.props.chartAlert}
|
||||
queryResponse={this.props.queryResponse}
|
||||
/>
|
||||
}
|
||||
|
||||
{!this.props.chartAlert &&
|
||||
<ChartBody
|
||||
containerId={this.containerId}
|
||||
vizType={this.props.formData.viz_type}
|
||||
height={this.height}
|
||||
width={this.width}
|
||||
ref={(inner) => {
|
||||
this.container = inner;
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Chart.propTypes = propTypes;
|
||||
Chart.defaultProps = defaultProps;
|
||||
|
||||
export default Chart;
|
||||
54
superset/assets/javascripts/chart/ChartBody.jsx
Normal 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;
|
||||
28
superset/assets/javascripts/chart/ChartContainer.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
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 {
|
||||
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);
|
||||
91
superset/assets/javascripts/chart/chartAction.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import { getExploreUrl } from '../explore/exploreUtils';
|
||||
import { t } from '../locales';
|
||||
|
||||
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(queryRequest, key) {
|
||||
if (queryRequest) {
|
||||
queryRequest.abort();
|
||||
}
|
||||
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 REMOVE_CHART = 'REMOVE_CHART';
|
||||
export function removeChart(key) {
|
||||
return { type: REMOVE_CHART, key };
|
||||
}
|
||||
|
||||
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);
|
||||
const queryRequest = $.ajax({
|
||||
url,
|
||||
dataType: 'json',
|
||||
timeout: timeout * 1000,
|
||||
success: (queryResponse =>
|
||||
dispatch(chartUpdateSucceeded(queryResponse, key))
|
||||
),
|
||||
error: ((xhr) => {
|
||||
if (xhr.statusText === 'timeout') {
|
||||
dispatch(chartUpdateTimeout(xhr.statusText, timeout, key));
|
||||
} else {
|
||||
let error = '';
|
||||
if (!xhr.responseText) {
|
||||
const status = xhr.status;
|
||||
if (status === 0) {
|
||||
// This may happen when the worker in gunicorn times out
|
||||
error += (
|
||||
t('The server could not be reached. You may want to ' +
|
||||
'verify your connection and try again.'));
|
||||
} else {
|
||||
error += (t('An unknown error occurred. (Status: %s )', status));
|
||||
}
|
||||
}
|
||||
const errorResponse = Object.assign({}, xhr.responseJSON, error);
|
||||
dispatch(chartUpdateFailed(errorResponse, key));
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
dispatch(chartUpdateStarted(queryRequest, key));
|
||||
dispatch(triggerQuery(false, key));
|
||||
};
|
||||
}
|
||||
103
superset/assets/javascripts/chart/chartReducer.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/* 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_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 };
|
||||
},
|
||||
};
|
||||
|
||||
/* 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;
|
||||
}
|
||||
145
superset/assets/javascripts/components/AlteredSliceTag.jsx
Normal 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;
|
||||
@@ -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() {
|
||||
|
||||
87
superset/assets/javascripts/components/OnPasteSelect.jsx
Normal 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,
|
||||
};
|
||||
59
superset/assets/javascripts/components/StackTraceMessage.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
/* 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() {
|
||||
const msg = (
|
||||
<div>
|
||||
<p
|
||||
dangerouslySetInnerHTML={{ __html: this.props.message }}
|
||||
/>
|
||||
</div>);
|
||||
|
||||
return (
|
||||
<div className={`stack-trace-container${this.hasTrace() ? ' has-trace' : ''}`}>
|
||||
<Alert
|
||||
bsStyle="warning"
|
||||
onClick={() => this.setState({ showStackTrace: !this.state.showStackTrace })}
|
||||
>
|
||||
{msg}
|
||||
</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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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')}
|
||||
<i className="fa fa-save" />
|
||||
{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();
|
||||
});
|
||||
112
superset/assets/javascripts/dashboard/actions.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/* 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) {
|
||||
return { type: REMOVE_FILTER, sliceId, col, vals };
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
@@ -14,6 +14,15 @@ const $ = window.$ = require('jquery');
|
||||
|
||||
const propTypes = {
|
||||
dashboard: PropTypes.object.isRequired,
|
||||
slices: PropTypes.array,
|
||||
userId: PropTypes.string.isRequired,
|
||||
addSlicesToDashboard: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
readFilters: PropTypes.func,
|
||||
renderSlices: PropTypes.func,
|
||||
serialize: PropTypes.func,
|
||||
startPeriodicRender: PropTypes.func,
|
||||
};
|
||||
|
||||
class Controls extends React.PureComponent {
|
||||
@@ -36,14 +45,16 @@ class Controls extends React.PureComponent {
|
||||
}
|
||||
refresh() {
|
||||
// Force refresh all slices
|
||||
this.props.dashboard.renderSlices(this.props.dashboard.sliceObjects, true);
|
||||
this.props.renderSlices(true);
|
||||
}
|
||||
changeCss(css) {
|
||||
this.setState({ css });
|
||||
this.props.dashboard.onChange();
|
||||
this.props.onChange();
|
||||
}
|
||||
render() {
|
||||
const dashboard = this.props.dashboard;
|
||||
const { dashboard, userId,
|
||||
addSlicesToDashboard, startPeriodicRender, readFilters,
|
||||
serialize, onSave } = this.props;
|
||||
const emailBody = t('Checkout this dashboard: %s', window.location.href);
|
||||
const emailLink = 'mailto:?Subject=Superset%20Dashboard%20'
|
||||
+ `${dashboard.dashboard_title}&Body=${emailBody}`;
|
||||
@@ -57,18 +68,20 @@ class Controls extends React.PureComponent {
|
||||
</Button>
|
||||
<SliceAdder
|
||||
dashboard={dashboard}
|
||||
addSlicesToDashboard={addSlicesToDashboard}
|
||||
userId={userId}
|
||||
triggerNode={
|
||||
<i className="fa fa-plus" />
|
||||
}
|
||||
/>
|
||||
<RefreshIntervalModal
|
||||
onChange={refreshInterval => dashboard.startPeriodicRender(refreshInterval * 1000)}
|
||||
onChange={refreshInterval => startPeriodicRender(refreshInterval * 1000)}
|
||||
triggerNode={
|
||||
<i className="fa fa-clock-o" />
|
||||
}
|
||||
/>
|
||||
<CodeModal
|
||||
codeCallback={dashboard.readFilters.bind(dashboard)}
|
||||
codeCallback={readFilters}
|
||||
triggerNode={<i className="fa fa-filter" />}
|
||||
/>
|
||||
<CssEditor
|
||||
@@ -96,6 +109,9 @@ class Controls extends React.PureComponent {
|
||||
</Button>
|
||||
<SaveModal
|
||||
dashboard={dashboard}
|
||||
readFilters={readFilters}
|
||||
serialize={serialize}
|
||||
onSave={onSave}
|
||||
css={this.state.css}
|
||||
triggerNode={
|
||||
<Button disabled={!dashboard.dash_save_perm}>
|
||||
|
||||
349
superset/assets/javascripts/dashboard/components/Dashboard.jsx
Normal file
@@ -0,0 +1,349 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import AlertsWrapper from '../../components/AlertsWrapper';
|
||||
import GridLayout from './GridLayout';
|
||||
import Header from './Header';
|
||||
import DashboardAlert from './DashboardAlert';
|
||||
import { getExploreUrl } from '../../explore/exploreUtils';
|
||||
import { areObjectsEqual } from '../../reduxUtils';
|
||||
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,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
initMessages: [],
|
||||
dashboard: {},
|
||||
slices: {},
|
||||
datasources: {},
|
||||
filters: {},
|
||||
timeout: 60,
|
||||
userId: '',
|
||||
isStarred: false,
|
||||
};
|
||||
|
||||
class Dashboard extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.refreshTimer = null;
|
||||
this.firstLoad = true;
|
||||
|
||||
// alert for unsaved changes
|
||||
this.state = {
|
||||
alert: null,
|
||||
trigger: 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.readFilters = this.readFilters.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() {
|
||||
this.loadPreSelectFilters();
|
||||
this.firstLoad = false;
|
||||
window.addEventListener('resize', this.rerenderCharts);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// check filters is changed
|
||||
if (!areObjectsEqual(nextProps.filters, this.props.filters)) {
|
||||
this.renderUnsavedChangeAlert();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!areObjectsEqual(prevProps.filters, this.props.filters) && this.props.refresh) {
|
||||
Object.keys(this.props.filters).forEach(sliceId => (this.refreshExcept(sliceId)));
|
||||
}
|
||||
}
|
||||
|
||||
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.renderUnsavedChangeAlert();
|
||||
}
|
||||
|
||||
onSave() {
|
||||
this.onBeforeUnload(false);
|
||||
this.setState({
|
||||
alert: '',
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
jsonEndpoint(data, force = false) {
|
||||
let endpoint = getExploreUrl(data, 'json', force);
|
||||
if (endpoint.charAt(0) !== '/') {
|
||||
// Known issue for IE <= 11:
|
||||
// https://connect.microsoft.com/IE/feedbackdetail/view/1002846/pathname-incorrect-for-out-of-document-elements
|
||||
endpoint = '/' + endpoint;
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
loadPreSelectFilters() {
|
||||
for (const key in this.props.filters) {
|
||||
for (const col in this.props.filters[key]) {
|
||||
const sliceId = parseInt(key, 10);
|
||||
this.props.actions.addFilter(sliceId, col,
|
||||
this.props.filters[key][col], false, false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refreshExcept(sliceId) {
|
||||
const immune = this.props.dashboard.metadata.filter_immune_slices || [];
|
||||
const slices = this.getAllSlices()
|
||||
.filter(slice => slice.slice_id !== sliceId && 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();
|
||||
}
|
||||
|
||||
readFilters() {
|
||||
// Returns a list of human readable active filters
|
||||
return JSON.stringify(this.props.filters, null, ' ');
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
renderUnsavedChangeAlert() {
|
||||
this.setState({
|
||||
alert: (
|
||||
<span>
|
||||
<strong>{t('You have unsaved changes.')}</strong> {t('Click the')}
|
||||
<i className="fa fa-save" />
|
||||
{t('button on the top right to save your changes.')}
|
||||
</span>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="dashboard-container">
|
||||
{this.state.alert && <DashboardAlert alertContent={this.state.alert} />}
|
||||
<div id="dashboard-header">
|
||||
<AlertsWrapper initMessages={this.props.initMessages} />
|
||||
<Header
|
||||
dashboard={this.props.dashboard}
|
||||
userId={this.props.userId}
|
||||
isStarred={this.props.isStarred}
|
||||
updateDashboardTitle={this.updateDashboardTitle}
|
||||
onSave={this.onSave}
|
||||
onChange={this.onChange}
|
||||
serialize={this.serialize}
|
||||
readFilters={this.readFilters}
|
||||
fetchFaveStar={this.props.actions.fetchFaveStar}
|
||||
saveFaveStar={this.props.actions.saveFaveStar}
|
||||
renderSlices={this.fetchAllSlices}
|
||||
startPeriodicRender={this.startPeriodicRender}
|
||||
addSlicesToDashboard={this.addSlicesToDashboard}
|
||||
/>
|
||||
</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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Dashboard.propTypes = propTypes;
|
||||
Dashboard.defaultProps = defaultProps;
|
||||
|
||||
export default Dashboard;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert } from 'react-bootstrap';
|
||||
|
||||
const propTypes = {
|
||||
alertContent: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
const DashboardAlert = ({ alertContent }) => (
|
||||
<div id="alert-container">
|
||||
<div className="container-fluid">
|
||||
<Alert bsStyle="warning">
|
||||
{alertContent}
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
DashboardAlert.propTypes = propTypes;
|
||||
|
||||
export default DashboardAlert;
|
||||
@@ -0,0 +1,29 @@
|
||||
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 }) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
const actions = { ...chartActions, ...dashboardActions };
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
|
||||
137
superset/assets/javascripts/dashboard/components/GridCell.jsx
Normal file
@@ -0,0 +1,137 @@
|
||||
/* 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,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
forceRefresh: () => ({}),
|
||||
removeSlice: () => ({}),
|
||||
updateSliceName: () => ({}),
|
||||
toggleExpandSlice: () => ({}),
|
||||
addFilter: () => ({}),
|
||||
getFilters: () => ({}),
|
||||
clearFilter: () => ({}),
|
||||
removeFilter: () => ({}),
|
||||
};
|
||||
|
||||
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,
|
||||
} = 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}
|
||||
/>
|
||||
</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;
|
||||
@@ -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,166 @@ 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,
|
||||
};
|
||||
|
||||
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}
|
||||
/>
|
||||
</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 +179,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;
|
||||
|
||||
@@ -3,22 +3,32 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import Controls from './Controls';
|
||||
import EditableTitle from '../../components/EditableTitle';
|
||||
import FaveStar from '../../components/FaveStar';
|
||||
|
||||
const propTypes = {
|
||||
dashboard: PropTypes.object,
|
||||
};
|
||||
const defaultProps = {
|
||||
dashboard: PropTypes.object.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
isStarred: PropTypes.bool,
|
||||
addSlicesToDashboard: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
fetchFaveStar: PropTypes.func,
|
||||
readFilters: PropTypes.func,
|
||||
renderSlices: PropTypes.func,
|
||||
saveFaveStar: PropTypes.func,
|
||||
serialize: PropTypes.func,
|
||||
startPeriodicRender: PropTypes.func,
|
||||
updateDashboardTitle: PropTypes.func,
|
||||
};
|
||||
|
||||
class Header extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
};
|
||||
|
||||
this.handleSaveTitle = this.handleSaveTitle.bind(this);
|
||||
}
|
||||
handleSaveTitle(title) {
|
||||
this.props.dashboard.updateDashboardTitle(title);
|
||||
this.props.updateDashboardTitle(title);
|
||||
}
|
||||
render() {
|
||||
const dashboard = this.props.dashboard;
|
||||
@@ -32,12 +42,29 @@ class Header extends React.PureComponent {
|
||||
onSaveTitle={this.handleSaveTitle}
|
||||
noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
|
||||
/>
|
||||
<span is class="favstar" class_name="Dashboard" obj_id={dashboard.id} />
|
||||
<span className="favstar">
|
||||
<FaveStar
|
||||
itemId={dashboard.id}
|
||||
fetchFaveStar={this.props.fetchFaveStar}
|
||||
saveFaveStar={this.props.saveFaveStar}
|
||||
isStarred={this.props.isStarred}
|
||||
/>
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div className="pull-right" style={{ marginTop: '35px' }}>
|
||||
{!this.props.dashboard.standalone_mode &&
|
||||
<Controls dashboard={dashboard} />
|
||||
<Controls
|
||||
dashboard={dashboard}
|
||||
userId={this.props.userId}
|
||||
addSlicesToDashboard={this.props.addSlicesToDashboard}
|
||||
onSave={this.props.onSave}
|
||||
onChange={this.props.onChange}
|
||||
readFilters={this.props.readFilters}
|
||||
renderSlices={this.props.renderSlices}
|
||||
serialize={this.props.serialize}
|
||||
startPeriodicRender={this.props.startPeriodicRender}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div className="clearfix" />
|
||||
@@ -46,6 +73,5 @@ class Header extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
Header.propTypes = propTypes;
|
||||
Header.defaultProps = defaultProps;
|
||||
|
||||
export default Header;
|
||||
|
||||
@@ -13,6 +13,9 @@ const propTypes = {
|
||||
css: PropTypes.string,
|
||||
dashboard: PropTypes.object.isRequired,
|
||||
triggerNode: PropTypes.node.isRequired,
|
||||
readFilters: PropTypes.func,
|
||||
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: this.props.readFilters(),
|
||||
duplicate_slices: this.state.duplicateSlices,
|
||||
};
|
||||
let url = null;
|
||||
|
||||
@@ -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 {
|
||||
@@ -43,7 +45,7 @@ class SliceAdder extends React.Component {
|
||||
}
|
||||
|
||||
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 +55,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 +68,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 +166,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
|
||||
|
||||
@@ -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;
|
||||
144
superset/assets/javascripts/dashboard/components/SliceHeader.jsx
Normal file
@@ -0,0 +1,144 @@
|
||||
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,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
forceRefresh: () => ({}),
|
||||
removeSlice: () => ({}),
|
||||
updateSliceName: () => ({}),
|
||||
toggleExpandSlice: () => ({}),
|
||||
};
|
||||
|
||||
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');
|
||||
|
||||
return (
|
||||
<div className="row chart-header">
|
||||
<div className="col-md-12">
|
||||
<div className="header">
|
||||
<EditableTitle
|
||||
title={slice.slice_name}
|
||||
canEdit={!!this.props.updateSliceName}
|
||||
onSaveTitle={this.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>
|
||||
<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>
|
||||
<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;
|
||||
29
superset/assets/javascripts/dashboard/index.jsx
Normal 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,
|
||||
);
|
||||
|
||||
198
superset/assets/javascripts/dashboard/reducers.js
Normal file
@@ -0,0 +1,198 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import d3 from 'd3';
|
||||
|
||||
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 };
|
||||
const filters = {};
|
||||
try {
|
||||
// allow request parameter overwrite dashboard metadata
|
||||
const filterData = JSON.parse(getParam('preselect_filters') || dashboard.metadata.default_filters);
|
||||
for (const key in filterData) {
|
||||
const sliceId = parseInt(key, 10);
|
||||
filters[sliceId] = filterData[key];
|
||||
}
|
||||
} 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 },
|
||||
};
|
||||
}
|
||||
|
||||
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 newLayout = state.dashboard.layout.filter(function (reactPos) {
|
||||
return reactPos.i !== String(action.slice.slice_id);
|
||||
});
|
||||
const newDashboard = removeFromArr(state.dashboard, 'slices', action.slice, 'slice_id');
|
||||
return { ...state, dashboard: { ...newDashboard, layout: newLayout } };
|
||||
},
|
||||
[actions.TOGGLE_FAVE_STAR]() {
|
||||
return { ...state, isStarred: action.isStarred };
|
||||
},
|
||||
[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;
|
||||
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) {
|
||||
if (!(sliceId in state.filters)) {
|
||||
filters = { ...state.filters, [sliceId]: {} };
|
||||
}
|
||||
|
||||
let newFilter = {};
|
||||
if (state.filters[sliceId] && !(col in state.filters[sliceId]) || !merge) {
|
||||
newFilter = { ...state.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 (state.filters[sliceId][col] instanceof Array) {
|
||||
newFilter[col] = d3.merge([state.filters[sliceId][col], vals]);
|
||||
} else {
|
||||
newFilter[col] = d3.merge([[state.filters[sliceId][col]], vals])[0] || '';
|
||||
}
|
||||
filters = { ...state.filters, [sliceId]: newFilter };
|
||||
}
|
||||
return { ...state, filters, refresh };
|
||||
},
|
||||
[actions.CLEAR_FILTER]() {
|
||||
const newFilters = { ...state.filters };
|
||||
delete newFilters[action.sliceId];
|
||||
|
||||
return { ...state.dashboard, filter: newFilters, refresh: true };
|
||||
},
|
||||
[actions.REMOVE_FILTER]() {
|
||||
const newFilters = { ...state.filters };
|
||||
const { sliceId, col, vals } = action;
|
||||
|
||||
if (sliceId in state.filters) {
|
||||
if (col in state.filters[sliceId]) {
|
||||
const a = [];
|
||||
newFilters[sliceId][col].forEach(function (v) {
|
||||
if (vals.indexOf(v) < 0) {
|
||||
a.push(v);
|
||||
}
|
||||
});
|
||||
newFilters[sliceId][col] = a;
|
||||
}
|
||||
}
|
||||
return { ...state.dashboard, filter: newFilters, refresh: true };
|
||||
},
|
||||
|
||||
// slice reducer
|
||||
[actions.UPDATE_SLICE_NAME]() {
|
||||
const newDashboard = alterInArr(
|
||||
state.dashboard, 'slices',
|
||||
action.slice, { slice_name: action.sliceName },
|
||||
'slice_id');
|
||||
return { ...state.dashboard, dashboard: newDashboard };
|
||||
},
|
||||
};
|
||||
|
||||
if (action.type in actionHandlers) {
|
||||
return actionHandlers[action.type]();
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export default combineReducers({
|
||||
charts,
|
||||
dashboard,
|
||||
});
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
@@ -21,6 +21,7 @@ const propTypes = {
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.object,
|
||||
PropTypes.bool,
|
||||
PropTypes.array,
|
||||
PropTypes.func]),
|
||||
|
||||
@@ -73,7 +73,7 @@ export default class EmbedCodeButton extends React.Component {
|
||||
<div className="col-md-6 col-sm-12">
|
||||
<div className="form-group">
|
||||
<small>
|
||||
<label className="control-label" htmlFor="embed-height">t('Height')</label>
|
||||
<label className="control-label" htmlFor="embed-height">{t('Height')}</label>
|
||||
</small>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
@@ -87,7 +87,7 @@ export default class EmbedCodeButton extends React.Component {
|
||||
<div className="col-md-6 col-sm-12">
|
||||
<div className="form-group">
|
||||
<small>
|
||||
<label className="control-label" htmlFor="embed-width">t('Width')</label>
|
||||
<label className="control-label" htmlFor="embed-width">{t('Width')}</label>
|
||||
</small>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { chartPropType } from '../../chart/chartReducer';
|
||||
import ExploreActionButtons from './ExploreActionButtons';
|
||||
import EditableTitle from '../../components/EditableTitle';
|
||||
import AlteredSliceTag from '../../components/AlteredSliceTag';
|
||||
import FaveStar from '../../components/FaveStar';
|
||||
import TooltipWrapper from '../../components/TooltipWrapper';
|
||||
import Timer from '../../components/Timer';
|
||||
import { getExploreUrl } from '../exploreUtils';
|
||||
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,
|
||||
can_overwrite: PropTypes.bool.isRequired,
|
||||
can_download: PropTypes.bool.isRequired,
|
||||
isStarred: PropTypes.bool.isRequired,
|
||||
slice: PropTypes.object,
|
||||
table_name: PropTypes.string,
|
||||
form_data: PropTypes.object,
|
||||
timeout: PropTypes.number,
|
||||
chart: PropTypes.shape(chartPropType),
|
||||
};
|
||||
|
||||
class ExploreChartHeader extends React.PureComponent {
|
||||
runQuery() {
|
||||
this.props.actions.runQuery(this.props.form_data, true,
|
||||
this.props.timeout, this.props.chart.chartKey);
|
||||
}
|
||||
|
||||
updateChartTitleOrSaveSlice(newTitle) {
|
||||
const isNewSlice = !this.props.slice;
|
||||
const params = {
|
||||
slice_name: newTitle,
|
||||
action: isNewSlice ? 'saveas' : 'overwrite',
|
||||
};
|
||||
const saveUrl = getExploreUrl(this.props.form_data, '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;
|
||||
}
|
||||
|
||||
render() {
|
||||
const queryResponse = this.props.chart.queryResponse;
|
||||
const data = {
|
||||
csv_endpoint: getExploreUrl(this.props.form_data, 'csv'),
|
||||
json_endpoint: getExploreUrl(this.props.form_data, 'json'),
|
||||
standalone_endpoint: getExploreUrl(this.props.form_data, 'standalone'),
|
||||
};
|
||||
|
||||
return (
|
||||
<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
|
||||
itemId={this.props.slice.slice_id}
|
||||
fetchFaveStar={this.props.actions.fetchFaveStar}
|
||||
saveFaveStar={this.props.actions.saveFaveStar}
|
||||
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>
|
||||
}
|
||||
{this.props.chart.sliceFormData &&
|
||||
<AlteredSliceTag
|
||||
origFormData={this.props.chart.sliceFormData}
|
||||
currentFormData={this.props.form_data}
|
||||
/>
|
||||
}
|
||||
<div className="pull-right">
|
||||
{this.props.chart.chartStatus === 'success' &&
|
||||
queryResponse &&
|
||||
queryResponse.is_cached &&
|
||||
<CachedLabel
|
||||
onClick={this.runQuery.bind(this)}
|
||||
cachedTimestamp={queryResponse.cached_dttm}
|
||||
/>
|
||||
}
|
||||
<Timer
|
||||
startTime={this.props.chart.chartUpdateStartTime}
|
||||
endTime={this.props.chart.chartUpdateEndTime}
|
||||
isRunning={this.props.chart.chartStatus === 'loading'}
|
||||
status={CHART_STATUS_MAP[this.props.chart.chartStatus]}
|
||||
style={{ fontSize: '10px', marginRight: '5px' }}
|
||||
/>
|
||||
<ExploreActionButtons
|
||||
slice={Object.assign({}, this.props.slice, { data })}
|
||||
canDownload={this.props.can_download}
|
||||
chartStatus={this.props.chart.chartStatus}
|
||||
queryResponse={queryResponse}
|
||||
queryEndpoint={getExploreUrl(this.props.form_data, 'query')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ExploreChartHeader.propTypes = propTypes;
|
||||
|
||||
export default ExploreChartHeader;
|
||||
@@ -0,0 +1,85 @@
|
||||
import $ from 'jquery';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Panel } from 'react-bootstrap';
|
||||
|
||||
import { chartPropType } from '../../chart/chartReducer';
|
||||
import ChartContainer from '../../chart/ChartContainer';
|
||||
import ExploreChartHeader from './ExploreChartHeader';
|
||||
|
||||
const propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
can_overwrite: PropTypes.bool.isRequired,
|
||||
can_download: PropTypes.bool.isRequired,
|
||||
datasource: PropTypes.object,
|
||||
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,
|
||||
vizType: PropTypes.string.isRequired,
|
||||
form_data: PropTypes.object,
|
||||
standalone: PropTypes.bool,
|
||||
timeout: PropTypes.number,
|
||||
chart: PropTypes.shape(chartPropType),
|
||||
};
|
||||
|
||||
class ExploreChartPanel extends React.PureComponent {
|
||||
getHeight() {
|
||||
const headerHeight = this.props.standalone ? 0 : 100;
|
||||
return parseInt(this.props.height, 10) - headerHeight;
|
||||
}
|
||||
|
||||
renderChart() {
|
||||
return (
|
||||
<ChartContainer
|
||||
containerId={this.props.containerId}
|
||||
datasource={this.props.datasource}
|
||||
formData={this.props.form_data}
|
||||
height={this.getHeight()}
|
||||
slice={this.props.slice}
|
||||
chartKey={this.props.chart.chartKey}
|
||||
setControlValue={this.props.actions.setControlValue}
|
||||
timeout={this.props.timeout}
|
||||
vizType={this.props.vizType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 header = (
|
||||
<ExploreChartHeader
|
||||
actions={this.props.actions}
|
||||
can_overwrite={this.props.can_overwrite}
|
||||
can_download={this.props.can_download}
|
||||
isStarred={this.props.isStarred}
|
||||
slice={this.props.slice}
|
||||
table_name={this.props.table_name}
|
||||
form_data={this.props.form_data}
|
||||
timeout={this.props.timeout}
|
||||
chart={this.props.chart}
|
||||
/>);
|
||||
return (
|
||||
<div className="chart-container">
|
||||
<Panel
|
||||
style={{ height: this.props.height }}
|
||||
header={header}
|
||||
>
|
||||
{this.renderChart()}
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ExploreChartPanel.propTypes = propTypes;
|
||||
|
||||
export default ExploreChartPanel;
|
||||
@@ -3,27 +3,29 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import ChartContainer from './ChartContainer';
|
||||
|
||||
import ExploreChartPanel from './ExploreChartPanel';
|
||||
import ControlPanelsContainer from './ControlPanelsContainer';
|
||||
import SaveModal from './SaveModal';
|
||||
import QueryAndSaveBtns from './QueryAndSaveBtns';
|
||||
import { getExploreUrl } from '../exploreUtils';
|
||||
import { areObjectsEqual } from '../../reduxUtils';
|
||||
import { getFormDataFromControls } from '../stores/store';
|
||||
import { chartPropType } from '../../chart/chartReducer';
|
||||
import * as exploreActions from '../actions/exploreActions';
|
||||
import * as saveModalActions from '../actions/saveModalActions';
|
||||
import * as chartActions from '../actions/chartActions';
|
||||
import * as chartActions from '../../chart/chartAction';
|
||||
|
||||
const propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
datasource_type: PropTypes.string.isRequired,
|
||||
isDatasourceMetaLoading: PropTypes.bool.isRequired,
|
||||
chartStatus: PropTypes.string,
|
||||
chart: PropTypes.shape(chartPropType).isRequired,
|
||||
controls: PropTypes.object.isRequired,
|
||||
forcedHeight: PropTypes.string,
|
||||
form_data: PropTypes.object.isRequired,
|
||||
standalone: PropTypes.bool.isRequired,
|
||||
triggerQuery: PropTypes.bool.isRequired,
|
||||
queryRequest: PropTypes.object,
|
||||
timeout: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -39,17 +41,22 @@ class ExploreViewContainer extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('resize', this.handleResize.bind(this));
|
||||
this.triggerQueryIfNeeded();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(np) {
|
||||
if (np.controls.viz_type.value !== this.props.controls.viz_type.value) {
|
||||
this.props.actions.resetControls();
|
||||
this.props.actions.triggerQuery();
|
||||
this.props.actions.triggerQuery(true, this.props.chart.chartKey);
|
||||
}
|
||||
if (np.controls.datasource.value !== this.props.controls.datasource.value) {
|
||||
this.props.actions.fetchDatasourceMetadata(np.form_data.datasource, true);
|
||||
}
|
||||
// if any control value changed and it's an instant control
|
||||
if (Object.keys(np.controls).some(key => (np.controls[key].renderTrigger &&
|
||||
typeof this.props.controls[key] !== 'undefined' &&
|
||||
!areObjectsEqual(np.controls[key].value, this.props.controls[key].value)))) {
|
||||
this.props.actions.renderTriggered(new Date().getTime(), this.props.chart.chartKey);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
@@ -63,9 +70,7 @@ class ExploreViewContainer extends React.Component {
|
||||
onQuery() {
|
||||
// remove alerts when query
|
||||
this.props.actions.removeControlPanelAlert();
|
||||
this.props.actions.removeChartAlert();
|
||||
|
||||
this.props.actions.triggerQuery();
|
||||
this.props.actions.triggerQuery(true, this.props.chart.chartKey);
|
||||
|
||||
history.pushState(
|
||||
{},
|
||||
@@ -74,7 +79,7 @@ class ExploreViewContainer extends React.Component {
|
||||
}
|
||||
|
||||
onStop() {
|
||||
this.props.actions.chartUpdateStopped(this.props.queryRequest);
|
||||
this.props.actions.chartUpdateStopped(this.props.chart.queryRequest);
|
||||
}
|
||||
|
||||
getWidth() {
|
||||
@@ -90,8 +95,9 @@ class ExploreViewContainer extends React.Component {
|
||||
}
|
||||
|
||||
triggerQueryIfNeeded() {
|
||||
if (this.props.triggerQuery && !this.hasErrors()) {
|
||||
this.props.actions.runQuery(this.props.form_data, false, this.props.timeout);
|
||||
if (this.props.chart.triggerQuery && !this.hasErrors()) {
|
||||
this.props.actions.runQuery(this.props.form_data, false,
|
||||
this.props.timeout, this.props.chart.chartKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,10 +140,10 @@ class ExploreViewContainer extends React.Component {
|
||||
}
|
||||
renderChartContainer() {
|
||||
return (
|
||||
<ChartContainer
|
||||
actions={this.props.actions}
|
||||
<ExploreChartPanel
|
||||
width={this.state.width}
|
||||
height={this.state.height}
|
||||
{...this.props}
|
||||
/>);
|
||||
}
|
||||
|
||||
@@ -168,7 +174,7 @@ class ExploreViewContainer extends React.Component {
|
||||
onQuery={this.onQuery.bind(this)}
|
||||
onSave={this.toggleModal.bind(this)}
|
||||
onStop={this.onStop.bind(this)}
|
||||
loading={this.props.chartStatus === 'loading'}
|
||||
loading={this.props.chart.chartStatus === 'loading'}
|
||||
errorMessage={this.renderErrorMessage()}
|
||||
/>
|
||||
<br />
|
||||
@@ -191,18 +197,28 @@ class ExploreViewContainer extends React.Component {
|
||||
|
||||
ExploreViewContainer.propTypes = propTypes;
|
||||
|
||||
function mapStateToProps({ explore, chart }) {
|
||||
function mapStateToProps({ explore, charts }) {
|
||||
const form_data = getFormDataFromControls(explore.controls);
|
||||
const chartKey = Object.keys(charts)[0];
|
||||
const chart = charts[chartKey];
|
||||
return {
|
||||
isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
|
||||
datasource: explore.datasource,
|
||||
datasource_type: explore.datasource.type,
|
||||
datasourceId: explore.datasource_id,
|
||||
controls: explore.controls,
|
||||
can_overwrite: !!explore.can_overwrite,
|
||||
can_download: !!explore.can_download,
|
||||
column_formats: explore.datasource ? explore.datasource.column_formats : null,
|
||||
containerId: explore.slice ? `slice-container-${explore.slice.slice_id}` : 'slice-container',
|
||||
isStarred: explore.isStarred,
|
||||
slice: explore.slice,
|
||||
form_data,
|
||||
table_name: form_data.datasource_name,
|
||||
vizType: form_data.viz_type,
|
||||
standalone: explore.standalone,
|
||||
triggerQuery: explore.triggerQuery,
|
||||
forcedHeight: explore.forced_height,
|
||||
queryRequest: chart.queryRequest,
|
||||
chartStatus: chart.chartStatus,
|
||||
chart,
|
||||
timeout: explore.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ const propTypes = {
|
||||
onHide: PropTypes.func.isRequired,
|
||||
actions: PropTypes.object.isRequired,
|
||||
form_data: PropTypes.object,
|
||||
user_id: PropTypes.string.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
dashboards: PropTypes.array.isRequired,
|
||||
alert: PropTypes.string,
|
||||
slice: PropTypes.object,
|
||||
@@ -34,7 +34,7 @@ class SaveModal extends React.Component {
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
this.props.actions.fetchDashboards(this.props.user_id);
|
||||
this.props.actions.fetchDashboards(this.props.userId);
|
||||
}
|
||||
onChange(name, event) {
|
||||
switch (name) {
|
||||
@@ -243,7 +243,7 @@ function mapStateToProps({ explore, saveModal }) {
|
||||
datasource: explore.datasource,
|
||||
slice: explore.slice,
|
||||
can_overwrite: explore.can_overwrite,
|
||||
user_id: explore.user_id,
|
||||
userId: explore.user_id,
|
||||
dashboards: saveModal.dashboards,
|
||||
alert: saveModal.saveModalAlert,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Label, Popover, OverlayTrigger } from 'react-bootstrap';
|
||||
|
||||
import controls from '../../stores/controls';
|
||||
import TextControl from './TextControl';
|
||||
import SelectControl from './SelectControl';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import PopoverSection from '../../../components/PopoverSection';
|
||||
|
||||
const controlTypes = {
|
||||
fixed: 'fix',
|
||||
metric: 'metric',
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.object,
|
||||
isFloat: PropTypes.bool,
|
||||
datasource: PropTypes.object,
|
||||
default: PropTypes.shape({
|
||||
type: PropTypes.oneOf(['fix', 'metric']),
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
}),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
default: { type: controlTypes.fixed, value: 5 },
|
||||
};
|
||||
|
||||
export default class FixedOrMetricControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.setType = this.setType.bind(this);
|
||||
this.setFixedValue = this.setFixedValue.bind(this);
|
||||
this.setMetric = this.setMetric.bind(this);
|
||||
const type = (props.value ? props.value.type : props.default.type) || controlTypes.fixed;
|
||||
const value = (props.value ? props.value.value : props.default.value) || '100';
|
||||
this.state = {
|
||||
type,
|
||||
fixedValue: type === controlTypes.fixed ? value : '',
|
||||
metricValue: type === controlTypes.metric ? value : null,
|
||||
};
|
||||
}
|
||||
onChange() {
|
||||
this.props.onChange({
|
||||
type: this.state.type,
|
||||
value: this.state.type === controlTypes.fixed ?
|
||||
this.state.fixedValue : this.state.metricValue,
|
||||
});
|
||||
}
|
||||
setType(type) {
|
||||
this.setState({ type }, this.onChange);
|
||||
}
|
||||
setFixedValue(fixedValue) {
|
||||
this.setState({ fixedValue }, this.onChange);
|
||||
}
|
||||
setMetric(metricValue) {
|
||||
this.setState({ metricValue }, this.onChange);
|
||||
}
|
||||
renderPopover() {
|
||||
const value = this.props.value || this.props.default;
|
||||
const type = value.type || controlTypes.fixed;
|
||||
const metrics = this.props.datasource ? this.props.datasource.metrics : null;
|
||||
return (
|
||||
<Popover id="filter-popover">
|
||||
<div style={{ width: '240px' }}>
|
||||
<PopoverSection
|
||||
title="Fixed"
|
||||
isSelected={type === controlTypes.fixed}
|
||||
onSelect={() => { this.onChange(controlTypes.fixed); }}
|
||||
>
|
||||
<TextControl
|
||||
isFloat
|
||||
onChange={this.setFixedValue}
|
||||
onFocus={() => { this.setType(controlTypes.fixed); }}
|
||||
value={this.state.fixedValue}
|
||||
/>
|
||||
</PopoverSection>
|
||||
<PopoverSection
|
||||
title="Based on a metric"
|
||||
isSelected={type === controlTypes.metric}
|
||||
onSelect={() => { this.onChange(controlTypes.metric); }}
|
||||
>
|
||||
<SelectControl
|
||||
{...controls.metric}
|
||||
name="metric"
|
||||
options={metrics}
|
||||
onFocus={() => { this.setType(controlTypes.metric); }}
|
||||
onChange={this.setMetric}
|
||||
value={this.state.metricValue}
|
||||
/>
|
||||
</PopoverSection>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...this.props} />
|
||||
<OverlayTrigger
|
||||
container={document.body}
|
||||
trigger="click"
|
||||
rootClose
|
||||
ref="trigger"
|
||||
placement="right"
|
||||
overlay={this.renderPopover()}
|
||||
>
|
||||
<Label style={{ cursor: 'pointer' }}>
|
||||
{this.state.type === controlTypes.fixed &&
|
||||
<span>{this.state.fixedValue}</span>
|
||||
}
|
||||
{this.state.type === controlTypes.metric &&
|
||||
<span>
|
||||
<span style={{ fontWeight: 'normal' }}>metric: </span>
|
||||
<strong>{this.state.metricValue}</strong>
|
||||
</span>
|
||||
}
|
||||
</Label>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FixedOrMetricControl.propTypes = propTypes;
|
||||
FixedOrMetricControl.defaultProps = defaultProps;
|
||||
@@ -1,8 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import Select, { Creatable } from 'react-select';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import { t } from '../../../locales';
|
||||
import VirtualizedRendererWrap from '../../../components/VirtualizedRendererWrap';
|
||||
import OnPasteSelect from '../../../components/OnPasteSelect';
|
||||
|
||||
const propTypes = {
|
||||
choices: PropTypes.array,
|
||||
@@ -14,6 +17,7 @@ const propTypes = {
|
||||
multi: PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]),
|
||||
showHeader: PropTypes.bool,
|
||||
optionRenderer: PropTypes.func,
|
||||
@@ -31,61 +35,13 @@ const defaultProps = {
|
||||
label: null,
|
||||
multi: false,
|
||||
onChange: () => {},
|
||||
onFocus: () => {},
|
||||
showHeader: true,
|
||||
optionRenderer: opt => opt.label,
|
||||
valueRenderer: opt => opt.label,
|
||||
valueKey: 'value',
|
||||
};
|
||||
|
||||
// Handle `onPaste` so that users may paste in
|
||||
// options as comma-delimited, slightly modified from
|
||||
// https://github.com/JedWatson/react-select/issues/1672
|
||||
function pasteSelect(props) {
|
||||
let pasteInput;
|
||||
return (
|
||||
<Select
|
||||
{...props}
|
||||
ref={(ref) => {
|
||||
// Creatable requires a reference to its Select child
|
||||
if (props.ref) {
|
||||
props.ref(ref);
|
||||
}
|
||||
pasteInput = ref;
|
||||
}}
|
||||
inputProps={{
|
||||
onPaste: (evt) => {
|
||||
if (!props.multi) {
|
||||
return;
|
||||
}
|
||||
evt.preventDefault();
|
||||
// pull text from the clipboard and split by comma
|
||||
const clipboard = evt.clipboardData.getData('Text');
|
||||
if (!clipboard) {
|
||||
return;
|
||||
}
|
||||
const values = clipboard.split(/[,]+/).map(v => v.trim());
|
||||
const options = values
|
||||
.filter(value =>
|
||||
// Creatable validates options
|
||||
props.isValidNewOption ? props.isValidNewOption({ label: value }) : !!value,
|
||||
)
|
||||
.map(value => ({
|
||||
[props.labelKey]: value,
|
||||
[props.valueKey]: value,
|
||||
}));
|
||||
if (options.length) {
|
||||
pasteInput.selectValue(options);
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
pasteSelect.propTypes = {
|
||||
multi: PropTypes.bool,
|
||||
ref: PropTypes.func,
|
||||
};
|
||||
|
||||
export default class SelectControl extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -161,23 +117,17 @@ export default class SelectControl extends React.PureComponent {
|
||||
clearable: this.props.clearable,
|
||||
isLoading: this.props.isLoading,
|
||||
onChange: this.onChange,
|
||||
optionRenderer: this.props.optionRenderer,
|
||||
onFocus: this.props.onFocus,
|
||||
optionRenderer: VirtualizedRendererWrap(this.props.optionRenderer),
|
||||
valueRenderer: this.props.valueRenderer,
|
||||
selectComponent: this.props.freeForm ? Creatable : Select,
|
||||
};
|
||||
// Tab, comma or Enter will trigger a new option created for FreeFormSelect
|
||||
const selectWrap = this.props.freeForm ? (
|
||||
<Creatable {...selectProps}>
|
||||
{pasteSelect}
|
||||
</Creatable>
|
||||
) : (
|
||||
pasteSelect(selectProps)
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
{this.props.showHeader &&
|
||||
<ControlHeader {...this.props} />
|
||||
}
|
||||
{selectWrap}
|
||||
<OnPasteSelect {...selectProps} selectWrap={VirtualizedSelect} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,10 +5,8 @@ import * as v from '../../validators';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
|
||||
const propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
@@ -18,9 +16,8 @@ const propTypes = {
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
label: null,
|
||||
description: null,
|
||||
onChange: () => {},
|
||||
onFocus: () => {},
|
||||
value: '',
|
||||
isInt: false,
|
||||
isFloat: false,
|
||||
@@ -64,6 +61,7 @@ export default class TextControl extends React.Component {
|
||||
type="text"
|
||||
placeholder=""
|
||||
onChange={this.onChange}
|
||||
onFocus={this.props.onFocus}
|
||||
value={value}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Label, Popover, OverlayTrigger } from 'react-bootstrap';
|
||||
import { decimal2sexagesimal } from 'geolib';
|
||||
|
||||
import TextControl from './TextControl';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import { defaultViewport } from '../../../modules/geo';
|
||||
|
||||
const PARAMS = [
|
||||
'longitude',
|
||||
'latitude',
|
||||
'zoom',
|
||||
'bearing',
|
||||
'pitch',
|
||||
];
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.shape({
|
||||
longitude: PropTypes.number,
|
||||
latitude: PropTypes.number,
|
||||
zoom: PropTypes.number,
|
||||
bearing: PropTypes.number,
|
||||
pitch: PropTypes.number,
|
||||
}),
|
||||
default: PropTypes.object,
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
default: { type: 'fix', value: 5 },
|
||||
value: defaultViewport,
|
||||
};
|
||||
|
||||
export default class ViewportControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
onChange(ctrl, value) {
|
||||
this.props.onChange({
|
||||
...this.props.value,
|
||||
[ctrl]: value,
|
||||
});
|
||||
}
|
||||
renderTextControl(ctrl) {
|
||||
return (
|
||||
<div key={ctrl}>
|
||||
{ctrl}
|
||||
<TextControl
|
||||
value={this.props.value[ctrl]}
|
||||
onChange={this.onChange.bind(this, ctrl)}
|
||||
isFloat
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
renderPopover() {
|
||||
return (
|
||||
<Popover id={`filter-popover-${this.props.name}`} title="Viewport">
|
||||
{PARAMS.map(ctrl => this.renderTextControl(ctrl))}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
renderLabel() {
|
||||
if (this.props.value.longitude && this.props.value.latitude) {
|
||||
return (
|
||||
decimal2sexagesimal(this.props.value.longitude) +
|
||||
' | ' +
|
||||
decimal2sexagesimal(this.props.value.latitude)
|
||||
);
|
||||
}
|
||||
return 'N/A';
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...this.props} />
|
||||
<OverlayTrigger
|
||||
container={document.body}
|
||||
trigger="click"
|
||||
rootClose
|
||||
ref="trigger"
|
||||
placement="right"
|
||||
overlay={this.renderPopover()}
|
||||
>
|
||||
<Label style={{ cursor: 'pointer' }}>
|
||||
{this.renderLabel()}
|
||||
</Label>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ViewportControl.propTypes = propTypes;
|
||||
ViewportControl.defaultProps = defaultProps;
|
||||
@@ -6,12 +6,14 @@ import ColorSchemeControl from './ColorSchemeControl';
|
||||
import DatasourceControl from './DatasourceControl';
|
||||
import DateFilterControl from './DateFilterControl';
|
||||
import FilterControl from './FilterControl';
|
||||
import FixedOrMetricControl from './FixedOrMetricControl';
|
||||
import HiddenControl from './HiddenControl';
|
||||
import SelectAsyncControl from './SelectAsyncControl';
|
||||
import SelectControl from './SelectControl';
|
||||
import TextAreaControl from './TextAreaControl';
|
||||
import TextControl from './TextControl';
|
||||
import TimeSeriesColumnControl from './TimeSeriesColumnControl';
|
||||
import ViewportControl from './ViewportControl';
|
||||
import VizTypeControl from './VizTypeControl';
|
||||
|
||||
const controlMap = {
|
||||
@@ -23,12 +25,14 @@ const controlMap = {
|
||||
DatasourceControl,
|
||||
DateFilterControl,
|
||||
FilterControl,
|
||||
FixedOrMetricControl,
|
||||
HiddenControl,
|
||||
SelectAsyncControl,
|
||||
SelectControl,
|
||||
TextAreaControl,
|
||||
TextControl,
|
||||
TimeSeriesColumnControl,
|
||||
ViewportControl,
|
||||
VizTypeControl,
|
||||
};
|
||||
export default controlMap;
|
||||
|
||||
@@ -34,19 +34,29 @@ const bootstrappedState = Object.assign(
|
||||
filterColumnOpts: [],
|
||||
isDatasourceMetaLoading: false,
|
||||
isStarred: false,
|
||||
triggerQuery: true,
|
||||
triggerRender: false,
|
||||
},
|
||||
);
|
||||
|
||||
const slice = bootstrappedState.slice;
|
||||
const sliceFormData = slice ?
|
||||
getFormDataFromControls(getControlsState(bootstrapData, slice.form_data))
|
||||
:
|
||||
null;
|
||||
const chartKey = slice ? ('slice_' + slice.slice_id) : 'slice';
|
||||
const initState = {
|
||||
chart: {
|
||||
chartAlert: null,
|
||||
chartStatus: null,
|
||||
chartUpdateEndTime: null,
|
||||
chartUpdateStartTime: now(),
|
||||
latestQueryFormData: getFormDataFromControls(controls),
|
||||
queryResponse: null,
|
||||
charts: {
|
||||
[chartKey]: {
|
||||
chartKey,
|
||||
chartAlert: null,
|
||||
chartStatus: 'loading',
|
||||
chartUpdateEndTime: null,
|
||||
chartUpdateStartTime: now(),
|
||||
latestQueryFormData: getFormDataFromControls(controls),
|
||||
sliceFormData,
|
||||
queryRequest: null,
|
||||
queryResponse: null,
|
||||
triggerQuery: true,
|
||||
lastRendered: 0,
|
||||
},
|
||||
},
|
||||
saveModal: {
|
||||
dashboards: [],
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
/* eslint camelcase: 0 */
|
||||
import { now } from '../../modules/dates';
|
||||
import * as actions from '../actions/chartActions';
|
||||
import { t } from '../../locales';
|
||||
|
||||
export default function chartReducer(state = {}, action) {
|
||||
const actionHandlers = {
|
||||
[actions.CHART_UPDATE_SUCCEEDED]() {
|
||||
return Object.assign(
|
||||
{},
|
||||
state,
|
||||
{
|
||||
chartStatus: 'success',
|
||||
queryResponse: action.queryResponse,
|
||||
},
|
||||
);
|
||||
},
|
||||
[actions.CHART_UPDATE_STARTED]() {
|
||||
return Object.assign({}, state,
|
||||
{
|
||||
chartStatus: 'loading',
|
||||
chartUpdateEndTime: null,
|
||||
chartUpdateStartTime: now(),
|
||||
queryRequest: action.queryRequest,
|
||||
latestQueryFormData: action.latestQueryFormData,
|
||||
});
|
||||
},
|
||||
[actions.CHART_UPDATE_STOPPED]() {
|
||||
return Object.assign({}, state,
|
||||
{
|
||||
chartStatus: 'stopped',
|
||||
chartAlert: t('Updating chart was stopped'),
|
||||
});
|
||||
},
|
||||
[actions.CHART_RENDERING_FAILED]() {
|
||||
return Object.assign({}, state, {
|
||||
chartStatus: 'failed',
|
||||
chartAlert: t('An error occurred while rendering the visualization: %s', action.error),
|
||||
});
|
||||
},
|
||||
[actions.CHART_UPDATE_TIMEOUT]() {
|
||||
return Object.assign({}, state, {
|
||||
chartStatus: 'failed',
|
||||
chartAlert: (
|
||||
'<strong>Query timeout</strong> - visualization query 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 to 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]() {
|
||||
return Object.assign({}, state, {
|
||||
chartStatus: 'failed',
|
||||
chartAlert: action.queryResponse ? action.queryResponse.error : t('Network error.'),
|
||||
chartUpdateEndTime: now(),
|
||||
queryResponse: action.queryResponse,
|
||||
});
|
||||
},
|
||||
[actions.UPDATE_CHART_STATUS]() {
|
||||
const newState = Object.assign({}, state, { chartStatus: action.status });
|
||||
if (action.status === 'success' || action.status === 'failed') {
|
||||
newState.chartUpdateEndTime = now();
|
||||
}
|
||||
return newState;
|
||||
},
|
||||
[actions.REMOVE_CHART_ALERT]() {
|
||||
if (state.chartAlert !== null) {
|
||||
return Object.assign({}, state, { chartAlert: null });
|
||||
}
|
||||
return state;
|
||||
},
|
||||
};
|
||||
|
||||
if (action.type in actionHandlers) {
|
||||
return actionHandlers[action.type]();
|
||||
}
|
||||
return state;
|
||||
}
|
||||
@@ -56,11 +56,6 @@ export default function exploreReducer(state = {}, action) {
|
||||
}
|
||||
return Object.assign({}, state, changes);
|
||||
},
|
||||
[actions.TRIGGER_QUERY]() {
|
||||
return Object.assign({}, state, {
|
||||
triggerQuery: action.value,
|
||||
});
|
||||
},
|
||||
[actions.UPDATE_CHART_TITLE]() {
|
||||
const updatedSlice = Object.assign({}, state.slice, { slice_name: action.slice_name });
|
||||
return Object.assign({}, state, { slice: updatedSlice });
|
||||
@@ -69,9 +64,6 @@ export default function exploreReducer(state = {}, action) {
|
||||
const controls = getControlsState(state, getFormDataFromControls(state.controls));
|
||||
return Object.assign({}, state, { controls });
|
||||
},
|
||||
[actions.RENDER_TRIGGERED]() {
|
||||
return Object.assign({}, state, { triggerRender: false });
|
||||
},
|
||||
[actions.CREATE_NEW_SLICE]() {
|
||||
return Object.assign({}, state, {
|
||||
slice: action.slice,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import chart from './chartReducer';
|
||||
import charts from '../../chart/chartReducer';
|
||||
import saveModal from './saveModalReducer';
|
||||
import explore from './exploreReducer';
|
||||
|
||||
export default combineReducers({
|
||||
chart,
|
||||
charts,
|
||||
saveModal,
|
||||
explore,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { formatSelectOptionsForRange, formatSelectOptions } from '../../modules/utils';
|
||||
import * as v from '../validators';
|
||||
import { ALL_COLOR_SCHEMES, spectrums } from '../../modules/colors';
|
||||
import { colorPrimary, ALL_COLOR_SCHEMES, spectrums } from '../../modules/colors';
|
||||
import { defaultViewport } from '../../modules/geo';
|
||||
import MetricOption from '../../components/MetricOption';
|
||||
import ColumnOption from '../../components/ColumnOption';
|
||||
import OptionDescription from '../../components/OptionDescription';
|
||||
@@ -135,6 +136,14 @@ export const controls = {
|
||||
}),
|
||||
},
|
||||
|
||||
color_picker: {
|
||||
label: t('Fixed Color'),
|
||||
description: t('Use this to define a static color for all circles'),
|
||||
type: 'ColorPickerControl',
|
||||
default: colorPrimary,
|
||||
renderTrigger: true,
|
||||
},
|
||||
|
||||
annotation_layers: {
|
||||
type: 'SelectAsyncControl',
|
||||
multi: true,
|
||||
@@ -424,6 +433,13 @@ export const controls = {
|
||||
},
|
||||
|
||||
groupby: groupByControl,
|
||||
dimension: {
|
||||
...groupByControl,
|
||||
label: t('Dimension'),
|
||||
description: t('Select a dimension'),
|
||||
multi: false,
|
||||
default: null,
|
||||
},
|
||||
|
||||
columns: Object.assign({}, groupByControl, {
|
||||
label: t('Columns'),
|
||||
@@ -441,6 +457,28 @@ export const controls = {
|
||||
}),
|
||||
},
|
||||
|
||||
longitude: {
|
||||
type: 'SelectControl',
|
||||
label: t('Longitude'),
|
||||
default: 1,
|
||||
validators: [v.nonEmpty],
|
||||
description: t('Select the longitude column'),
|
||||
mapStateToProps: state => ({
|
||||
choices: (state.datasource) ? state.datasource.all_cols : [],
|
||||
}),
|
||||
},
|
||||
|
||||
latitude: {
|
||||
type: 'SelectControl',
|
||||
label: t('Latitude'),
|
||||
default: 1,
|
||||
validators: [v.nonEmpty],
|
||||
description: t('Select the latitude column'),
|
||||
mapStateToProps: state => ({
|
||||
choices: (state.datasource) ? state.datasource.all_cols : [],
|
||||
}),
|
||||
},
|
||||
|
||||
all_columns_x: {
|
||||
type: 'SelectControl',
|
||||
label: 'X',
|
||||
@@ -568,17 +606,28 @@ export const controls = {
|
||||
granularity_sqla: {
|
||||
type: 'SelectControl',
|
||||
label: t('Time Column'),
|
||||
default: control =>
|
||||
control.choices && control.choices.length > 0 ? control.choices[0][0] : null,
|
||||
description: t('The time column for the visualization. Note that you ' +
|
||||
'can define arbitrary expression that return a DATETIME ' +
|
||||
'column in the table or. Also note that the ' +
|
||||
'column in the table. Also note that the ' +
|
||||
'filter below is applied against this column or ' +
|
||||
'expression'),
|
||||
mapStateToProps: state => ({
|
||||
choices: (state.datasource) ? state.datasource.granularity_sqla : [],
|
||||
}),
|
||||
freeForm: true,
|
||||
default: (c) => {
|
||||
if (c.options && c.options.length > 0) {
|
||||
return c.options[0].column_name;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
clearable: false,
|
||||
optionRenderer: c => <ColumnOption column={c} />,
|
||||
valueRenderer: c => <ColumnOption column={c} />,
|
||||
valueKey: 'column_name',
|
||||
mapStateToProps: (state) => {
|
||||
const newState = {};
|
||||
if (state.datasource) {
|
||||
newState.options = state.datasource.columns.filter(c => c.is_dttm);
|
||||
}
|
||||
return newState;
|
||||
},
|
||||
},
|
||||
|
||||
time_grain_sqla: {
|
||||
@@ -680,6 +729,7 @@ export const controls = {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Row limit'),
|
||||
validators: [v.integer],
|
||||
default: null,
|
||||
choices: formatSelectOptions(ROW_LIMIT_OPTIONS),
|
||||
},
|
||||
@@ -688,6 +738,7 @@ export const controls = {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Series limit'),
|
||||
validators: [v.integer],
|
||||
choices: formatSelectOptions(SERIES_LIMITS),
|
||||
default: 50,
|
||||
description: t('Limits the number of time series that get displayed'),
|
||||
@@ -719,6 +770,14 @@ export const controls = {
|
||||
'with the [Periods] text box'),
|
||||
},
|
||||
|
||||
multiplier: {
|
||||
type: 'TextControl',
|
||||
label: t('Multiplier'),
|
||||
isFloat: true,
|
||||
default: 1,
|
||||
description: t('Factor to multiply the metric by'),
|
||||
},
|
||||
|
||||
rolling_periods: {
|
||||
type: 'TextControl',
|
||||
label: t('Periods'),
|
||||
@@ -727,6 +786,15 @@ export const controls = {
|
||||
'relative to the time granularity selected'),
|
||||
},
|
||||
|
||||
grid_size: {
|
||||
type: 'TextControl',
|
||||
label: t('Grid Size'),
|
||||
renderTrigger: true,
|
||||
default: 20,
|
||||
isInt: true,
|
||||
description: t('Defines the grid size in pixels'),
|
||||
},
|
||||
|
||||
min_periods: {
|
||||
type: 'TextControl',
|
||||
label: t('Min Periods'),
|
||||
@@ -1032,6 +1100,14 @@ export const controls = {
|
||||
),
|
||||
},
|
||||
|
||||
extruded: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Extruded'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: ('Whether to make the grid 3D'),
|
||||
},
|
||||
|
||||
show_brush: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Range Filter'),
|
||||
@@ -1244,6 +1320,7 @@ export const controls = {
|
||||
mapbox_style: {
|
||||
type: 'SelectControl',
|
||||
label: t('Map Style'),
|
||||
renderTrigger: true,
|
||||
choices: [
|
||||
['mapbox://styles/mapbox/streets-v9', 'Streets'],
|
||||
['mapbox://styles/mapbox/dark-v9', 'Dark'],
|
||||
@@ -1277,6 +1354,15 @@ export const controls = {
|
||||
'number of points (>1000) will cause lag.'),
|
||||
},
|
||||
|
||||
point_radius_fixed: {
|
||||
type: 'FixedOrMetricControl',
|
||||
label: t('Point Size'),
|
||||
description: t('Fixed point radius'),
|
||||
mapStateToProps: state => ({
|
||||
datasource: state.datasource,
|
||||
}),
|
||||
},
|
||||
|
||||
point_radius: {
|
||||
type: 'SelectControl',
|
||||
label: t('Point Radius'),
|
||||
@@ -1297,6 +1383,22 @@ export const controls = {
|
||||
description: t('The unit of measure for the specified point radius'),
|
||||
},
|
||||
|
||||
point_unit: {
|
||||
type: 'SelectControl',
|
||||
label: t('Point Unit'),
|
||||
default: 'square_m',
|
||||
clearable: false,
|
||||
choices: [
|
||||
['square_m', 'Square meters'],
|
||||
['square_km', 'Square kilometers'],
|
||||
['square_miles', 'Square miles'],
|
||||
['radius_m', 'Radius in meters'],
|
||||
['radius_km', 'Radius in kilometers'],
|
||||
['radius_miles', 'Radius in miles'],
|
||||
],
|
||||
description: t('The unit of measure for the specified point radius'),
|
||||
},
|
||||
|
||||
global_opacity: {
|
||||
type: 'TextControl',
|
||||
label: t('Opacity'),
|
||||
@@ -1306,6 +1408,15 @@ export const controls = {
|
||||
'Between 0 and 1.'),
|
||||
},
|
||||
|
||||
viewport: {
|
||||
type: 'ViewportControl',
|
||||
label: t('Viewport'),
|
||||
renderTrigger: true,
|
||||
description: t('Parameters related to the view and perspective on the map'),
|
||||
// default is whole world mostly centered
|
||||
default: defaultViewport,
|
||||
},
|
||||
|
||||
viewport_zoom: {
|
||||
type: 'TextControl',
|
||||
label: t('Zoom'),
|
||||
@@ -1359,6 +1470,7 @@ export const controls = {
|
||||
color: {
|
||||
type: 'ColorPickerControl',
|
||||
label: t('Color'),
|
||||
default: colorPrimary,
|
||||
description: t('Pick a color'),
|
||||
},
|
||||
|
||||
|
||||
@@ -294,6 +294,153 @@ export const visTypes = {
|
||||
},
|
||||
},
|
||||
|
||||
deck_hex: {
|
||||
label: t('Deck.gl - Hexagons'),
|
||||
requiresTime: true,
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['longitude', 'latitude'],
|
||||
['groupby', 'size'],
|
||||
['row_limit'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Map'),
|
||||
controlSetRows: [
|
||||
['mapbox_style', 'viewport'],
|
||||
['color_picker', null],
|
||||
['grid_size', 'extruded'],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
size: {
|
||||
label: t('Height'),
|
||||
description: t('Metric used to control height'),
|
||||
validators: [v.nonEmpty],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
deck_grid: {
|
||||
label: t('Deck.gl - Grid'),
|
||||
requiresTime: true,
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['longitude', 'latitude'],
|
||||
['groupby', 'size'],
|
||||
['row_limit'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Map'),
|
||||
controlSetRows: [
|
||||
['mapbox_style', 'viewport'],
|
||||
['color_picker', null],
|
||||
['grid_size', 'extruded'],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
size: {
|
||||
label: t('Height'),
|
||||
description: t('Metric used to control height'),
|
||||
validators: [v.nonEmpty],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
deck_screengrid: {
|
||||
label: t('Deck.gl - Screen grid'),
|
||||
requiresTime: true,
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['longitude', 'latitude'],
|
||||
['groupby', 'size'],
|
||||
['row_limit'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Map'),
|
||||
controlSetRows: [
|
||||
['mapbox_style', 'viewport'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Grid'),
|
||||
controlSetRows: [
|
||||
['grid_size', 'color_picker'],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
size: {
|
||||
label: t('Weight'),
|
||||
description: t("Metric used as a weight for the grid's coloring"),
|
||||
validators: [v.nonEmpty],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
deck_scatter: {
|
||||
label: t('Deck.gl - Scatter plot'),
|
||||
requiresTime: true,
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['longitude', 'latitude'],
|
||||
['groupby'],
|
||||
['row_limit'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Map'),
|
||||
controlSetRows: [
|
||||
['mapbox_style', 'viewport'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Point Size'),
|
||||
controlSetRows: [
|
||||
['point_radius_fixed', 'point_unit'],
|
||||
['multiplier', null],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Point Color'),
|
||||
controlSetRows: [
|
||||
['color_picker', null],
|
||||
['dimension', 'color_scheme'],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
all_columns_x: {
|
||||
label: t('Longitude Column'),
|
||||
validators: [v.nonEmpty],
|
||||
},
|
||||
all_columns_y: {
|
||||
label: t('Latitude Column'),
|
||||
validators: [v.nonEmpty],
|
||||
},
|
||||
dimension: {
|
||||
label: t('Categorical Color'),
|
||||
description: t('Pick a dimension from which categorical colors are defined'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
area: {
|
||||
label: t('Time Series - Stacked'),
|
||||
requiresTime: true,
|
||||
@@ -463,7 +610,8 @@ export const visTypes = {
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['series', 'metric', 'limit'],
|
||||
['series', 'metric'],
|
||||
['row_limit', null],
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -1062,9 +1210,8 @@ export const visTypes = {
|
||||
{
|
||||
label: t('Viewport'),
|
||||
controlSetRows: [
|
||||
['viewport_longitude'],
|
||||
['viewport_latitude'],
|
||||
['viewport_zoom'],
|
||||
['viewport_longitude', 'viewport_latitude'],
|
||||
['viewport_zoom', null],
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -103,17 +103,36 @@ export const spectrums = {
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a color from a scheme specific palette (scheme)
|
||||
* The function cycles through the palette while memoizing labels
|
||||
* association to colors. If the function is called twice with the
|
||||
* same string, it will return the same color.
|
||||
*
|
||||
* @param {string} s - The label for which we want to get a color
|
||||
* @param {string} scheme - The palette name, or "scheme"
|
||||
* @param {string} forcedColor - A color that the caller wants to
|
||||
forcibly associate to a label.
|
||||
*/
|
||||
export const getColorFromScheme = (function () {
|
||||
// Color factory
|
||||
const seen = {};
|
||||
return function (s, scheme) {
|
||||
const forcedColors = {};
|
||||
return function (s, scheme, forcedColor) {
|
||||
if (!s) {
|
||||
return;
|
||||
}
|
||||
const selectedScheme = scheme ? ALL_COLOR_SCHEMES[scheme] : ALL_COLOR_SCHEMES.bnbColors;
|
||||
let stringifyS = String(s);
|
||||
let stringifyS = String(s).toLowerCase();
|
||||
// next line is for superset series that should have the same color
|
||||
stringifyS = stringifyS.replace('---', '');
|
||||
|
||||
if (forcedColor && !forcedColors[stringifyS]) {
|
||||
forcedColors[stringifyS] = forcedColor;
|
||||
}
|
||||
if (forcedColors[stringifyS]) {
|
||||
return forcedColors[stringifyS];
|
||||
}
|
||||
|
||||
if (seen[selectedScheme] === undefined) {
|
||||
seen[selectedScheme] = {};
|
||||
}
|
||||
@@ -142,3 +161,13 @@ export const colorScalerFactory = function (colors, data, accessor, extents) {
|
||||
const points = colors.map((col, i) => ext[0] + (i * chunkSize));
|
||||
return d3.scale.linear().domain(points).range(colors).clamp(true);
|
||||
};
|
||||
|
||||
export function hexToRGB(hex, alpha = 255) {
|
||||
if (!hex) {
|
||||
return [0, 0, 0, alpha];
|
||||
}
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
return [r, g, b, alpha];
|
||||
}
|
||||
|
||||
25
superset/assets/javascripts/modules/geo.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export const defaultViewport = {
|
||||
longitude: 6.85236157047845,
|
||||
latitude: 31.222656842808707,
|
||||
zoom: 1,
|
||||
bearing: 0,
|
||||
pitch: 0,
|
||||
};
|
||||
|
||||
const METER_TO_MILE = 1609.34;
|
||||
export function unitToRadius(unit, num) {
|
||||
if (unit === 'square_m') {
|
||||
return Math.sqrt(num / Math.PI);
|
||||
} else if (unit === 'radius_m') {
|
||||
return num;
|
||||
} else if (unit === 'radius_km') {
|
||||
return num * 1000;
|
||||
} else if (unit === 'radius_miles') {
|
||||
return num * METER_TO_MILE;
|
||||
} else if (unit === 'square_km') {
|
||||
return Math.sqrt(num / Math.PI) * 1000;
|
||||
} else if (unit === 'square_miles') {
|
||||
return Math.sqrt(num / Math.PI) * METER_TO_MILE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
/* eslint camel-case: 0 */
|
||||
import $ from 'jquery';
|
||||
import Mustache from 'mustache';
|
||||
import vizMap from '../../visualizations/main';
|
||||
import { getExploreUrl } from '../explore/exploreUtils';
|
||||
import { applyDefaultFormData } from '../explore/stores/store';
|
||||
import { t } from '../locales';
|
||||
|
||||
const utils = require('./utils');
|
||||
|
||||
/* eslint wrap-iife: 0 */
|
||||
const px = function (state) {
|
||||
let slice;
|
||||
const timeout = state.common.conf.SUPERSET_WEBSERVER_TIMEOUT;
|
||||
function getParam(name) {
|
||||
/* eslint no-useless-escape: 0 */
|
||||
const formattedName = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
|
||||
const regex = new RegExp('[\\?&]' + formattedName + '=([^&#]*)');
|
||||
const results = regex.exec(location.search);
|
||||
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
|
||||
}
|
||||
function initFavStars() {
|
||||
const baseUrl = '/superset/favstar/';
|
||||
// Init star behavihor for favorite
|
||||
function show() {
|
||||
if ($(this).hasClass('selected')) {
|
||||
$(this).html('<i class="fa fa-star"></i>');
|
||||
} else {
|
||||
$(this).html('<i class="fa fa-star-o"></i>');
|
||||
}
|
||||
}
|
||||
$('.favstar')
|
||||
.attr('title', t('Click to favorite/unfavorite'))
|
||||
.css('cursor', 'pointer')
|
||||
.each(show)
|
||||
.each(function () {
|
||||
let url = baseUrl + $(this).attr('class_name');
|
||||
const star = this;
|
||||
url += '/' + $(this).attr('obj_id') + '/';
|
||||
$.getJSON(url + 'count/', function (data) {
|
||||
if (data.count > 0) {
|
||||
$(star).addClass('selected').each(show);
|
||||
}
|
||||
});
|
||||
})
|
||||
.click(function () {
|
||||
$(this).toggleClass('selected');
|
||||
let url = baseUrl + $(this).attr('class_name');
|
||||
url += '/' + $(this).attr('obj_id') + '/';
|
||||
if ($(this).hasClass('selected')) {
|
||||
url += 'select/';
|
||||
} else {
|
||||
url += 'unselect/';
|
||||
}
|
||||
$.get(url);
|
||||
$(this).each(show);
|
||||
})
|
||||
.tooltip();
|
||||
}
|
||||
const Slice = function (data, datasource, controller) {
|
||||
const token = $('#token_' + data.slice_id);
|
||||
const controls = $('#controls_' + data.slice_id);
|
||||
const containerId = 'con_' + data.slice_id;
|
||||
const selector = '#' + containerId;
|
||||
const container = $(selector);
|
||||
const sliceId = data.slice_id;
|
||||
const formData = applyDefaultFormData(data.form_data);
|
||||
const sliceCell = $(`#${data.slice_id}-cell`);
|
||||
slice = {
|
||||
data,
|
||||
formData,
|
||||
container,
|
||||
containerId,
|
||||
datasource,
|
||||
selector,
|
||||
getWidgetHeader() {
|
||||
return this.container.parents('div.widget').find('.chart-header');
|
||||
},
|
||||
render_template(s) {
|
||||
const context = {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
};
|
||||
return Mustache.render(s, context);
|
||||
},
|
||||
jsonEndpoint(data) {
|
||||
return this.endpoint(data, 'json');
|
||||
},
|
||||
endpoint(data, endpointType = 'json') {
|
||||
let endpoint = getExploreUrl(data, endpointType, this.force);
|
||||
if (endpoint.charAt(0) !== '/') {
|
||||
// Known issue for IE <= 11:
|
||||
// https://connect.microsoft.com/IE/feedbackdetail/view/1002846/pathname-incorrect-for-out-of-document-elements
|
||||
endpoint = '/' + endpoint;
|
||||
}
|
||||
return endpoint;
|
||||
},
|
||||
d3format(col, number) {
|
||||
// uses the utils memoized d3format function and formats based on
|
||||
// column level defined preferences
|
||||
let format = '.3s';
|
||||
if (this.datasource.column_formats[col]) {
|
||||
format = this.datasource.column_formats[col];
|
||||
}
|
||||
return utils.d3format(format, number);
|
||||
},
|
||||
/* eslint no-shadow: 0 */
|
||||
always(data) {
|
||||
if (data && data.query) {
|
||||
slice.viewSqlQuery = data.query;
|
||||
}
|
||||
},
|
||||
done(payload) {
|
||||
Object.assign(data, payload);
|
||||
|
||||
token.find('img.loading').hide();
|
||||
container.fadeTo(0.5, 1);
|
||||
sliceCell.removeClass('slice-cell-highlight');
|
||||
container.show();
|
||||
|
||||
$('.query-and-save button').removeAttr('disabled');
|
||||
this.always(data);
|
||||
controller.done(this);
|
||||
},
|
||||
getErrorMsg(xhr) {
|
||||
let msg = '';
|
||||
if (!xhr.responseText) {
|
||||
const status = xhr.status;
|
||||
if (status === 0) {
|
||||
// This may happen when the worker in gunicorn times out
|
||||
msg += (
|
||||
t('The server could not be reached. You may want to ' +
|
||||
'verify your connection and try again.'));
|
||||
} else {
|
||||
msg += (t('An unknown error occurred. (Status: %s )', status));
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
},
|
||||
error(msg, xhr) {
|
||||
let errorMsg = msg;
|
||||
token.find('img.loading').hide();
|
||||
container.fadeTo(0.5, 1);
|
||||
sliceCell.removeClass('slice-cell-highlight');
|
||||
let errHtml = '';
|
||||
let o;
|
||||
try {
|
||||
o = JSON.parse(msg);
|
||||
if (o.error) {
|
||||
errorMsg = o.error;
|
||||
}
|
||||
} catch (e) {
|
||||
// pass
|
||||
}
|
||||
if (errorMsg) {
|
||||
errHtml += `<div class="alert alert-danger">${errorMsg}</div>`;
|
||||
}
|
||||
if (xhr) {
|
||||
if (xhr.statusText === 'timeout') {
|
||||
errHtml += (
|
||||
'<div class="alert alert-warning">' +
|
||||
'Query timeout - visualization query are set to time out ' +
|
||||
`at ${timeout} seconds.</div>`);
|
||||
} else {
|
||||
const extendedMsg = this.getErrorMsg(xhr);
|
||||
if (extendedMsg) {
|
||||
errHtml += `<div class="alert alert-danger">${extendedMsg}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
container.html(errHtml);
|
||||
container.show();
|
||||
$('span.query').removeClass('disabled');
|
||||
$('.query-and-save button').removeAttr('disabled');
|
||||
this.always(o);
|
||||
controller.error(this);
|
||||
},
|
||||
clearError() {
|
||||
$(selector + ' div.alert').remove();
|
||||
},
|
||||
width() {
|
||||
return container.width();
|
||||
},
|
||||
height() {
|
||||
let others = 0;
|
||||
const widget = container.parents('.widget');
|
||||
const sliceDescription = widget.find('.slice_description');
|
||||
if (sliceDescription.is(':visible')) {
|
||||
others += widget.find('.slice_description').height() + 25;
|
||||
}
|
||||
others += widget.find('.chart-header').height();
|
||||
return widget.height() - others - 10;
|
||||
},
|
||||
bindResizeToWindowResize() {
|
||||
let resizeTimer;
|
||||
const slice = this;
|
||||
$(window).on('resize', function () {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(function () {
|
||||
slice.resize();
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
render(force) {
|
||||
if (force === undefined) {
|
||||
this.force = false;
|
||||
} else {
|
||||
this.force = force;
|
||||
}
|
||||
const formDataExtra = Object.assign({}, formData);
|
||||
formDataExtra.extra_filters = controller.effectiveExtraFilters(sliceId);
|
||||
controls.find('a.exploreChart').attr('href', getExploreUrl(formDataExtra));
|
||||
controls.find('a.exportCSV').attr('href', getExploreUrl(formDataExtra, 'csv'));
|
||||
token.find('img.loading').show();
|
||||
container.fadeTo(0.5, 0.25);
|
||||
sliceCell.addClass('slice-cell-highlight');
|
||||
container.css('height', this.height());
|
||||
$.ajax({
|
||||
url: this.jsonEndpoint(formDataExtra),
|
||||
timeout: timeout * 1000,
|
||||
success: (queryResponse) => {
|
||||
try {
|
||||
vizMap[formData.viz_type](this, queryResponse);
|
||||
this.done(queryResponse);
|
||||
} catch (e) {
|
||||
this.error(t('An error occurred while rendering the visualization: %s', e));
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
this.error(err.responseText, err);
|
||||
},
|
||||
});
|
||||
},
|
||||
resize() {
|
||||
this.render();
|
||||
},
|
||||
addFilter(col, vals, merge = true, refresh = true) {
|
||||
controller.addFilter(sliceId, col, vals, merge, refresh);
|
||||
},
|
||||
setFilter(col, vals, refresh = true) {
|
||||
controller.setFilter(sliceId, col, vals, refresh);
|
||||
},
|
||||
getFilters() {
|
||||
return controller.filters[sliceId];
|
||||
},
|
||||
clearFilter() {
|
||||
controller.clearFilter(sliceId);
|
||||
},
|
||||
removeFilter(col, vals) {
|
||||
controller.removeFilter(sliceId, col, vals);
|
||||
},
|
||||
};
|
||||
return slice;
|
||||
};
|
||||
// Export public functions
|
||||
return {
|
||||
getParam,
|
||||
initFavStars,
|
||||
Slice,
|
||||
};
|
||||
};
|
||||
module.exports = px;
|
||||
@@ -240,3 +240,11 @@ export function tryNumify(s) {
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
export function getParam(name) {
|
||||
/* eslint no-useless-escape: 0 */
|
||||
const formattedName = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
|
||||
const regex = new RegExp('[\\?&]' + formattedName + '=([^&#]*)');
|
||||
const results = regex.exec(location.search);
|
||||
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
|
||||
}
|
||||
|
||||
@@ -19,10 +19,9 @@ export function alterInObject(state, arrKey, obj, alterations) {
|
||||
return Object.assign({}, state, { [arrKey]: newObject });
|
||||
}
|
||||
|
||||
export function alterInArr(state, arrKey, obj, alterations) {
|
||||
export function alterInArr(state, arrKey, obj, alterations, idKey = 'id') {
|
||||
// Finds an item in an array in the state and replaces it with a
|
||||
// new object with an altered property
|
||||
const idKey = 'id';
|
||||
const newArr = [];
|
||||
state[arrKey].forEach((arrItem) => {
|
||||
if (obj[idKey] === arrItem[idKey]) {
|
||||
@@ -96,19 +95,5 @@ export function areArraysShallowEqual(arr1, arr2) {
|
||||
}
|
||||
|
||||
export function areObjectsEqual(obj1, obj2) {
|
||||
if (!obj1 || !obj2) {
|
||||
return false;
|
||||
}
|
||||
if (!Object.keys(obj1).length !== Object.keys(obj2).length) {
|
||||
return false;
|
||||
}
|
||||
for (const id in obj1) {
|
||||
if (!obj2.hasOwnProperty(id)) {
|
||||
return false;
|
||||
}
|
||||
if (obj1[id] !== obj2[id]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return JSON.stringify(obj1) === JSON.stringify(obj2);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superset",
|
||||
"version": "0.20.5",
|
||||
"version": "0.21.0rc1",
|
||||
"description": "Superset is a data exploration platform designed to be visual, intuitive, and interactive.",
|
||||
"license": "Apache-2.0",
|
||||
"directories": {
|
||||
@@ -55,11 +55,14 @@
|
||||
"d3-tip": "^0.6.7",
|
||||
"datamaps": "^0.5.8",
|
||||
"datatables.net-bs": "^1.10.15",
|
||||
"deck.gl": "^4.1.5",
|
||||
"distributions": "^1.0.0",
|
||||
"geolib": "^2.0.24",
|
||||
"immutable": "^3.8.2",
|
||||
"jed": "^1.1.1",
|
||||
"jquery": "3.1.1",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"luma.gl": "^4.0.5",
|
||||
"moment": "2.18.1",
|
||||
"mustache": "^2.2.1",
|
||||
"nvd3": "1.8.6",
|
||||
@@ -96,6 +99,7 @@
|
||||
"srcdoc-polyfill": "^1.0.0",
|
||||
"supercluster": "https://github.com/georgeke/supercluster/tarball/ac3492737e7ce98e07af679623aad452373bbc40",
|
||||
"urijs": "^1.18.10",
|
||||
"underscore": "^1.8.3",
|
||||
"viewport-mercator-project": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -134,10 +138,10 @@
|
||||
"sinon": "^4.0.0",
|
||||
"style-loader": "^0.18.2",
|
||||
"transform-loader": "^0.2.3",
|
||||
"uglifyjs-webpack-plugin": "^0.4.6",
|
||||
"url-loader": "^0.5.7",
|
||||
"webpack": "^3.4.1",
|
||||
"webpack-manifest-plugin": "1.3.1",
|
||||
"webworkify-webpack": "2.0.5"
|
||||
"uglifyjs-webpack-plugin": "^1.1.0",
|
||||
"url-loader": "^0.6.2",
|
||||
"webpack": "^3.8.1",
|
||||
"webpack-manifest-plugin": "1.3.2",
|
||||
"webworkify-webpack": "2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { Table, Thead, Td, Th, Tr } from 'reactable';
|
||||
|
||||
import AlteredSliceTag from '../../../javascripts/components/AlteredSliceTag';
|
||||
import ModalTrigger from '../../../javascripts/components/ModalTrigger';
|
||||
import TooltipWrapper from '../../../javascripts/components/TooltipWrapper';
|
||||
|
||||
const defaultProps = {
|
||||
origFormData: {
|
||||
filters: [{ col: 'a', op: '==', val: 'hello' }],
|
||||
y_axis_bounds: [10, 20],
|
||||
column_collection: [{ 1: 'a', b: ['6', 'g'] }],
|
||||
bool: false,
|
||||
alpha: undefined,
|
||||
gucci: [1, 2, 3, 4],
|
||||
never: 5,
|
||||
ever: { a: 'b', c: 'd' },
|
||||
},
|
||||
currentFormData: {
|
||||
filters: [{ col: 'b', op: 'in', val: ['hello', 'my', 'name'] }],
|
||||
y_axis_bounds: [15, 16],
|
||||
column_collection: [{ 1: 'a', b: [9, '15'], t: 'gggg' }],
|
||||
bool: true,
|
||||
alpha: null,
|
||||
gucci: ['a', 'b', 'c', 'd'],
|
||||
never: 10,
|
||||
ever: { x: 'y', z: 'z' },
|
||||
},
|
||||
};
|
||||
|
||||
const expectedDiffs = {
|
||||
filters: {
|
||||
before: [{ col: 'a', op: '==', val: 'hello' }],
|
||||
after: [{ col: 'b', op: 'in', val: ['hello', 'my', 'name'] }],
|
||||
},
|
||||
y_axis_bounds: {
|
||||
before: [10, 20],
|
||||
after: [15, 16],
|
||||
},
|
||||
column_collection: {
|
||||
before: [{ 1: 'a', b: ['6', 'g'] }],
|
||||
after: [{ 1: 'a', b: [9, '15'], t: 'gggg' }],
|
||||
},
|
||||
bool: {
|
||||
before: false,
|
||||
after: true,
|
||||
},
|
||||
gucci: {
|
||||
before: [1, 2, 3, 4],
|
||||
after: ['a', 'b', 'c', 'd'],
|
||||
},
|
||||
never: {
|
||||
before: 5,
|
||||
after: 10,
|
||||
},
|
||||
ever: {
|
||||
before: { a: 'b', c: 'd' },
|
||||
after: { x: 'y', z: 'z' },
|
||||
},
|
||||
};
|
||||
|
||||
describe('AlteredSliceTag', () => {
|
||||
let wrapper;
|
||||
let props;
|
||||
|
||||
beforeEach(() => {
|
||||
props = Object.assign({}, defaultProps);
|
||||
wrapper = shallow(<AlteredSliceTag {...props} />);
|
||||
});
|
||||
|
||||
it('correctly determines form data differences', () => {
|
||||
const diffs = wrapper.instance().getDiffs(props);
|
||||
expect(diffs).to.deep.equal(expectedDiffs);
|
||||
expect(wrapper.instance().state.diffs).to.deep.equal(expectedDiffs);
|
||||
expect(wrapper.instance().state.hasDiffs).to.equal(true);
|
||||
});
|
||||
|
||||
it('does not run when there are no differences', () => {
|
||||
props = {
|
||||
origFormData: props.origFormData,
|
||||
currentFormData: props.origFormData,
|
||||
};
|
||||
wrapper = shallow(<AlteredSliceTag {...props} />);
|
||||
expect(wrapper.instance().state.diffs).to.deep.equal({});
|
||||
expect(wrapper.instance().state.hasDiffs).to.equal(false);
|
||||
expect(wrapper.instance().render()).to.equal(null);
|
||||
});
|
||||
|
||||
it('sets new diffs when receiving new props', () => {
|
||||
const newProps = {
|
||||
currentFormData: Object.assign({}, props.currentFormData),
|
||||
origFormData: Object.assign({}, props.origFormData),
|
||||
};
|
||||
newProps.currentFormData.beta = 10;
|
||||
wrapper = shallow(<AlteredSliceTag {...props} />);
|
||||
wrapper.instance().componentWillReceiveProps(newProps);
|
||||
const newDiffs = wrapper.instance().state.diffs;
|
||||
const expectedBeta = { before: undefined, after: 10 };
|
||||
expect(newDiffs.beta).to.deep.equal(expectedBeta);
|
||||
});
|
||||
|
||||
it('does not set new state when props are the same', () => {
|
||||
const currentDiff = wrapper.instance().state.diffs;
|
||||
wrapper.instance().componentWillReceiveProps(props);
|
||||
// Check equal references
|
||||
expect(wrapper.instance().state.diffs).to.equal(currentDiff);
|
||||
});
|
||||
|
||||
it('renders a ModalTrigger', () => {
|
||||
expect(wrapper.find(ModalTrigger)).to.have.lengthOf(1);
|
||||
});
|
||||
|
||||
describe('renderTriggerNode', () => {
|
||||
it('renders a TooltipWrapper', () => {
|
||||
const triggerNode = shallow(<div>{wrapper.instance().renderTriggerNode()}</div>);
|
||||
expect(triggerNode.find(TooltipWrapper)).to.have.lengthOf(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderModalBody', () => {
|
||||
it('renders a Table', () => {
|
||||
const modalBody = shallow(<div>{wrapper.instance().renderModalBody()}</div>);
|
||||
expect(modalBody.find(Table)).to.have.lengthOf(1);
|
||||
});
|
||||
|
||||
it('renders a Thead', () => {
|
||||
const modalBody = shallow(<div>{wrapper.instance().renderModalBody()}</div>);
|
||||
expect(modalBody.find(Thead)).to.have.lengthOf(1);
|
||||
});
|
||||
|
||||
it('renders Th', () => {
|
||||
const modalBody = shallow(<div>{wrapper.instance().renderModalBody()}</div>);
|
||||
const th = modalBody.find(Th);
|
||||
expect(th).to.have.lengthOf(3);
|
||||
['control', 'before', 'after'].forEach((v, i) => {
|
||||
expect(th.get(i).props.column).to.equal(v);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the correct number of Tr', () => {
|
||||
const modalBody = shallow(<div>{wrapper.instance().renderModalBody()}</div>);
|
||||
const tr = modalBody.find(Tr);
|
||||
expect(tr).to.have.lengthOf(7);
|
||||
});
|
||||
|
||||
it('renders the correct number of Td', () => {
|
||||
const modalBody = shallow(<div>{wrapper.instance().renderModalBody()}</div>);
|
||||
const td = modalBody.find(Td);
|
||||
expect(td).to.have.lengthOf(21);
|
||||
['control', 'before', 'after'].forEach((v, i) => {
|
||||
expect(td.get(i).props.column).to.equal(v);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderRows', () => {
|
||||
it('returns an array of rows with one Tr and three Td', () => {
|
||||
const rows = wrapper.instance().renderRows();
|
||||
expect(rows).to.have.lengthOf(7);
|
||||
const fakeRow = shallow(<div>{rows[0]}</div>);
|
||||
expect(fakeRow.find(Tr)).to.have.lengthOf(1);
|
||||
expect(fakeRow.find(Td)).to.have.lengthOf(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatValue', () => {
|
||||
it('returns "N/A" for undefined values', () => {
|
||||
expect(wrapper.instance().formatValue(undefined, 'b')).to.equal('N/A');
|
||||
});
|
||||
|
||||
it('returns "null" for null values', () => {
|
||||
expect(wrapper.instance().formatValue(null, 'b')).to.equal('null');
|
||||
});
|
||||
|
||||
it('returns "Max" and "Min" for BoundsControl', () => {
|
||||
expect(wrapper.instance().formatValue([5, 6], 'y_axis_bounds')).to.equal(
|
||||
'Min: 5, Max: 6',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns stringified objects for CollectionControl', () => {
|
||||
const value = [{ 1: 2, alpha: 'bravo' }, { sent: 'imental', w0ke: 5 }];
|
||||
const expected = '{"1":2,"alpha":"bravo"}, {"sent":"imental","w0ke":5}';
|
||||
expect(wrapper.instance().formatValue(value, 'column_collection')).to.equal(expected);
|
||||
});
|
||||
|
||||
it('returns boolean values as string', () => {
|
||||
expect(wrapper.instance().formatValue(true, 'b')).to.equal('true');
|
||||
expect(wrapper.instance().formatValue(false, 'b')).to.equal('false');
|
||||
});
|
||||
|
||||
it('returns Array joined by commas', () => {
|
||||
const value = [5, 6, 7, 8, 'hello', 'goodbye'];
|
||||
const expected = '5, 6, 7, 8, hello, goodbye';
|
||||
expect(wrapper.instance().formatValue(value)).to.equal(expected);
|
||||
});
|
||||
|
||||
it('stringifies objects', () => {
|
||||
const value = { 1: 2, alpha: 'bravo' };
|
||||
const expected = '{"1":2,"alpha":"bravo"}';
|
||||
expect(wrapper.instance().formatValue(value)).to.equal(expected);
|
||||
});
|
||||
|
||||
it('does nothing to strings and numbers', () => {
|
||||
expect(wrapper.instance().formatValue(5)).to.equal(5);
|
||||
expect(wrapper.instance().formatValue('hello')).to.equal('hello');
|
||||
});
|
||||
|
||||
it('returns "[]" for empty filters', () => {
|
||||
expect(wrapper.instance().formatValue([], 'filters')).to.equal('[]');
|
||||
});
|
||||
|
||||
it('correctly formats filters with array values', () => {
|
||||
const filters = [
|
||||
{ col: 'a', op: 'in', val: ['1', 'g', '7', 'ho'] },
|
||||
{ col: 'b', op: 'not in', val: ['hu', 'ho', 'ha'] },
|
||||
];
|
||||
const expected = 'a in [1, g, 7, ho], b not in [hu, ho, ha]';
|
||||
expect(wrapper.instance().formatValue(filters, 'filters')).to.equal(expected);
|
||||
});
|
||||
|
||||
it('correctly formats filters with string values', () => {
|
||||
const filters = [
|
||||
{ col: 'a', op: '==', val: 'gucci' },
|
||||
{ col: 'b', op: 'LIKE', val: 'moshi moshi' },
|
||||
];
|
||||
const expected = 'a == gucci, b LIKE moshi moshi';
|
||||
expect(wrapper.instance().formatValue(filters, 'filters')).to.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
import React from 'react';
|
||||
import sinon from 'sinon';
|
||||
import { expect } from 'chai';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import Select, { Creatable } from 'react-select';
|
||||
|
||||
import OnPasteSelect from '../../../javascripts/components/OnPasteSelect';
|
||||
|
||||
const defaultProps = {
|
||||
onChange: sinon.spy(),
|
||||
multi: true,
|
||||
isValidNewOption: sinon.spy(s => !!s.label),
|
||||
value: [],
|
||||
options: [
|
||||
{ value: 'United States', label: 'United States' },
|
||||
{ value: 'China', label: 'China' },
|
||||
{ value: 'India', label: 'India' },
|
||||
{ value: 'Canada', label: 'Canada' },
|
||||
{ value: 'Russian Federation', label: 'Russian Federation' },
|
||||
{ value: 'Japan', label: 'Japan' },
|
||||
{ value: 'Mexico', label: 'Mexico' },
|
||||
],
|
||||
};
|
||||
|
||||
const defaultEvt = {
|
||||
preventDefault: sinon.spy(),
|
||||
clipboardData: {
|
||||
getData: sinon.spy(() => ' United States, China , India, Canada, '),
|
||||
},
|
||||
};
|
||||
|
||||
describe('OnPasteSelect', () => {
|
||||
let wrapper;
|
||||
let props;
|
||||
let evt;
|
||||
let expected;
|
||||
beforeEach(() => {
|
||||
props = Object.assign({}, defaultProps);
|
||||
wrapper = shallow(<OnPasteSelect {...props} />);
|
||||
evt = Object.assign({}, defaultEvt);
|
||||
});
|
||||
|
||||
it('renders the supplied selectWrap component', () => {
|
||||
const select = wrapper.find(Select);
|
||||
expect(select).to.have.lengthOf(1);
|
||||
});
|
||||
|
||||
it('renders custom selectWrap components', () => {
|
||||
props.selectWrap = Creatable;
|
||||
wrapper = shallow(<OnPasteSelect {...props} />);
|
||||
expect(wrapper.find(Creatable)).to.have.lengthOf(1);
|
||||
props.selectWrap = VirtualizedSelect;
|
||||
wrapper = shallow(<OnPasteSelect {...props} />);
|
||||
expect(wrapper.find(VirtualizedSelect)).to.have.lengthOf(1);
|
||||
});
|
||||
|
||||
describe('onPaste', () => {
|
||||
it('calls onChange with pasted values', () => {
|
||||
wrapper.instance().onPaste(evt);
|
||||
expected = props.options.slice(0, 4);
|
||||
expect(props.onChange.calledWith(expected)).to.be.true;
|
||||
expect(evt.preventDefault.called).to.be.true;
|
||||
expect(props.isValidNewOption.callCount).to.equal(5);
|
||||
});
|
||||
|
||||
it('calls onChange without any duplicate values and adds new values', () => {
|
||||
evt.clipboardData.getData = sinon.spy(() =>
|
||||
'China, China, China, China, Mexico, Mexico, Chi na, Mexico, ',
|
||||
);
|
||||
expected = [
|
||||
props.options[1],
|
||||
props.options[6],
|
||||
{ label: 'Chi na', value: 'Chi na' },
|
||||
];
|
||||
wrapper.instance().onPaste(evt);
|
||||
expect(props.onChange.calledWith(expected)).to.be.true;
|
||||
expect(evt.preventDefault.called).to.be.true;
|
||||
expect(props.isValidNewOption.callCount).to.equal(9);
|
||||
expect(props.options[0].value).to.equal(expected[2].value);
|
||||
props.options.splice(0, 1);
|
||||
});
|
||||
|
||||
it('calls onChange with currently selected values and new values', () => {
|
||||
props.value = ['United States', 'Canada', 'Mexico'];
|
||||
evt.clipboardData.getData = sinon.spy(() =>
|
||||
'United States, Canada, Japan, India',
|
||||
);
|
||||
wrapper = shallow(<OnPasteSelect {...props} />);
|
||||
expected = [
|
||||
props.options[0],
|
||||
props.options[3],
|
||||
props.options[6],
|
||||
props.options[5],
|
||||
props.options[2],
|
||||
];
|
||||
wrapper.instance().onPaste(evt);
|
||||
expect(props.onChange.calledWith(expected)).to.be.true;
|
||||
expect(evt.preventDefault.called).to.be.true;
|
||||
expect(props.isValidNewOption.callCount).to.equal(11);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
import React from 'react';
|
||||
import sinon from 'sinon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import VirtualizedRendererWrap from '../../../javascripts/components/VirtualizedRendererWrap';
|
||||
|
||||
const defaultProps = {
|
||||
focusedOption: { label: 'focusedOn', value: 'focusedOn' },
|
||||
focusOption: sinon.spy(),
|
||||
key: 'key1',
|
||||
option: { label: 'option1', value: 'option1' },
|
||||
selectValue: sinon.spy(),
|
||||
valueArray: [],
|
||||
};
|
||||
|
||||
function TestOption({ option }) {
|
||||
return (
|
||||
<span>{option.label}</span>
|
||||
);
|
||||
}
|
||||
TestOption.propTypes = {
|
||||
option: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const defaultRenderer = opt => <TestOption option={opt} />;
|
||||
const RendererWrap = VirtualizedRendererWrap(defaultRenderer);
|
||||
|
||||
describe('VirtualizedRendererWrap', () => {
|
||||
let wrapper;
|
||||
let props;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<RendererWrap {...defaultProps} />);
|
||||
props = Object.assign({}, defaultProps);
|
||||
});
|
||||
|
||||
it('uses the provided renderer', () => {
|
||||
const option = wrapper.find(TestOption);
|
||||
expect(option).to.have.lengthOf(1);
|
||||
});
|
||||
|
||||
it('renders nothing when no option is provided', () => {
|
||||
props.option = null;
|
||||
wrapper = shallow(<RendererWrap {...props} />);
|
||||
const option = wrapper.find(TestOption);
|
||||
expect(option).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('renders unfocused, unselected options with the default class', () => {
|
||||
const optionDiv = wrapper.find('div');
|
||||
expect(optionDiv).to.have.lengthOf(1);
|
||||
expect(optionDiv.props().className).to.equal('VirtualizedSelectOption');
|
||||
});
|
||||
|
||||
it('renders focused option with the correct class', () => {
|
||||
props.option = props.focusedOption;
|
||||
wrapper = shallow(<RendererWrap {...props} />);
|
||||
const optionDiv = wrapper.find('div');
|
||||
expect(optionDiv.props().className).to.equal(
|
||||
'VirtualizedSelectOption VirtualizedSelectFocusedOption',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders disabled option with the correct class', () => {
|
||||
props.option.disabled = true;
|
||||
wrapper = shallow(<RendererWrap {...props} />);
|
||||
const optionDiv = wrapper.find('div');
|
||||
expect(optionDiv.props().className).to.equal(
|
||||
'VirtualizedSelectOption VirtualizedSelectDisabledOption',
|
||||
);
|
||||
props.option.disabled = false;
|
||||
});
|
||||
|
||||
it('renders selected option with the correct class', () => {
|
||||
props.valueArray = [props.option, props.focusedOption];
|
||||
wrapper = shallow(<RendererWrap {...props} />);
|
||||
const optionDiv = wrapper.find('div');
|
||||
expect(optionDiv.props().className).to.equal(
|
||||
'VirtualizedSelectOption VirtualizedSelectSelectedOption',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders options with custom classes', () => {
|
||||
props.option.className = 'CustomClass';
|
||||
wrapper = shallow(<RendererWrap {...props} />);
|
||||
const optionDiv = wrapper.find('div');
|
||||
expect(optionDiv.props().className).to.equal(
|
||||
'VirtualizedSelectOption CustomClass',
|
||||
);
|
||||
});
|
||||
|
||||
it('calls focusedOption on its own option onMouseEnter', () => {
|
||||
const optionDiv = wrapper.find('div');
|
||||
optionDiv.simulate('mouseEnter');
|
||||
expect(props.focusOption.calledWith(props.option)).to.be.true;
|
||||
});
|
||||
|
||||
it('calls selectValue on its own option onClick', () => {
|
||||
const optionDiv = wrapper.find('div');
|
||||
optionDiv.simulate('click');
|
||||
expect(props.selectValue.calledWith(props.option)).to.be.true;
|
||||
});
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { slice } from './fixtures';
|
||||
|
||||
import SliceCell from '../../../javascripts/dashboard/components/SliceCell';
|
||||
|
||||
describe('SliceCell', () => {
|
||||
const mockedProps = {
|
||||
slice,
|
||||
removeSlice: () => {},
|
||||
expandedSlices: {},
|
||||
};
|
||||
it('is valid', () => {
|
||||
expect(
|
||||
React.isValidElement(<SliceCell {...mockedProps} />),
|
||||
).to.equal(true);
|
||||
});
|
||||
it('renders six links', () => {
|
||||
const wrapper = mount(<SliceCell {...mockedProps} />);
|
||||
expect(wrapper.find('a')).to.have.length(6);
|
||||
});
|
||||
});
|
||||
@@ -66,5 +66,5 @@ export const contextData = {
|
||||
dash_save_perm: true,
|
||||
standalone_mode: false,
|
||||
dash_edit_perm: true,
|
||||
user_id: '1',
|
||||
userId: '1',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
import $ from 'jquery';
|
||||
import * as exploreUtils from '../../../javascripts/explore/exploreUtils';
|
||||
import * as actions from '../../../javascripts/explore/actions/chartActions';
|
||||
import * as actions from '../../../javascripts/chart/chartAction';
|
||||
|
||||
describe('chart actions', () => {
|
||||
let dispatch;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
import React from 'react';
|
||||
import { expect } from 'chai';
|
||||
import { describe, it, beforeEach } from 'mocha';
|
||||
import { shallow } from 'enzyme';
|
||||
import { OverlayTrigger } from 'react-bootstrap';
|
||||
|
||||
import FixedOrMetricControl from
|
||||
'../../../../javascripts/explore/components/controls/FixedOrMetricControl';
|
||||
import SelectControl from
|
||||
'../../../../javascripts/explore/components/controls/SelectControl';
|
||||
import TextControl from
|
||||
'../../../../javascripts/explore/components/controls/TextControl';
|
||||
import ControlHeader from '../../../../javascripts/explore/components/ControlHeader';
|
||||
|
||||
const defaultProps = {
|
||||
value: { },
|
||||
};
|
||||
|
||||
describe('FixedOrMetricControl', () => {
|
||||
let wrapper;
|
||||
let inst;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<FixedOrMetricControl {...defaultProps} />);
|
||||
inst = wrapper.instance();
|
||||
});
|
||||
|
||||
it('renders a OverlayTrigger', () => {
|
||||
const controlHeader = wrapper.find(ControlHeader);
|
||||
expect(controlHeader).to.have.lengthOf(1);
|
||||
expect(wrapper.find(OverlayTrigger)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('renders a TextControl and a SelectControl', () => {
|
||||
const popOver = shallow(inst.renderPopover());
|
||||
expect(popOver.find(TextControl)).to.have.lengthOf(1);
|
||||
expect(popOver.find(SelectControl)).to.have.lengthOf(1);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,13 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
import React from 'react';
|
||||
import Select, { Creatable } from 'react-select';
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import sinon from 'sinon';
|
||||
import { expect } from 'chai';
|
||||
import { describe, it, beforeEach } from 'mocha';
|
||||
import { shallow } from 'enzyme';
|
||||
import OnPasteSelect from '../../../../javascripts/components/OnPasteSelect';
|
||||
import VirtualizedRendererWrap from '../../../../javascripts/components/VirtualizedRendererWrap';
|
||||
import SelectControl from '../../../../javascripts/explore/components/controls/SelectControl';
|
||||
|
||||
const defaultProps = {
|
||||
@@ -26,19 +29,39 @@ describe('SelectControl', () => {
|
||||
wrapper = shallow(<SelectControl {...defaultProps} />);
|
||||
});
|
||||
|
||||
it('renders a Select', () => {
|
||||
expect(wrapper.find(Select)).to.have.lengthOf(1);
|
||||
it('renders an OnPasteSelect', () => {
|
||||
expect(wrapper.find(OnPasteSelect)).to.have.lengthOf(1);
|
||||
});
|
||||
|
||||
it('calls onChange when toggled', () => {
|
||||
const select = wrapper.find(Select);
|
||||
const select = wrapper.find(OnPasteSelect);
|
||||
select.simulate('change', { value: 50 });
|
||||
expect(defaultProps.onChange.calledWith(50)).to.be.true;
|
||||
});
|
||||
|
||||
it('renders a Creatable for freeform', () => {
|
||||
it('passes VirtualizedSelect as selectWrap', () => {
|
||||
const select = wrapper.find(OnPasteSelect);
|
||||
expect(select.props().selectWrap).to.equal(VirtualizedSelect);
|
||||
});
|
||||
|
||||
it('passes Creatable as selectComponent when freeForm=true', () => {
|
||||
wrapper = shallow(<SelectControl {...defaultProps} freeForm />);
|
||||
expect(wrapper.find(Creatable)).to.have.lengthOf(1);
|
||||
const select = wrapper.find(OnPasteSelect);
|
||||
expect(select.props().selectComponent).to.equal(Creatable);
|
||||
});
|
||||
|
||||
it('passes Select as selectComponent when freeForm=false', () => {
|
||||
const select = wrapper.find(OnPasteSelect);
|
||||
expect(select.props().selectComponent).to.equal(Select);
|
||||
});
|
||||
|
||||
it('wraps optionRenderer in a VirtualizedRendererWrap', () => {
|
||||
const select = wrapper.find(OnPasteSelect);
|
||||
const defaultOptionRenderer = SelectControl.defaultProps.optionRenderer;
|
||||
const wrappedRenderer = VirtualizedRendererWrap(defaultOptionRenderer);
|
||||
expect(select.props().optionRenderer).to.be.a('Function');
|
||||
// different instances of wrapper with same inner renderer are unequal
|
||||
expect(select.props().optionRenderer.name).to.equal(wrappedRenderer.name);
|
||||
});
|
||||
|
||||
describe('getOptions', () => {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
import React from 'react';
|
||||
import { expect } from 'chai';
|
||||
import { describe, it, beforeEach } from 'mocha';
|
||||
import { shallow } from 'enzyme';
|
||||
import { OverlayTrigger, Label } from 'react-bootstrap';
|
||||
|
||||
import ViewportControl from
|
||||
'../../../../javascripts/explore/components/controls/ViewportControl';
|
||||
import TextControl from
|
||||
'../../../../javascripts/explore/components/controls/TextControl';
|
||||
import ControlHeader from '../../../../javascripts/explore/components/ControlHeader';
|
||||
|
||||
const defaultProps = {
|
||||
value: {
|
||||
longitude: 6.85236157047845,
|
||||
latitude: 31.222656842808707,
|
||||
zoom: 1,
|
||||
bearing: 0,
|
||||
pitch: 0,
|
||||
},
|
||||
};
|
||||
|
||||
describe('ViewportControl', () => {
|
||||
let wrapper;
|
||||
let inst;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<ViewportControl {...defaultProps} />);
|
||||
inst = wrapper.instance();
|
||||
});
|
||||
|
||||
it('renders a OverlayTrigger', () => {
|
||||
const controlHeader = wrapper.find(ControlHeader);
|
||||
expect(controlHeader).to.have.lengthOf(1);
|
||||
expect(wrapper.find(OverlayTrigger)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('renders a Popover with 5 TextControl', () => {
|
||||
const popOver = shallow(inst.renderPopover());
|
||||
expect(popOver.find(TextControl)).to.have.lengthOf(5);
|
||||
});
|
||||
|
||||
it('renders a summary in the label', () => {
|
||||
expect(wrapper.find(Label).first().render().text()).to.equal('6° 51\' 8.50" | 31° 13\' 21.56"');
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { it, describe } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
import $ from 'jquery';
|
||||
import * as chartActions from '../../../javascripts/chart/chartAction';
|
||||
import * as actions from '../../../javascripts/explore/actions/exploreActions';
|
||||
import { defaultState } from '../../../javascripts/explore/stores/store';
|
||||
import exploreReducer from '../../../javascripts/explore/reducers/exploreReducer';
|
||||
@@ -77,7 +78,7 @@ describe('fetching actions', () => {
|
||||
ajaxStub.yieldsTo('success', { data: '' });
|
||||
makeRequest(true);
|
||||
expect(dispatch.callCount).to.equal(5);
|
||||
expect(dispatch.getCall(4).args[0].type).to.equal(actions.TRIGGER_QUERY);
|
||||
expect(dispatch.getCall(4).args[0].type).to.equal(chartActions.TRIGGER_QUERY);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { it, describe } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { ALL_COLOR_SCHEMES, getColorFromScheme } from '../../../javascripts/modules/colors';
|
||||
import { ALL_COLOR_SCHEMES, getColorFromScheme, hexToRGB } from '../../../javascripts/modules/colors';
|
||||
|
||||
describe('colors', () => {
|
||||
it('default to bnbColors', () => {
|
||||
const color1 = getColorFromScheme('CA');
|
||||
expect(color1).to.equal(ALL_COLOR_SCHEMES.bnbColors[0]);
|
||||
});
|
||||
it('series with same scheme should have the same color', () => {
|
||||
it('getColorFromScheme series with same scheme should have the same color', () => {
|
||||
const color1 = getColorFromScheme('CA', 'bnbColors');
|
||||
const color2 = getColorFromScheme('CA', 'googleCategory20c');
|
||||
const color3 = getColorFromScheme('CA', 'bnbColors');
|
||||
@@ -19,4 +19,28 @@ describe('colors', () => {
|
||||
expect(color1).to.equal(color3);
|
||||
expect(color4).to.equal(ALL_COLOR_SCHEMES.bnbColors[1]);
|
||||
});
|
||||
it('getColorFromScheme forcing colors persists through calls', () => {
|
||||
expect(getColorFromScheme('boys', 'bnbColors', 'blue')).to.equal('blue');
|
||||
expect(getColorFromScheme('boys', 'bnbColors')).to.equal('blue');
|
||||
expect(getColorFromScheme('boys', 'googleCategory20c')).to.equal('blue');
|
||||
|
||||
expect(getColorFromScheme('girls', 'bnbColors', 'pink')).to.equal('pink');
|
||||
expect(getColorFromScheme('girls', 'bnbColors')).to.equal('pink');
|
||||
expect(getColorFromScheme('girls', 'googleCategory20c')).to.equal('pink');
|
||||
});
|
||||
it('getColorFromScheme is not case sensitive', () => {
|
||||
const c1 = getColorFromScheme('girls', 'bnbColors');
|
||||
const c2 = getColorFromScheme('Girls', 'bnbColors');
|
||||
const c3 = getColorFromScheme('GIRLS', 'bnbColors');
|
||||
expect(c1).to.equal(c2);
|
||||
expect(c3).to.equal(c2);
|
||||
});
|
||||
it('hexToRGB converts properly', () => {
|
||||
expect(hexToRGB('#FFFFFF')).to.have.same.members([255, 255, 255, 255]);
|
||||
expect(hexToRGB('#000000')).to.have.same.members([0, 0, 0, 255]);
|
||||
expect(hexToRGB('#FF0000')).to.have.same.members([255, 0, 0, 255]);
|
||||
expect(hexToRGB('#00FF00')).to.have.same.members([0, 255, 0, 255]);
|
||||
expect(hexToRGB('#0000FF')).to.have.same.members([0, 0, 255, 255]);
|
||||
expect(hexToRGB('#FF0000', 128)).to.have.same.members([255, 0, 0, 128]);
|
||||
});
|
||||
});
|
||||
|
||||
27
superset/assets/spec/javascripts/modules/geo_spec.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { it, describe } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { unitToRadius } from '../../../javascripts/modules/geo';
|
||||
|
||||
const METER_TO_MILE = 1609.34;
|
||||
|
||||
describe('unitToRadius', () => {
|
||||
it('converts to square meters', () => {
|
||||
expect(unitToRadius('square_m', 4 * Math.PI)).to.equal(2);
|
||||
});
|
||||
it('converts to square meters', () => {
|
||||
expect(unitToRadius('square_km', 25 * Math.PI)).to.equal(5000);
|
||||
});
|
||||
it('converts to radius meters', () => {
|
||||
expect(unitToRadius('radius_m', 1000)).to.equal(1000);
|
||||
});
|
||||
it('converts to radius km', () => {
|
||||
expect(unitToRadius('radius_km', 1)).to.equal(1000);
|
||||
});
|
||||
it('converts to radius miles', () => {
|
||||
expect(unitToRadius('radius_miles', 1)).to.equal(METER_TO_MILE);
|
||||
});
|
||||
it('converts to square miles', () => {
|
||||
expect(unitToRadius('square_miles', 25 * Math.PI)).to.equal(5000 * (METER_TO_MILE / 1000));
|
||||
});
|
||||
});
|
||||
@@ -18,17 +18,25 @@ div.widget .chart-controls {
|
||||
right: 0;
|
||||
top: 5px;
|
||||
padding: 5px 5px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
div.widget:hover .chart-controls {
|
||||
opacity: 0.75;
|
||||
display: none;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
.slice-grid div.widget {
|
||||
border-radius: 0;
|
||||
border: 0px;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
background-color: #fff;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.slice-grid .slice_container {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.dashboard .slice-grid .dragging,
|
||||
.dashboard .slice-grid .resizing {
|
||||
opacity: 0.5;
|
||||
@@ -84,10 +92,12 @@ div.widget .chart-controls {
|
||||
.slice-cell {
|
||||
box-shadow: 0px 0px 20px 5px rgba(0,0,0,0);
|
||||
transition: box-shadow 1s ease-in;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.slice-cell-highlight {
|
||||
box-shadow: 0px 0px 20px 5px rgba(0,0,0,0.2);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.slice-cell .editable-title input[type="button"] {
|
||||
@@ -95,7 +105,7 @@ div.widget .chart-controls {
|
||||
}
|
||||
|
||||
.dashboard .separator.widget .slice_container {
|
||||
padding: 0px;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
.dashboard .separator.widget .slice_container hr {
|
||||
@@ -116,6 +126,8 @@ div.widget .chart-controls {
|
||||
|
||||
.dashboard .title .favstar {
|
||||
font-size: 20px;
|
||||
position: relative;
|
||||
top: -5px;
|
||||
}
|
||||
|
||||
.chart-header .header {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "~react-select/less/select.less";
|
||||
@select-primary-color: black;
|
||||
@select-input-height: 30px;
|
||||
|
||||
// imports
|
||||
@import "~react-select/less/control.less";
|
||||
@@ -8,6 +9,21 @@
|
||||
@import "~react-select/less/multi.less";
|
||||
@import "~react-select/less/spinner.less";
|
||||
|
||||
.Select--multi {
|
||||
.Select-multi-value-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.Select-value {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.Select-input > input {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.VirtualSelectGrid {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -32,10 +32,6 @@ input.form-control {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
input.form-control.input-sm {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.chart-header a.danger {
|
||||
color: red;
|
||||
}
|
||||
@@ -189,8 +185,23 @@ div.widget .chart-header a {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.widget .slice_container {
|
||||
overflow: hidden;
|
||||
div.widget {
|
||||
.slice_container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stack-trace-container.has-trace {
|
||||
.alert-warning:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.is-loading {
|
||||
.stack-trace-container,
|
||||
.slice_container {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar .alert {
|
||||
@@ -345,22 +356,6 @@ iframe {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
// overwrite react-select css
|
||||
.Select--multi {
|
||||
.Select-multi-value-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.Select-value {
|
||||
height: 23px;
|
||||
}
|
||||
|
||||
.Select-input > input {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.dimmed {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -190,5 +190,30 @@
|
||||
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-3.889312982559205,51.61791610717779],[-3.923054933547974,51.60902786254877],[-3.965831995010319,51.61180496215826],[-3.998054981231689,51.592361450195305],[-3.990278005599918,51.56319427490234],[-4.036943912506103,51.56791687011713],[-4.074166774749641,51.56180572509776],[-4.114721775054818,51.57125091552739],[-4.13972091674799,51.56930541992187],[-4.160276889800969,51.554862976074276],[-4.234723091125431,51.54097366333008],[-4.280834197998047,51.560695648193416],[-4.306387901306096,51.60902786254877],[-4.283055782318115,51.613471984863395],[-4.253056049346924,51.63152694702143],[-4.248611927032471,51.64402770996088],[-4.200833797454834,51.62625122070324],[-4.118611812591553,51.64402770996088],[-4.080276966094971,51.65902709960943],[-4.070831775665283,51.673873901367244],[-4.041944026947021,51.702220916748104],[-4.023056983947754,51.744159698486385],[-3.978055000305175,51.76665878295904],[-3.933089971542301,51.762283325195256],[-3.82989597320551,51.68771362304687],[-3.889312982559205,51.61791610717779]]]},"properties":{"ID_0":242,"ISO":"GB-SWA","NAME_0":"United Kingdom","ID_1":4,"NAME_1":"Wales","ID_2":189,"NAME_2":"Swansea","TYPE_2":"Unitary Authority (wales)","ENGTYPE_2":"Unitary Authority (wales","NL_NAME_2":null,"VARNAME_2":null}},
|
||||
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-3.031853914260864,51.60578918457037],[-3.075934886932373,51.62974166870123],[-3.070498943328857,51.68046569824219],[-3.114710092544499,51.70082473754883],[-3.106729984283447,51.70401763916021],[-3.110353946685791,51.765609741210994],[-3.128468990325871,51.79459381103521],[-3.081367969512825,51.79459381103521],[-3.048760890960637,51.78372192382818],[-3.021589040756226,51.727565765380916],[-2.990792989730835,51.7221298217774],[-2.976300001144352,51.70944976806635],[-2.976300001144352,51.66778564453131],[-2.961265087127629,51.62749099731457],[-3.031853914260864,51.60578918457037]]]},"properties":{"ID_0":242,"ISO":"GB-TOF","NAME_0":"United Kingdom","ID_1":4,"NAME_1":"Wales","ID_2":190,"NAME_2":"Torfaen","TYPE_2":"Unitary Authority (wales)","ENGTYPE_2":"Unitary Authority (wales","NL_NAME_2":null,"VARNAME_2":null}},
|
||||
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-3.179722070693913,51.458801269531364],[-3.164166927337532,51.440139770507805],[-3.169167041778508,51.405693054199276],[-3.189723014831543,51.399307250976506],[-3.22749996185297,51.402362823486385],[-3.290832042694035,51.38569259643555],[-3.310277938842717,51.39236068725585],[-3.340277910232487,51.38069534301769],[-3.40416693687439,51.38069534301769],[-3.520833015441781,51.399581909179744],[-3.560277938842773,51.40124893188482],[-3.578056097030583,51.41977310180675],[-3.525556087493896,51.43582916259771],[-3.517220973968392,51.45000076293945],[-3.523056030273438,51.49361038208019],[-3.475198030471745,51.510440826416016],[-3.442500114440918,51.51583099365246],[-3.392221927642822,51.49610900878912],[-3.372221946716308,51.506938934326286],[-3.291353940963688,51.50046157836925],[-3.260731935501099,51.46937179565441],[-3.200683116912841,51.46776962280279],[-3.179722070693913,51.458801269531364]]]},"properties":{"ID_0":242,"ISO":"GB-VGL","NAME_0":"United Kingdom","ID_1":4,"NAME_1":"Wales","ID_2":191,"NAME_2":"Vale of Glamorgan","TYPE_2":"Unitary Authority (wales)","ENGTYPE_2":"Unitary Authority (wales","NL_NAME_2":null,"VARNAME_2":null}},
|
||||
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-3.140832901000977,52.796390533447266],[-3.190834999084416,52.793331146240234],[-3.242223024368172,52.779159545898494],[-3.299722909927311,52.82693862915045],[-3.371943950653076,52.84999847412121],[-3.390722036361581,52.86741638183594],[-3.373075008392334,52.88907241821289],[-3.311233043670654,52.93287658691406],[-3.200437068939209,52.938030242920036],[-3.094794988632202,52.96379470825207],[-3.141175031661987,52.99729156494135],[-3.132157087325936,53.05913162231457],[-3.122706890106144,53.0664329528808],[-3.10381293296814,53.08103561401373],[-2.991724967956543,53.1390113830567],[-2.956809043884277,53.14467239379883],[-2.93833398818964,53.124721527099666],[-2.895487070083504,53.100749969482536],[-2.864376068115177,53.05360031127941],[-2.860470056533813,53.021930694580185],[-2.827500104904174,53.00222015380871],[-2.812777042388916,52.984161376953125],[-2.7744460105896,52.9849891662597],[-2.727740049362126,52.96660995483404],[-2.716943979263249,52.946109771728516],[-2.71972203254694,52.91722106933593],[-2.75,52.91471862792969],[-2.783334970474129,52.898609161376946],[-2.853610038757324,52.939720153808594],[-2.879168033599854,52.9419403076173],[-2.92833304405201,52.93278121948242],[-2.962729930877686,52.95272827148443],[-2.993890047073364,52.95360946655279],[-3.089446067810059,52.913059234619254],[-3.096668004989624,52.89416885375988],[-3.131666898727417,52.883331298828175],[-3.11916804313654,52.85610961914074],[-3.155833959579468,52.82194137573242],[-3.140832901000977,52.796390533447266]]]},"properties":{"ID_0":242,"ISO":"GB-WRX","NAME_0":"United Kingdom","ID_1":4,"NAME_1":"Wales","ID_2":192,"NAME_2":"Wrexham","TYPE_2":"Unitary Authority (wales)","ENGTYPE_2":"Unitary Authority (wales","NL_NAME_2":null,"VARNAME_2":null}}
|
||||
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-3.140832901000977,52.796390533447266],[-3.190834999084416,52.793331146240234],[-3.242223024368172,52.779159545898494],[-3.299722909927311,52.82693862915045],[-3.371943950653076,52.84999847412121],[-3.390722036361581,52.86741638183594],[-3.373075008392334,52.88907241821289],[-3.311233043670654,52.93287658691406],[-3.200437068939209,52.938030242920036],[-3.094794988632202,52.96379470825207],[-3.141175031661987,52.99729156494135],[-3.132157087325936,53.05913162231457],[-3.122706890106144,53.0664329528808],[-3.10381293296814,53.08103561401373],[-2.991724967956543,53.1390113830567],[-2.956809043884277,53.14467239379883],[-2.93833398818964,53.124721527099666],[-2.895487070083504,53.100749969482536],[-2.864376068115177,53.05360031127941],[-2.860470056533813,53.021930694580185],[-2.827500104904174,53.00222015380871],[-2.812777042388916,52.984161376953125],[-2.7744460105896,52.9849891662597],[-2.727740049362126,52.96660995483404],[-2.716943979263249,52.946109771728516],[-2.71972203254694,52.91722106933593],[-2.75,52.91471862792969],[-2.783334970474129,52.898609161376946],[-2.853610038757324,52.939720153808594],[-2.879168033599854,52.9419403076173],[-2.92833304405201,52.93278121948242],[-2.962729930877686,52.95272827148443],[-2.993890047073364,52.95360946655279],[-3.089446067810059,52.913059234619254],[-3.096668004989624,52.89416885375988],[-3.131666898727417,52.883331298828175],[-3.11916804313654,52.85610961914074],[-3.155833959579468,52.82194137573242],[-3.140832901000977,52.796390533447266]]]},"properties":{"ID_0":242,"ISO":"GB-WRX","NAME_0":"United Kingdom","ID_1":4,"NAME_1":"Wales","ID_2":192,"NAME_2":"Wrexham","TYPE_2":"Unitary Authority (wales)","ENGTYPE_2":"Unitary Authority (wales","NL_NAME_2":null,"VARNAME_2":null}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-NET","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":205,"NAME_2":"Newcastle upon Tyne","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.637893605385297,55.064768597812005],[-1.6385078008313936,55.041551349041356],[-1.5926377415084274,55.038940380402764],[-1.5873825018672498,55.025777798790564],[-1.6002664388791212,55.00983844727313],[-1.5585039788567177,55.00554365830061],[-1.562981060121042,54.992317493771715],[-1.5292129836146306,54.98334130741928],[-1.5355253430199736,54.96523484767873],[-1.5538817384420445,54.95908490640094],[-1.5943326191343197,54.97060544223477],[-1.640073958064805,54.95926565999575],[-1.7003925142994276,54.97078331957437],[-1.719662014574189,54.96748374975912],[-1.747127463447843,54.981765602633345],[-1.766667008399963,54.98443984985363],[-1.728332042694035,55.02804946899419],[-1.697221994400024,55.033050537109375],[-1.688889026641789,55.07221984863287],[-1.637893605385297,55.064768597812005]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-NTY","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":204,"NAME_2":"North Tyneside","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.562981060121042,54.992317493771715],[-1.5585039788567177,55.00554365830061],[-1.6002664388791212,55.00983844727313],[-1.5873825018672498,55.025777798790564],[-1.5926377415084274,55.038940380402764],[-1.6385078008313936,55.041551349041356],[-1.637893605385297,55.064768597812005],[-1.586109042167664,55.06055068969721],[-1.559999942779484,55.05110931396479],[-1.493610978126469,55.04916000366211],[-1.454121947288513,55.07013702392589],[-1.428056001663208,55.03819274902344],[-1.42972195148468,55.007362365722706],[-1.4533035788215456,54.98981534304894],[-1.5308458423819309,54.98401528309465],[-1.5292129836146306,54.98334130741928],[-1.562981060121042,54.992317493771715]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-STY","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":203,"NAME_2":"South Tyneside","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.42972195148468,55.007362365722706],[-1.379166007041931,54.98597335815424],[-1.353055953979379,54.95652770996093],[-1.367498993873483,54.93847274780285],[-1.3696831726000283,54.9424293829535],[-1.4193708269241863,54.92992401027664],[-1.5112175753936958,54.93166591553027],[-1.5153298812543685,54.957349818849906],[-1.5355253430199736,54.96523484767873],[-1.5292129836146306,54.98334130741928],[-1.5308458423819309,54.98401528309465],[-1.4495276527721541,54.9843355871573],[-1.42972195148468,55.007362365722706]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-SND","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":202,"NAME_2":"Sunderland","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.351943969726506,54.86963653564459],[-1.389835000038033,54.860801696777344],[-1.411224007606506,54.80133056640631],[-1.440425038337708,54.79700088500976],[-1.506346940994263,54.7999000549317],[-1.523211956024113,54.831798553466854],[-1.552342057228032,54.85789871215826],[-1.5594108423845006,54.88203709563008],[-1.5570802954893481,54.90921128231478],[-1.5688913696667814,54.92462482411063],[-1.5112175753936958,54.93166591553027],[-1.4193708269241863,54.92992401027664],[-1.3696831726000283,54.9424293829535],[-1.3696850794979791,54.94242762876271],[-1.3756848680534932,54.93690908273037],[-1.367498993873483,54.93847274780285],[-1.350834012031498,54.905418395996094],[-1.360277056693917,54.89014053344732],[-1.351943969726506,54.86963653564459]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-GAT","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":201,"NAME_2":"Gateshead","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.766667008399963,54.98443984985363],[-1.747127463447843,54.981765602633345],[-1.719662014574189,54.96748374975912],[-1.7003925142994276,54.97078331957437],[-1.640073958064805,54.95926565999575],[-1.5943326191343197,54.97060544223477],[-1.5538817384420445,54.95908490640094],[-1.5355253430199736,54.96523484767873],[-1.5153298812543685,54.957349818849906],[-1.5112175753936958,54.93166591553027],[-1.5688913696667814,54.92462482411063],[-1.5570802954893481,54.90921128231478],[-1.5594108423845006,54.88203709563008],[-1.560006022453308,54.879650115966854],[-1.595293998718205,54.89270019531256],[-1.70114004611969,54.894161224365284],[-1.820001006126347,54.90472030639643],[-1.850558042526132,54.91971969604498],[-1.821666955947819,54.92943954467779],[-1.799998998641968,54.97222137451172],[-1.766667008399963,54.98443984985363]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-BRD","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":206,"NAME_2":"Bradford","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.728610038757267,53.90277099609374],[-1.73204879266721,53.89594325075279],[-1.717057547419288,53.89226166106078],[-1.7254307560718554,53.88568922907194],[-1.7563547985969659,53.884706129499996],[-1.7874648701095268,53.896900483232514],[-1.800424229633669,53.88596457577919],[-1.7605110704384566,53.863609042741246],[-1.7152860252485,53.86624455080066],[-1.6950919029119742,53.857537979195925],[-1.7119957303650024,53.78306976757263],[-1.6822705352699179,53.786414904743225],[-1.6742333842008723,53.7800047384966],[-1.640405933268333,53.77968368790602],[-1.6495638555890175,53.76819793719327],[-1.6816208329636275,53.75646888034447],[-1.7144316115903575,53.76245816984497],[-1.7337654265688964,53.74691831169833],[-1.7472250975667662,53.7468142321564],[-1.7459009737585947,53.73448984312321],[-1.760908957841842,53.73464545418409],[-1.7700862781627797,53.72625231008317],[-1.8093678285622556,53.7643806918915],[-1.827836366896612,53.76373265358225],[-1.8554563588416322,53.748307995249604],[-1.8726533729287733,53.75494083924053],[-1.8734027438503054,53.77869041991246],[-1.9281716252138732,53.787577043719274],[-1.9808488762530807,53.786353020276685],[-1.9867652441712251,53.796150574521526],[-2.047224044799748,53.82444000244146],[-2.032510042190552,53.84766006469738],[-2.002779006957951,53.86999893188471],[-1.966109991073608,53.87638854980463],[-1.973610043525696,53.91722106933594],[-1.950834989547729,53.93193817138672],[-1.943611979484501,53.957771301269645],[-1.901944994926396,53.95499038696294],[-1.871762990951538,53.9404411315918],[-1.79778003692627,53.94305038452142],[-1.728610038757267,53.90277099609374]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-CLD","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":207,"NAME_2":"Calderdale","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.7700862781627797,53.72625231008317],[-1.7536367530860233,53.7258603031649],[-1.7358710915125493,53.71324284745348],[-1.7312828421501738,53.680371982778546],[-1.7473017947117206,53.69459457269439],[-1.8257242848934756,53.67025621583839],[-1.8536828281573843,53.672537894106384],[-1.8435663241589404,53.66612189117391],[-1.8945985643971932,53.64542178165289],[-1.9341522381980882,53.648339600553584],[-1.9727437163101147,53.62577347065747],[-2.009470755392109,53.61677953810537],[-1.988055944442749,53.6147193908692],[-2.01694393157959,53.61861038208002],[-2.035001039505005,53.646938323974666],[-2.031388998031616,53.663330078124936],[-2.046390056610051,53.68416976928722],[-2.098612070083618,53.67193984985357],[-2.15134596824646,53.69654846191406],[-2.159722089767456,53.725559234619254],[-2.121388912200871,53.755268096923885],[-2.120558023452759,53.7913818359375],[-2.095555067062378,53.810550689697266],[-2.047224044799748,53.82444000244146],[-2.047224044799748,53.82444000244146],[-1.9867652441712251,53.796150574521526],[-1.9808488762530807,53.786353020276685],[-1.9281716252138732,53.787577043719274],[-1.8734027438503054,53.77869041991246],[-1.8726533729287733,53.75494083924053],[-1.8554563588416322,53.748307995249604],[-1.827836366896612,53.76373265358225],[-1.8093678285622556,53.7643806918915],[-1.7700862781627797,53.72625231008317]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-KIR","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":208,"NAME_2":"Kirklees","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.6816208329636275,53.75646888034447],[-1.6584439961554542,53.74545777049434],[-1.6378819490635026,53.747728913926046],[-1.6233716039931367,53.71854696150013],[-1.5922042738516327,53.71853616246708],[-1.5711222061724999,53.7064046197108],[-1.5983760291819142,53.699780759264726],[-1.6024994122575773,53.692171616634305],[-1.591920090885816,53.689334699229015],[-1.615216853840093,53.67758113210644],[-1.590626057095915,53.66066361221685],[-1.6249014560431192,53.653642139813186],[-1.6136755976408235,53.62457316512705],[-1.5864532989071738,53.60717405390706],[-1.584167957305851,53.59777069091797],[-1.615278005599975,53.56333160400402],[-1.674445986747685,53.54999923706055],[-1.714722990989628,53.55472183227539],[-1.771667957305851,53.53527069091802],[-1.80666601657856,53.531940460205135],[-1.816110968589726,53.51610946655279],[-1.870833039283696,53.532218933105526],[-1.897222995758,53.53305053710949],[-1.937777996063176,53.5680503845216],[-1.97694194316864,53.593891143798885],[-1.988055944442749,53.6147193908692],[-2.01694393157959,53.61861038208002],[-2.009470755392109,53.61677953810537],[-1.9727437163101147,53.62577347065747],[-1.9341522381980882,53.648339600553584],[-1.8945985643971932,53.64542178165289],[-1.8435663241589404,53.66612189117391],[-1.8536828281573843,53.672537894106384],[-1.8257242848934756,53.67025621583839],[-1.7473017947117206,53.69459457269439],[-1.7312828421501738,53.680371982778546],[-1.7358710915125493,53.71324284745348],[-1.7536367530860233,53.7258603031649],[-1.7700862781627797,53.72625231008317],[-1.760908957841842,53.73464545418409],[-1.7459009737585947,53.73448984312321],[-1.7472250975667662,53.7468142321564],[-1.7337654265688964,53.74691831169833],[-1.7144316115903575,53.76245816984497],[-1.6816208329636275,53.75646888034447]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-LDS","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":209,"NAME_2":"Leeds","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.304167032241764,53.749721527099666],[-1.3997217052678315,53.71930941607449],[-1.4431494342181035,53.72822662954728],[-1.4882982958378521,53.72775701029367],[-1.4957279953127665,53.722308403027206],[-1.5103956816858568,53.72969011580311],[-1.5593228953993103,53.69898368127084],[-1.5711222061724999,53.7064046197108],[-1.5922042738516327,53.71853616246708],[-1.6233716039931367,53.71854696150013],[-1.6378819490635026,53.747728913926046],[-1.6584439961554542,53.74545777049434],[-1.6816208329636275,53.75646888034447],[-1.6495638555890175,53.76819793719327],[-1.640405933268333,53.77968368790602],[-1.6742333842008723,53.7800047384966],[-1.6822705352699179,53.786414904743225],[-1.7119957303650024,53.78306976757263],[-1.6950919029119742,53.857537979195925],[-1.7152860252485,53.86624455080066],[-1.7605110704384566,53.863609042741246],[-1.800424229633669,53.88596457577919],[-1.7874648701095268,53.896900483232514],[-1.7563547985969659,53.884706129499996],[-1.7254307560718554,53.88568922907194],[-1.717057547419288,53.89226166106078],[-1.73204879266721,53.89594325075279],[-1.728610038757267,53.90277099609374],[-1.641389966011047,53.907218933105526],[-1.583333015441838,53.900829315185604],[-1.549167037010193,53.90861129760747],[-1.534446001052856,53.93526840209961],[-1.507187962532043,53.91077041625988],[-1.454722046852055,53.90610885620117],[-1.386667013168221,53.9394416809082],[-1.333611011505127,53.94499969482433],[-1.294167995452824,53.9255485534668],[-1.307777047157288,53.89471817016607],[-1.305557012557869,53.860828399658146],[-1.326110005378666,53.846378326416016],[-1.305276989936772,53.819721221923885],[-1.307500958442574,53.79027938842773],[-1.288612008094788,53.77027130126959],[-1.304167032241764,53.749721527099666]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-WKF","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":210,"NAME_2":"Wakefield","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.584167957305851,53.59777069091797],[-1.5864532989071738,53.60717405390706],[-1.6136755976408235,53.62457316512705],[-1.6249014560431192,53.653642139813186],[-1.590626057095915,53.66066361221685],[-1.615216853840093,53.67758113210644],[-1.591920090885816,53.689334699229015],[-1.6024994122575773,53.692171616634305],[-1.5983760291819142,53.699780759264726],[-1.5711222061724999,53.7064046197108],[-1.5593228953993103,53.69898368127084],[-1.5103956816858568,53.72969011580311],[-1.4957279953127665,53.722308403027206],[-1.4882982958378521,53.72775701029367],[-1.4431494342181035,53.72822662954728],[-1.3997217052678315,53.71930941607449],[-1.304167032241764,53.749721527099666],[-1.259443044662419,53.71776962280285],[-1.219980001449585,53.714698791503906],[-1.245278000831547,53.67193984985357],[-1.246947050094548,53.63555145263671],[-1.229943990707341,53.620719909668026],[-1.24333202838892,53.59693908691417],[-1.305276989936772,53.57611083984386],[-1.344444036483708,53.58082962036144],[-1.367776989936829,53.59888076782232],[-1.423889994621277,53.59498977661133],[-1.448055028915405,53.600830078125],[-1.485000014305115,53.59222030639654],[-1.535277962684631,53.593608856201286],[-1.560556054115239,53.60667037963873],[-1.584167957305851,53.59777069091797]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"IM","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"Isle of Man","ID_2":2,"NAME_2":"Isle of Man","TYPE_2":"Crown Dependency","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-4.383544921875,54.34134830540375],[-4.37530517578125,54.36375806691923],[-4.362945556640625,54.39175308509071],[-4.365692138671874,54.41733182694633],[-4.482421875,54.38855462060335],[-4.53460693359375,54.37255855831926],[-4.54833984375,54.35815677227375],[-4.5600128173828125,54.334943271492],[-4.603958129882812,54.279257540609336],[-4.6451568603515625,54.250784674876684],[-4.6904754638671875,54.22470072101279],[-4.7021484375,54.225503550071174],[-4.720001220703125,54.20984556727275],[-4.7234344482421875,54.18373573436089],[-4.7412872314453125,54.17489482345622],[-4.7454071044921875,54.16444403731646],[-4.733734130859375,54.15559900222241],[-4.7385406494140625,54.131868892085414],[-4.750213623046875,54.1069175102521],[-4.7646331787109375,54.11174798232063],[-4.7824859619140625,54.090811871873825],[-4.7632598876953125,54.0900064257852],[-4.7975921630859375,54.063820915086225],[-4.8332977294921875,54.05495438499805],[-4.82025146484375,54.043666972870625],[-4.780426025390625,54.055357450153174],[-4.757080078125,54.0609999517185],[-4.7296142578125,54.08356229415844],[-4.692535400390625,54.081951104880396],[-4.6856689453125,54.067044638606795],[-4.6630096435546875,54.062611954247565],[-4.658203125,54.07187975467914],[-4.648590087890625,54.07590858798479],[-4.6307373046875,54.07631144981169],[-4.622497558593749,54.067044638606795],[-4.6300506591796875,54.05817879674286],[-4.623870849609375,54.05374516606874],[-4.603271484375,54.07550572224815],[-4.6149444580078125,54.07067102844564],[-4.618377685546875,54.0787285386706],[-4.5764923095703125,54.10047600536083],[-4.54833984375,54.10168386374337],[-4.5298004150390625,54.12382170046237],[-4.50439453125,54.12502887884479],[-4.4659423828125,54.142730121693035],[-4.4707489013671875,54.14514334149681],[-4.478302001953125,54.15077364078996],[-4.4769287109375,54.159217654166895],[-4.464569091796875,54.165650031996904],[-4.43572998046875,54.16725797022493],[-4.387664794921875,54.19538677476911],[-4.401397705078125,54.20342006190774],[-4.404144287109375,54.213861000644926],[-4.382171630859375,54.22751055441583],[-4.35333251953125,54.26361995010228],[-4.30389404296875,54.299697745943455],[-4.340972900390625,54.30690951430536],[-4.379425048828125,54.3197273165176],[-4.383544921875,54.34134830540375]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-CHE","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":211,"NAME_2":"Cheshire East","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.503124,53.332322],[-2.447022914886475,53.355045318603516],[-2.42877006530756,53.38039016723633],[-2.364721059799137,53.36249923706055],[-2.316668033599854,53.359989166259766],[-2.28333306312561,53.34222030639659],[-2.238333940505868,53.35499954223644],[-2.176111936569157,53.34555053710949],[-2.159446954727173,53.321388244628906],[-2.140279054641724,53.3277702331543],[-2.119998931884766,53.359439849853516],[-2.080832004547119,53.3611106872558],[-2.049443960189762,53.3522186279298],[-2.016736030578613,53.370479583740234],[-1.998056054115296,53.33583068847667],[-1.995555996894836,53.26416015624999],[-1.990556001663208,53.241661071777344],[-1.970834016799927,53.23027038574224],[-1.976848959922791,53.207370758056754],[-2.005557060241642,53.18610000610357],[-2.030555009841919,53.18471908569335],[-2.06833291053772,53.165828704833984],[-2.140001058578491,53.16999816894537],[-2.134721994399968,53.15027999877935],[-2.157778024673462,53.14749908447271],[-2.20491099357605,53.113666534423885],[-2.243597030639535,53.08589172363286],[-2.272222995757943,53.07749938964849],[-2.305557012557927,53.0791587829591],[-2.33555793762207,53.056110382080185],[-2.36583399772644,53.0522193908692],[-2.37388801574707,53.03722000122081],[-2.366389989852848,53.00027084350586],[-2.378781080245972,52.98960876464855],[-2.423609972000065,52.9786109924317],[-2.430835008621216,52.96500015258788],[-2.478888034820557,52.95415878295904],[-2.512778997421208,52.96165847778332],[-2.527224063873234,52.94721984863287],[-2.57778000831604,52.95360946655279],[-2.589446067810059,52.9769401550294],[-2.622776985168457,52.98582839965826],[-2.727740049362126,52.96660995483404],[-2.69929242541042,52.99543881492112],[-2.668817362661816,53.03865395690339],[-2.7024105948798103,53.0543212271167],[-2.718262793890581,53.04421408238489],[-2.7529286697448767,53.069226255530296],[-2.7317386071692047,53.091808954566744],[-2.71136363193619,53.09364241328605],[-2.706054071937529,53.118509149806194],[-2.671145068527531,53.11587002635502],[-2.6597303498762486,53.130716437310326],[-2.6412634457727053,53.12842106828762],[-2.6252837142956897,53.1508474454638],[-2.5920897823706253,53.14450477640153],[-2.5965047449945127,53.1588720534501],[-2.583580955796339,53.155497891245666],[-2.572402573358417,53.16347223219018],[-2.54292438715405,53.14977217705609],[-2.4997406601267818,53.164151979736],[-2.4688673347879777,53.152736732701385],[-2.443759598733158,53.15988323300474],[-2.4434774505382038,53.1708381853081],[-2.4573741833378864,53.176693897687585],[-2.4565568822113124,53.20261297077961],[-2.4306634912809755,53.197965287645374],[-2.4063665724789765,53.174087431160686],[-2.392109733478435,53.17991522992649],[-2.378319611154369,53.172012897685356],[-2.3687309526100115,53.18293310732565],[-2.372554311462734,53.19558288298865],[-2.4101609019432515,53.205696397148344],[-2.414181807392303,53.21929974563053],[-2.4011722965015694,53.22175880134511],[-2.3962893093078983,53.23436231637357],[-2.363799855536885,53.22357083157431],[-2.3490260583775426,53.249012074682156],[-2.3641523737211614,53.24850109129004],[-2.394115190421391,53.266754280242004],[-2.4144548386760607,53.268319390382516],[-2.4274608589892663,53.2611695571301],[-2.453170662658378,53.28455076075722],[-2.498006104194348,53.289908925446895],[-2.512281155915481,53.32134620700367],[-2.503124,53.332322]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-CHW","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":212,"NAME_2":"Cheshire West and Chester","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.69929242541042,52.99543881492112],[-2.727740049362126,52.96660995483404],[-2.7744460105896,52.9849891662597],[-2.812777042388916,52.984161376953125],[-2.827500104904174,53.00222015380871],[-2.860470056533813,53.021930694580185],[-2.864376068115177,53.05360031127941],[-2.895487070083504,53.100749969482536],[-2.93833398818964,53.124721527099666],[-2.956809043884277,53.14467239379883],[-2.909168004989624,53.171390533447266],[-2.912499904632568,53.18638992309582],[-2.955276966094914,53.215549468994254],[-3.001389026641846,53.23944091796881],[-3.037220954894906,53.25111007690429],[-3.092499971389771,53.25672149658203],[-3.099165916442757,53.285758972168026],[-3.104025388762669,53.28805576260414],[-3.103733121138837,53.29999435427673],[-3.0741654904919797,53.316380908019056],[-3.026139522596778,53.29774869209405],[-2.992737815610769,53.30710878331546],[-2.968033296688178,53.3012551093274],[-2.939551319692313,53.31041563488773],[-2.931602543078512,53.306068832815846],[-2.9287564668151167,53.30841299304941],[-2.9285733193370813,53.30824801712984],[-2.901730978034698,53.29923482940577],[-2.8761936549383713,53.29065064886871],[-2.84558026284004,53.29113403786215],[-2.8126861614917296,53.30481522264111],[-2.7894899824606516,53.296849726505705],[-2.752827376996626,53.31456936820613],[-2.7524892430550585,53.31473267321978],[-2.739292174240041,53.30687033883386],[-2.738836637359983,53.30705152380454],[-2.7382055794543425,53.30669637740584],[-2.7386470041916118,53.307126947182],[-2.7235253728668827,53.31313964458688],[-2.700801145463609,53.30580718868542],[-2.685151721447282,53.31545145878494],[-2.6415569202347107,53.30503479895434],[-2.6450614629819666,53.31013510072038],[-2.624122729072159,53.30939815904222],[-2.619934770655549,53.32032094142578],[-2.609086573898332,53.312071202293055],[-2.595223105393976,53.32245434572998],[-2.580697059631234,53.31558609008789],[-2.55492901802063,53.31236648559575],[-2.447022914886475,53.355045318603516],[-2.503124,53.332322],[-2.512281155915481,53.32134620700367],[-2.498006104194348,53.289908925446895],[-2.453170662658378,53.28455076075722],[-2.4274608589892663,53.2611695571301],[-2.4144548386760607,53.268319390382516],[-2.394115190421391,53.266754280242004],[-2.3641523737211614,53.24850109129004],[-2.3490260583775426,53.249012074682156],[-2.363799855536885,53.22357083157431],[-2.3962893093078983,53.23436231637357],[-2.4011722965015694,53.22175880134511],[-2.414181807392303,53.21929974563053],[-2.4101609019432515,53.205696397148344],[-2.372554311462734,53.19558288298865],[-2.3687309526100115,53.18293310732565],[-2.378319611154369,53.172012897685356],[-2.392109733478435,53.17991522992649],[-2.4063665724789765,53.174087431160686],[-2.4306634912809755,53.197965287645374],[-2.4565568822113124,53.20261297077961],[-2.4573741833378864,53.176693897687585],[-2.4434774505382038,53.1708381853081],[-2.443759598733158,53.15988323300474],[-2.4688673347879777,53.152736732701385],[-2.4997406601267818,53.164151979736],[-2.54292438715405,53.14977217705609],[-2.572402573358417,53.16347223219018],[-2.583580955796339,53.155497891245666],[-2.5965047449945127,53.1588720534501],[-2.5920897823706253,53.14450477640153],[-2.6252837142956897,53.1508474454638],[-2.6412634457727053,53.12842106828762],[-2.6597303498762486,53.130716437310326],[-2.671145068527531,53.11587002635502],[-2.706054071937529,53.118509149806194],[-2.71136363193619,53.09364241328605],[-2.7317386071692047,53.091808954566744],[-2.7529286697448767,53.069226255530296],[-2.718262793890581,53.04421408238489],[-2.7024105948798103,53.0543212271167],[-2.668817362661816,53.03865395690339],[-2.69929242541042,52.99543881492112]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-HAL","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":213,"NAME_2":"Halton","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"MultiPolygon","coordinates":[[[[-2.6906328033643474,53.38538819887531],[-2.680557012557927,53.37887954711914],[-2.662832021713257,53.357460021972656],[-2.605659961700383,53.338134765624936],[-2.580697059631234,53.31558609008789],[-2.595223105393976,53.32245434572998],[-2.609086573898332,53.312071202293055],[-2.619934770655549,53.32032094142578],[-2.624122729072159,53.30939815904222],[-2.6450614629819666,53.31013510072038],[-2.6415569202347107,53.30503479895434],[-2.685151721447282,53.31545145878494],[-2.700801145463609,53.30580718868542],[-2.7235253728668827,53.31313964458688],[-2.7386470041916118,53.307126947182],[-2.7463205093272602,53.31461009870791],[-2.7630580540853917,53.33092174120937],[-2.753249998793975,53.34359955705048],[-2.740202777458212,53.34506489881115],[-2.7381914457377103,53.34793646284791],[-2.7591335731230733,53.34960649072296],[-2.7755257843675882,53.33895375514488],[-2.785584172527674,53.32345942791167],[-2.8221495120534503,53.3334093671374],[-2.8275834556324004,53.33098145503876],[-2.832457303217845,53.33728947774635],[-2.8187798745443087,53.33977154129373],[-2.818806904374221,53.34800073421497],[-2.7873017229574066,53.35629039621995],[-2.7766710534347734,53.38105868293533],[-2.7576540799353046,53.38073789254475],[-2.745174605499206,53.402095648481286],[-2.715226797703995,53.399034871811146],[-2.712803447263301,53.39062599574497],[-2.6906328033643474,53.38538819887531]]],[[[-2.738836637359983,53.30705152380454],[-2.739292174240041,53.30687033883386],[-2.7524892430550585,53.31473267321978],[-2.752205913970762,53.314573312537085],[-2.738836637359983,53.30705152380454]]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-KWL","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":215,"NAME_2":"Knowsley","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.8249644186438823,53.48520925194317],[-2.818711728158975,53.46689107502111],[-2.804407775672048,53.467236409853285],[-2.7950694680621293,53.44323009479305],[-2.8050295780688237,53.43865997711093],[-2.776201389989665,53.42668703835805],[-2.7867759144555664,53.40118756707545],[-2.745174605499206,53.402095648481286],[-2.7576540799353046,53.38073789254475],[-2.7766710534347734,53.38105868293533],[-2.7873017229574066,53.35629039621995],[-2.818806904374221,53.34800073421497],[-2.8404055929822314,53.347331071139905],[-2.853495811111275,53.363514490179924],[-2.8561650149124373,53.378815010693316],[-2.821954837185798,53.38066707898254],[-2.8371937067863002,53.399741433945394],[-2.856219824988291,53.39496011378423],[-2.892411156705693,53.4107621106839],[-2.865927223044769,53.41830162941371],[-2.867855282181373,53.4492945946253],[-2.900315030312638,53.4691586759534],[-2.914377598458968,53.46500517358472],[-2.922615127321468,53.474983221867795],[-2.8879942087273798,53.5038287512821],[-2.863610982894841,53.51805114746094],[-2.805000066757145,53.493610382080135],[-2.8249644186438823,53.48520925194317]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-LIV","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":216,"NAME_2":"Liverpool","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.818806904374221,53.34800073421497],[-2.8187798745443087,53.33977154129373],[-2.832457303217845,53.33728947774635],[-2.8275834556324004,53.33098145503876],[-2.836474654189664,53.3270076975817],[-2.878158399223132,53.334198839003406],[-2.9027093942427484,53.345606530987155],[-2.9817232994180594,53.3822445228796],[-3.0033556769120726,53.41410662121449],[-3.00864346820523,53.43787912516354],[-2.974914555250172,53.443323961595766],[-2.9738232140443714,53.462756641485505],[-2.9563065635473573,53.472993440769805],[-2.922615127321468,53.474983221867795],[-2.914377598458968,53.46500517358472],[-2.900315030312638,53.4691586759534],[-2.867855282181373,53.4492945946253],[-2.865927223044769,53.41830162941371],[-2.892411156705693,53.4107621106839],[-2.856219824988291,53.39496011378423],[-2.8371937067863002,53.399741433945394],[-2.821954837185798,53.38066707898254],[-2.8561650149124373,53.378815010693316],[-2.853495811111275,53.363514490179924],[-2.8404055929822314,53.347331071139905],[-2.818806904374221,53.34800073421497]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-SHN","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":217,"NAME_2":"St. Helens","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.6906328033643474,53.38538819887531],[-2.712803447263301,53.39062599574497],[-2.715226797703995,53.399034871811146],[-2.745174605499206,53.402095648481286],[-2.7867759144555664,53.40118756707545],[-2.776201389989665,53.42668703835805],[-2.8050295780688237,53.43865997711093],[-2.7950694680621293,53.44323009479305],[-2.804407775672048,53.467236409853285],[-2.818711728158975,53.46689107502111],[-2.8249644186438823,53.48520925194317],[-2.805000066757145,53.493610382080135],[-2.801666021346989,53.51694107055658],[-2.776109933853149,53.5288810729981],[-2.751987934112549,53.51115036010747],[-2.690000057220345,53.47166061401373],[-2.603888988494873,53.47249984741205],[-2.59972095489502,53.458328247070305],[-2.566404104232788,53.43566894531255],[-2.616111993789559,53.43054962158208],[-2.639722108840942,53.44055175781244],[-2.670834064483643,53.431110382080014],[-2.648056983947754,53.391391754150504],[-2.680557012557927,53.37887954711914],[-2.6906328033643474,53.38538819887531]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-SFT","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":218,"NAME_2":"Sefton","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.863610982894841,53.51805114746094],[-2.8879942087273798,53.5038287512821],[-2.922615127321468,53.474983221867795],[-2.9563065635473573,53.472993440769805],[-2.9738232140443714,53.462756641485505],[-2.974914555250172,53.443323961595766],[-3.00864346820523,53.43787912516354],[-3.008755289200723,53.43838154498731],[-3.040833950042668,53.4651374816895],[-3.06138896942133,53.501251220703125],[-3.06361198425293,53.5223617553712],[-3.1002779006958,53.54291534423834],[-3.1002779006958,53.56652832031256],[-3.051388978958016,53.62236022949219],[-3.01916599273676,53.65124893188476],[-2.99083399772644,53.66736221313482],[-2.972501039504948,53.69402694702154],[-2.947770118713265,53.70819473266613],[-2.932777881622258,53.66054916381847],[-2.963609933853149,53.6261100769043],[-2.996387958526554,53.613609313964844],[-3.018887996673527,53.593608856201286],[-3.011667966842651,53.57749938964837],[-3.030555009841805,53.56026840209961],[-3.031666994094849,53.54193878173834],[-2.958611965179443,53.51583099365239],[-2.954444885253906,53.54582977294933],[-2.910278081893864,53.52444076538086],[-2.863610982894841,53.51805114746094]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-WRL","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":219,"NAME_2":"Wirral","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.9287564668151167,53.30841299304941],[-2.931602543078512,53.306068832815846],[-2.939551319692313,53.31041563488773],[-2.968033296688178,53.3012551093274],[-2.992737815610769,53.30710878331546],[-3.026139522596778,53.29774869209405],[-3.0741654904919797,53.316380908019056],[-3.103733121138837,53.29999435427673],[-3.1037322636345945,53.30002934804953],[-3.1090147166578257,53.29725750745639],[-3.1166343558439107,53.31171865570986],[-3.122336556752299,53.32253388267948],[-3.186277019464458,53.36444603100735],[-3.2003693343126156,53.38751432995951],[-3.1736326305871425,53.40143583086426],[-3.0403203214367336,53.44289654953395],[-3.01645759994499,53.410577110896554],[-3.002522132920189,53.37473245214664],[-2.934169251041866,53.313288041250125],[-2.9287564668151167,53.30841299304941]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-BIR","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":220,"NAME_2":"Birmingham","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.7535232040259174,52.51296690956616],[-1.7994465715786179,52.50429325232655],[-1.7557828743406476,52.49948012688693],[-1.759600499826434,52.45191093161349],[-1.779146248816952,52.450154615190755],[-1.8003174036920766,52.45829756060253],[-1.8346435093023545,52.4174332370946],[-1.8444961945912532,52.410252604968385],[-1.8666451481106627,52.41104531977225],[-1.8687468031867753,52.404737506339956],[-1.845498272804539,52.39978223961458],[-1.844444990158081,52.383609771728516],[-1.90055501461029,52.39278030395508],[-1.954723000526428,52.38694000244135],[-1.983054995536747,52.37749099731439],[-2.001945018768254,52.39278030395508],[-1.983054995536747,52.41777038574224],[-2.016990282419047,52.4326829049244],[-2.013243469381798,52.4621907598166],[-1.9778967430437608,52.46716543160672],[-1.964006976484736,52.481985666946336],[-1.9507770607038752,52.48324663801256],[-1.9381321414490529,52.49842476289323],[-1.9630249126644133,52.50489727915291],[-1.9619814538100087,52.5284156793635],[-1.9294410516361902,52.53129919343769],[-1.9331498676191794,52.54581281031442],[-1.9181574543575888,52.547306508689616],[-1.878715984113474,52.56943440492526],[-1.872564469611435,52.584944671524696],[-1.875277042388859,52.58639144897461],[-1.855556011199895,52.57555007934576],[-1.815001010894719,52.590831756591854],[-1.777673006057739,52.575420379638786],[-1.753056049346867,52.54666137695318],[-1.733332037925663,52.5116691589356],[-1.7535232040259174,52.51296690956616]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-COV","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":221,"NAME_2":"Coventry","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.6010673268718723,52.38929945930054],[-1.6021755434992606,52.41605700579916],[-1.6144499964861112,52.42796797184463],[-1.6082707976447488,52.438841008005525],[-1.5952286648660792,52.439926413404116],[-1.5952286648660792,52.442926413404116],[-1.455000042915287,52.43804931640619],[-1.438053965568486,52.429439544677734],[-1.431666016578617,52.383609771728516],[-1.46277904510498,52.36582946777343],[-1.510833978652954,52.36748886108404],[-1.536944031715336,52.35583114624029],[-1.562777996063176,52.37221908569335],[-1.5676068988662508,52.384688959934735],[-1.6010673268718723,52.38929945930054]]]}},{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-DUD","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":222,"NAME_2":"Dudley","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.073942090146734,52.5498796695657],[-2.0795904608909996,52.524084766328386],[-2.057257620053761,52.51227148967935],[-2.0643229399889718,52.48716900687397],[-2.097090363561211,52.46839477878771],[-2.0590167147981466,52.46197535251913],[-2.0227492132989906,52.48055141631847],[-2.013243469381798,52.4621907598166],[-2.016990282419047,52.4326829049244],[-2.030555009841919,52.43693923950201],[-2.058332920074463,52.42248916625988],[-2.082776069641056,52.436100006103516],[-2.118889093399048,52.42221832275385],[-2.153856039047241,52.423000335693416],[-2.158890008926335,52.47748947143555],[-2.177500009536743,52.5011100769043],[-2.134443998336735,52.5152702331543],[-2.12611198425293,52.544441223144645],[-2.1214536447560484,52.55693494609824],[-2.108244358337544,52.543944892365815],[-2.0797787067929865,52.55702729259615],[-2.073942090146734,52.5498796695657]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-SAW","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":223,"NAME_2":"Sandwell","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.9181574543575888,52.547306508689616],[-1.9331498676191794,52.54581281031442],[-1.9294410516361902,52.53129919343769],[-1.9619814538100087,52.5284156793635],[-1.9630249126644133,52.50489727915291],[-1.9381321414490529,52.49842476289323],[-1.9507770607038752,52.48324663801256],[-1.964006976484736,52.481985666946336],[-1.9778967430437608,52.46716543160672],[-2.013243469381798,52.4621907598166],[-2.0227492132989906,52.48055141631847],[-2.0590167147981466,52.46197535251913],[-2.097090363561211,52.46839477878771],[-2.0643229399889718,52.48716900687397],[-2.057257620053761,52.51227148967935],[-2.0795904608909996,52.524084766328386],[-2.073942090146734,52.5498796695657],[-2.050982445654555,52.55272911259622],[-2.010978452375163,52.56906533086296],[-1.975496610455888,52.55556040195912],[-1.9641434593340144,52.5632268063678],[-1.9510685945348045,52.55683306471927],[-1.9339272996596213,52.560034196862155],[-1.9181574543575888,52.547306508689616]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-SOL","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":224,"NAME_2":"Solihull","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.5952286648660792,52.442926413404116],[-1.5952286648660792,52.439926413404116],[-1.6082707976447488,52.438841008005525],[-1.6144499964861112,52.42796797184463],[-1.6021755434992606,52.41605700579916],[-1.6010673268718723,52.38929945930054],[-1.5676068988662508,52.384688959934735],[-1.562777996063176,52.37221908569335],[-1.682500958442688,52.34249877929682],[-1.714722990989628,52.35527038574213],[-1.751667976379281,52.34749984741222],[-1.79694497585291,52.35221862792963],[-1.82249903678894,52.3763885498048],[-1.844444990158081,52.383609771728516],[-1.845498272804539,52.39978223961458],[-1.8687468031867753,52.404737506339956],[-1.8666451481106627,52.41104531977225],[-1.8444961945912532,52.410252604968385],[-1.8346435093023545,52.4174332370946],[-1.8003174036920766,52.45829756060253],[-1.779146248816952,52.450154615190755],[-1.759600499826434,52.45191093161349],[-1.7557828743406476,52.49948012688693],[-1.7994465715786179,52.50429325232655],[-1.7535232040259174,52.51296690956616],[-1.733332037925663,52.5116691589356],[-1.741111040115356,52.49750137329101],[-1.682500958442688,52.43972015380871],[-1.661666989326477,52.42472076416015],[-1.619444012641793,52.444438934326115],[-1.5952286648660792,52.442926413404116]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-WLL","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":225,"NAME_2":"Walsall","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.872564469611435,52.584944671524696],[-1.878715984113474,52.56943440492526],[-1.9181574543575888,52.547306508689616],[-1.9339272996596213,52.560034196862155],[-1.9510685945348045,52.55683306471927],[-1.9641434593340144,52.5632268063678],[-1.975496610455888,52.55556040195912],[-2.010978452375163,52.56906533086296],[-2.050982445654555,52.55272911259622],[-2.061952548381922,52.55824902064915],[-2.0500221605804145,52.57214052345834],[-2.077823919994913,52.58606048217102],[-2.054599038491829,52.60084758246004],[-2.066389083862248,52.60805892944347],[-2.025834083557072,52.61610031127924],[-1.952499032020512,52.637500762939446],[-1.934445977210999,52.66527938842785],[-1.897222995758,52.649440765380966],[-1.896389007568303,52.617488861083984],[-1.875277042388859,52.58639144897461],[-1.855556011199895,52.57555007934576],[-1.872564469611435,52.584944671524696]]]}},
|
||||
{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-WLV","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":226,"NAME_2":"Wolverhampton","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.1214536447560484,52.55693494609824],[-2.12611198425293,52.544441223144645],[-2.157500028610229,52.54639053344732],[-2.174444913864136,52.567501068115234],[-2.200557947158757,52.579441070556754],[-2.180556058883667,52.599998474121094],[-2.144166946411132,52.61278152465826],[-2.131109952926579,52.629440307617244],[-2.097500085830575,52.63415908813482],[-2.066389083862248,52.60805892944347],[-2.054599038491829,52.60084758246004],[-2.077823919994913,52.58606048217102],[-2.0500221605804145,52.57214052345834],[-2.061952548381922,52.55824902064915],[-2.050982445654555,52.55272911259622],[-2.073942090146734,52.5498796695657],[-2.0797787067929865,52.55702729259615],[-2.108244358337544,52.543944892365815],[-2.1214536447560484,52.55693494609824]]]}}
|
||||
]}
|
||||
87
superset/assets/visualizations/deckgl/DeckGLContainer.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MapGL from 'react-map-gl';
|
||||
import DeckGL from 'deck.gl';
|
||||
|
||||
const propTypes = {
|
||||
viewport: PropTypes.object.isRequired,
|
||||
layers: PropTypes.array.isRequired,
|
||||
setControlValue: PropTypes.func.isRequired,
|
||||
mapStyle: PropTypes.string,
|
||||
mapboxApiAccessToken: PropTypes.string.isRequired,
|
||||
onViewportChange: PropTypes.func,
|
||||
};
|
||||
const defaultProps = {
|
||||
mapStyle: 'light',
|
||||
onViewportChange: () => {},
|
||||
};
|
||||
|
||||
export default class DeckGLContainer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
viewport: props.viewport,
|
||||
};
|
||||
this.tick = this.tick.bind(this);
|
||||
this.onViewportChange = this.onViewportChange.bind(this);
|
||||
}
|
||||
componentWillMount() {
|
||||
const timer = setInterval(this.tick, 1000);
|
||||
this.setState(() => ({ timer }));
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.setState(() => ({
|
||||
viewport: { ...nextProps.viewport },
|
||||
}));
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.clearInterval(this.state.timer);
|
||||
}
|
||||
onViewportChange(viewport) {
|
||||
const vp = Object.assign({}, viewport);
|
||||
delete vp.width;
|
||||
delete vp.height;
|
||||
const newVp = { ...this.state.viewport, ...vp };
|
||||
|
||||
this.setState(() => ({ viewport: newVp }));
|
||||
this.props.onViewportChange(newVp);
|
||||
}
|
||||
tick() {
|
||||
// Limiting updating viewport controls through Redux at most 1*sec
|
||||
if (this.state.previousViewport !== this.state.viewport) {
|
||||
const setCV = this.props.setControlValue;
|
||||
const vp = this.state.viewport;
|
||||
if (setCV) {
|
||||
setCV('viewport', vp);
|
||||
}
|
||||
this.setState(() => ({ previousViewport: this.state.viewport }));
|
||||
}
|
||||
}
|
||||
layers() {
|
||||
// Support for layer factory
|
||||
if (this.props.layers.some(l => typeof l === 'function')) {
|
||||
return this.props.layers.map(l => typeof l === 'function' ? l() : l);
|
||||
}
|
||||
return this.props.layers;
|
||||
}
|
||||
render() {
|
||||
const { viewport } = this.state;
|
||||
return (
|
||||
<MapGL
|
||||
{...viewport}
|
||||
mapStyle={this.props.mapStyle}
|
||||
onViewportChange={this.onViewportChange}
|
||||
mapboxApiAccessToken={this.props.mapboxApiAccessToken}
|
||||
>
|
||||
<DeckGL
|
||||
{...viewport}
|
||||
layers={this.layers()}
|
||||
initWebGLParameters
|
||||
/>
|
||||
</MapGL>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeckGLContainer.propTypes = propTypes;
|
||||
DeckGLContainer.defaultProps = defaultProps;
|
||||
43
superset/assets/visualizations/deckgl/grid.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { GridLayer } from 'deck.gl';
|
||||
|
||||
import DeckGLContainer from './DeckGLContainer';
|
||||
|
||||
function deckScreenGridLayer(slice, payload, setControlValue) {
|
||||
const fd = slice.formData;
|
||||
const c = fd.color_picker;
|
||||
const data = payload.data.features.map(d => ({
|
||||
...d,
|
||||
color: [c.r, c.g, c.b, 255 * c.a],
|
||||
}));
|
||||
|
||||
const layer = new GridLayer({
|
||||
id: `grid-layer-${slice.containerId}`,
|
||||
data,
|
||||
pickable: true,
|
||||
cellSize: fd.grid_size,
|
||||
minColor: [0, 0, 0, 0],
|
||||
extruded: fd.extruded,
|
||||
maxColor: [c.r, c.g, c.b, 255 * c.a],
|
||||
outline: false,
|
||||
getElevationValue: points => points.reduce((sum, point) => sum + point.weight, 0),
|
||||
getColorValue: points => points.reduce((sum, point) => sum + point.weight, 0),
|
||||
});
|
||||
const viewport = {
|
||||
...fd.viewport,
|
||||
width: slice.width(),
|
||||
height: slice.height(),
|
||||
};
|
||||
ReactDOM.render(
|
||||
<DeckGLContainer
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport}
|
||||
layers={[layer]}
|
||||
mapStyle={fd.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
/>,
|
||||
document.getElementById(slice.containerId),
|
||||
);
|
||||
}
|
||||
module.exports = deckScreenGridLayer;
|
||||
43
superset/assets/visualizations/deckgl/hex.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { HexagonLayer } from 'deck.gl';
|
||||
|
||||
import DeckGLContainer from './DeckGLContainer';
|
||||
|
||||
function deckHex(slice, payload, setControlValue) {
|
||||
const fd = slice.formData;
|
||||
const c = fd.color_picker;
|
||||
const data = payload.data.features.map(d => ({
|
||||
...d,
|
||||
color: [c.r, c.g, c.b, 255 * c.a],
|
||||
}));
|
||||
|
||||
const layer = new HexagonLayer({
|
||||
id: `hex-layer-${slice.containerId}`,
|
||||
data,
|
||||
pickable: true,
|
||||
radius: fd.grid_size,
|
||||
minColor: [0, 0, 0, 0],
|
||||
extruded: fd.extruded,
|
||||
maxColor: [c.r, c.g, c.b, 255 * c.a],
|
||||
outline: false,
|
||||
getElevationValue: points => points.reduce((sum, point) => sum + point.weight, 0),
|
||||
getColorValue: points => points.reduce((sum, point) => sum + point.weight, 0),
|
||||
});
|
||||
const viewport = {
|
||||
...fd.viewport,
|
||||
width: slice.width(),
|
||||
height: slice.height(),
|
||||
};
|
||||
ReactDOM.render(
|
||||
<DeckGLContainer
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport}
|
||||
layers={[layer]}
|
||||
mapStyle={fd.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
/>,
|
||||
document.getElementById(slice.containerId),
|
||||
);
|
||||
}
|
||||
module.exports = deckHex;
|
||||
55
superset/assets/visualizations/deckgl/scatter.jsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { ScatterplotLayer } from 'deck.gl';
|
||||
|
||||
import DeckGLContainer from './DeckGLContainer';
|
||||
import { getColorFromScheme, hexToRGB } from '../../javascripts/modules/colors';
|
||||
import { unitToRadius } from '../../javascripts/modules/geo';
|
||||
|
||||
function deckScatter(slice, payload, setControlValue) {
|
||||
const fd = slice.formData;
|
||||
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
|
||||
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
|
||||
|
||||
const data = payload.data.features.map((d) => {
|
||||
let radius = unitToRadius(fd.point_unit, d.radius) || 10;
|
||||
if (fd.multiplier) {
|
||||
radius *= fd.multiplier;
|
||||
}
|
||||
let color;
|
||||
if (fd.dimension) {
|
||||
color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255);
|
||||
} else {
|
||||
color = fixedColor;
|
||||
}
|
||||
return {
|
||||
...d,
|
||||
radius,
|
||||
color,
|
||||
};
|
||||
});
|
||||
|
||||
const layer = new ScatterplotLayer({
|
||||
id: `scatter-layer-${slice.containerId}`,
|
||||
data,
|
||||
pickable: true,
|
||||
fp64: true,
|
||||
outline: false,
|
||||
});
|
||||
const viewport = {
|
||||
...fd.viewport,
|
||||
width: slice.width(),
|
||||
height: slice.height(),
|
||||
};
|
||||
ReactDOM.render(
|
||||
<DeckGLContainer
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport}
|
||||
layers={[layer]}
|
||||
mapStyle={fd.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
/>,
|
||||
document.getElementById(slice.containerId),
|
||||
);
|
||||
}
|
||||
module.exports = deckScatter;
|
||||
43
superset/assets/visualizations/deckgl/screengrid.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { ScreenGridLayer } from 'deck.gl';
|
||||
|
||||
import DeckGLContainer from './DeckGLContainer';
|
||||
|
||||
function deckScreenGridLayer(slice, payload, setControlValue) {
|
||||
const fd = slice.formData;
|
||||
const c = fd.color_picker;
|
||||
const data = payload.data.features.map(d => ({
|
||||
...d,
|
||||
color: [c.r, c.g, c.b, 255 * c.a],
|
||||
}));
|
||||
|
||||
const viewport = {
|
||||
...fd.viewport,
|
||||
width: slice.width(),
|
||||
height: slice.height(),
|
||||
};
|
||||
// Passing a layer creator function instead of a layer since the
|
||||
// layer needs to be regenerated at each render
|
||||
const layer = () => new ScreenGridLayer({
|
||||
id: `screengrid-layer-${slice.containerId}`,
|
||||
data,
|
||||
pickable: true,
|
||||
cellSizePixels: fd.grid_size,
|
||||
minColor: [c.r, c.g, c.b, 0],
|
||||
maxColor: [c.r, c.g, c.b, 255 * c.a],
|
||||
outline: false,
|
||||
getWeight: d => d.weight || 0,
|
||||
});
|
||||
ReactDOM.render(
|
||||
<DeckGLContainer
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport}
|
||||
layers={[layer]}
|
||||
mapStyle={fd.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
/>,
|
||||
document.getElementById(slice.containerId),
|
||||
);
|
||||
}
|
||||
module.exports = deckScreenGridLayer;
|
||||
@@ -3,13 +3,16 @@ import d3 from 'd3';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Select from 'react-select';
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import { Creatable } from 'react-select';
|
||||
import { Button } from 'react-bootstrap';
|
||||
|
||||
import DateFilterControl from '../javascripts/explore/components/controls/DateFilterControl';
|
||||
import ControlRow from '../javascripts/explore/components/ControlRow';
|
||||
import Control from '../javascripts/explore/components/Control';
|
||||
import controls from '../javascripts/explore/stores/controls';
|
||||
import OnPasteSelect from '../javascripts/components/OnPasteSelect';
|
||||
import VirtualizedRendererWrap from '../javascripts/components/VirtualizedRendererWrap';
|
||||
import './filter_box.css';
|
||||
import { t } from '../javascripts/locales';
|
||||
|
||||
@@ -69,7 +72,14 @@ class FilterBox extends React.Component {
|
||||
return control;
|
||||
}
|
||||
clickApply() {
|
||||
this.props.onChange(Object.keys(this.state.selectedValues)[0], [], true, true);
|
||||
const { selectedValues } = this.state;
|
||||
Object.keys(selectedValues).forEach((fltr, i, arr) => {
|
||||
let refresh = false;
|
||||
if (i === arr.length - 1) {
|
||||
refresh = true;
|
||||
}
|
||||
this.props.onChange(fltr, selectedValues[fltr], false, refresh);
|
||||
});
|
||||
this.setState({ hasChanged: false });
|
||||
}
|
||||
changeFilter(filter, options) {
|
||||
@@ -87,7 +97,9 @@ class FilterBox extends React.Component {
|
||||
const selectedValues = Object.assign({}, this.state.selectedValues);
|
||||
selectedValues[fltr] = vals;
|
||||
this.setState({ selectedValues, hasChanged: true });
|
||||
this.props.onChange(fltr, vals, false, this.props.instantFiltering);
|
||||
if (this.props.instantFiltering) {
|
||||
this.props.onChange(fltr, vals, false, true);
|
||||
}
|
||||
}
|
||||
render() {
|
||||
let dateFilter;
|
||||
@@ -164,7 +176,7 @@ class FilterBox extends React.Component {
|
||||
text: v,
|
||||
metric: 0,
|
||||
};
|
||||
this.props.filtersChoices[filterKey].push(addChoice);
|
||||
this.props.filtersChoices[filterKey].unshift(addChoice);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,7 +189,7 @@ class FilterBox extends React.Component {
|
||||
return (
|
||||
<div key={filter} className="m-b-5">
|
||||
{this.props.datasource.verbose_map[filter] || filter}
|
||||
<Select.Creatable
|
||||
<OnPasteSelect
|
||||
placeholder={t('Select [%s]', filter)}
|
||||
key={filter}
|
||||
multi
|
||||
@@ -195,6 +207,9 @@ class FilterBox extends React.Component {
|
||||
return { value: opt.id, label: opt.id, style };
|
||||
})}
|
||||
onChange={this.changeFilter.bind(this, filter)}
|
||||
selectComponent={Creatable}
|
||||
selectWrap={VirtualizedSelect}
|
||||
optionRenderer={VirtualizedRendererWrap(opt => opt.label)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -36,5 +36,9 @@ const vizMap = {
|
||||
event_flow: require('./EventFlow.jsx'),
|
||||
paired_ttest: require('./paired_ttest.jsx'),
|
||||
partition: require('./partition.js'),
|
||||
deck_scatter: require('./deckgl/scatter.jsx'),
|
||||
deck_screengrid: require('./deckgl/screengrid.jsx'),
|
||||
deck_grid: require('./deckgl/grid.jsx'),
|
||||
deck_hex: require('./deckgl/hex.jsx'),
|
||||
};
|
||||
export default vizMap;
|
||||
|
||||
@@ -22,7 +22,7 @@ function markupWidget(slice, payload) {
|
||||
jqdiv.html(`
|
||||
<iframe id="${iframeId}"
|
||||
frameborder="0"
|
||||
height="${slice.height()}"
|
||||
height="${slice.height() - 20}"
|
||||
sandbox="allow-same-origin allow-scripts allow-top-navigation allow-popups">
|
||||
</iframe>
|
||||
`);
|
||||
|
||||