Compare commits

..

93 Commits

Author SHA1 Message Date
Maxime Beauchemin
ea3a0814fc Merge branch 'master' into 0.21 2017-11-21 15:41:27 -08:00
Maxime Beauchemin
680e1cbb42 Revert "Filter out unavailable databases (#3875)" (#3918)
This reverts commit ae2205aeb5.
2017-11-21 12:58:47 -08:00
Maxime Beauchemin
2d37dec5ff [bugfix] remove quotes from Postgres time grains (#3913) 2017-11-21 11:24:17 -08:00
Maxime Beauchemin
3f4c306bd6 Fix left padding in dashboard widgets (#3915) 2017-11-21 10:24:28 -08:00
Maxime Beauchemin
ac432495d7 [cosmetic] remove border from table viz (#3916) 2017-11-21 10:23:48 -08:00
michellethomas
12fb7c1a62 When checking if you should renderTriggered make sure key exists in controls (#3912) 2017-11-21 10:22:55 -08:00
Yu Xiao
feb15a30a2 fix the schema-fetching problem for impala in sql_lab (#3906)
* fix the schema-fetching problem for impala in sql_lab

* delete redundant print

* remove blank lines...

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

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

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

This also makes the function that maps labels to colors case insensitive.

(cherry picked from commit a82bb588f4)
2017-11-17 16:15:41 -08:00
Maxime Beauchemin
a82bb588f4 Allow users to specify label->color mapping (#3879)
Users can define `label_colors` in a dashboard's JSON metadata that
enforces a label to color mapping.

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

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

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

* ScatterLayer
* HexagonLayer
* GridLayer
* ScreenGridLayer

* Addressing comments

* lint

* Linting

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

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

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

And mark the google group as deprecated

* README: move the screenshots at the end

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

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

* unit tests

* Moved getDiffs logic into AlteredSliceTag

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

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

* fixed mergeconflicts forreal this time

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

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

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

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

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

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

* Added unit tests

* Added virtualized/paste select to filterbox
2017-11-06 15:20:13 -08:00
Alejandro Fernandez
b059506afa DI-1113. ADDENDUM. Authentication: Enable user impersonation for Superset to HiveServer2 using hive.server2.proxy.user (a.fernandez) (#3697) 2017-11-06 10:20:38 -08:00
164 changed files with 10764 additions and 3871 deletions

View File

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

View File

@@ -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)

View File

@@ -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

View File

@@ -127,33 +127,14 @@ Installation & Configuration
[See in the documentation](https://superset.incubator.apache.org/installation.html)
More screenshots
----------------
![superset-security-menu](https://cloud.githubusercontent.com/assets/130878/20234707/0f565886-a835-11e6-9277-b4f5f4aa2fcc.png)
![superset-slice-bubble](https://cloud.githubusercontent.com/assets/130878/20234708/0f57f3d0-a835-11e6-8268-fcefe8f868c8.png)
![superset-slice-map](https://cloud.githubusercontent.com/assets/130878/20234709/0f5a5a44-a835-11e6-987a-1b6f8ac9922b.png)
![superset-slice-multiline](https://cloud.githubusercontent.com/assets/130878/20234710/0f632d68-a835-11e6-98d1-542dcb618193.png)
![superset-slice-sankey](https://cloud.githubusercontent.com/assets/130878/20234711/0f639136-a835-11e6-8721-fe5e48dab8e7.png)
![superset-slice-view](https://cloud.githubusercontent.com/assets/130878/20234712/0f63c4c6-a835-11e6-8595-6091a6428fa9.png)
![superset-sql-lab-2](https://cloud.githubusercontent.com/assets/130878/20234713/0f67b856-a835-11e6-9d50-7a52168f66fd.png)
![superset-sql-lab](https://cloud.githubusercontent.com/assets/130878/20234714/0f68f45a-a835-11e6-9467-f47ad0af7e79.png)
Resources
-------------
* [Superset Google Group](https://groups.google.com/forum/#!forum/airbnb_superset)
* [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
----------------
![superset-security-menu](https://cloud.githubusercontent.com/assets/130878/20234707/0f565886-a835-11e6-9277-b4f5f4aa2fcc.png)
![superset-slice-bubble](https://cloud.githubusercontent.com/assets/130878/20234708/0f57f3d0-a835-11e6-8268-fcefe8f868c8.png)
![superset-slice-map](https://cloud.githubusercontent.com/assets/130878/20234709/0f5a5a44-a835-11e6-987a-1b6f8ac9922b.png)
![superset-slice-multiline](https://cloud.githubusercontent.com/assets/130878/20234710/0f632d68-a835-11e6-98d1-542dcb618193.png)
![superset-slice-sankey](https://cloud.githubusercontent.com/assets/130878/20234711/0f639136-a835-11e6-8721-fe5e48dab8e7.png)
![superset-slice-view](https://cloud.githubusercontent.com/assets/130878/20234712/0f63c4c6-a835-11e6-8595-6091a6428fa9.png)
![superset-sql-lab-2](https://cloud.githubusercontent.com/assets/130878/20234713/0f67b856-a835-11e6-9d50-7a52168f66fd.png)
![superset-sql-lab](https://cloud.githubusercontent.com/assets/130878/20234714/0f68f45a-a835-11e6-9467-f47ad0af7e79.png)

View File

@@ -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"
}
}

View File

@@ -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``.

View File

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

View File

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

View File

@@ -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',

View File

@@ -4,14 +4,13 @@ from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import json
import logging
from logging.handlers import TimedRotatingFileHandler
import json
import os
from flask import Flask, redirect
from flask_appbuilder import SQLA, AppBuilder, IndexView
from flask_appbuilder import AppBuilder, IndexView, SQLA
from flask_appbuilder.baseviews import expose
from flask_migrate import Migrate
from flask_wtf.csrf import CSRFProtect
@@ -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

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

View File

@@ -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.');

View File

@@ -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;

View File

@@ -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>

View 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;

View File

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

View File

@@ -0,0 +1,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);

View 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));
};
}

View 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;
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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;

View File

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

View File

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

View File

@@ -0,0 +1,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 };
}

View File

@@ -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}>

View 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')} &nbsp;
<i className="fa fa-save" />&nbsp;
{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;

View File

@@ -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;

View File

@@ -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);

View 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;

View File

@@ -1,10 +1,8 @@
/* global notify */
import React from 'react';
import PropTypes from 'prop-types';
import { Responsive, WidthProvider } from 'react-grid-layout';
import $ from 'jquery';
import SliceCell from './SliceCell';
import GridCell from './GridCell';
import { getExploreUrl } from '../../explore/exploreUtils';
require('react-grid-layout/css/styles.css');
@@ -14,119 +12,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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -11,6 +11,8 @@ require('react-bootstrap-table/css/react-bootstrap-table.css');
const propTypes = {
dashboard: PropTypes.object.isRequired,
triggerNode: PropTypes.node.isRequired,
userId: PropTypes.string.isRequired,
addSlicesToDashboard: PropTypes.func,
};
class SliceAdder extends React.Component {
@@ -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

View File

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

View File

@@ -0,0 +1,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;

View File

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

View File

@@ -0,0 +1,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,
});

View File

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

View File

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

View File

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

View File

@@ -21,6 +21,7 @@ const propTypes = {
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
PropTypes.bool,
PropTypes.array,
PropTypes.func]),

View File

@@ -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"

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
};
}

View File

@@ -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,
};

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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: [],

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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,
});

View File

@@ -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'),
},

View File

@@ -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],
],
},
],

View File

@@ -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];
}

View 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;
}

View File

@@ -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;

View File

@@ -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, ' '));
}

View File

@@ -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);
}

View File

@@ -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"
}
}

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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;
});
});

View File

@@ -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);
});
});

View File

@@ -66,5 +66,5 @@ export const contextData = {
dash_save_perm: true,
standalone_mode: false,
dash_edit_perm: true,
user_id: '1',
userId: '1',
};

View File

@@ -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;

View File

@@ -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);
});
});

View File

@@ -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', () => {

View File

@@ -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"');
});
});

View File

@@ -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);
});
});
});

View File

@@ -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]);
});
});

View 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));
});
});

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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]]]}}
]}

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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>
`);

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