Compare commits
390 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acc880c4df | ||
|
|
557b557503 | ||
|
|
3018356588 | ||
|
|
ede4dffcb7 | ||
|
|
cad392eb76 | ||
|
|
0296158100 | ||
|
|
b2a4692a02 | ||
|
|
2fbadea9e3 | ||
|
|
dc05be36a6 | ||
|
|
dac0d1d0dc | ||
|
|
459f7160ac | ||
|
|
aff524d843 | ||
|
|
3a91667e92 | ||
|
|
3e0d3584f7 | ||
|
|
1e47d6fb41 | ||
|
|
d5ba88b407 | ||
|
|
ce1e18b31b | ||
|
|
ec84aa7577 | ||
|
|
8b4d72cf32 | ||
|
|
85e6e65a47 | ||
|
|
7cad3655f5 | ||
|
|
b9e7f292c3 | ||
|
|
fc85034c60 | ||
|
|
f5e3d0cc02 | ||
|
|
fe377e8b94 | ||
|
|
5bb87138e9 | ||
|
|
579e58206e | ||
|
|
172b6ce892 | ||
|
|
0cc8eff1c3 | ||
|
|
3b023e5eaa | ||
|
|
615d8f1624 | ||
|
|
b4409ace21 | ||
|
|
dbee6aca1f | ||
|
|
acfe62eaf7 | ||
|
|
527a8af060 | ||
|
|
a5a931a670 | ||
|
|
2f05efaf12 | ||
|
|
83ef8a2e12 | ||
|
|
c564881867 | ||
|
|
b16930f35d | ||
|
|
2d910e3f07 | ||
|
|
daa1420c8e | ||
|
|
cea310e50b | ||
|
|
fcdd5c6752 | ||
|
|
2ace73e9a1 | ||
|
|
80cfb08794 | ||
|
|
1edc2b91cf | ||
|
|
1f58e18b6f | ||
|
|
f2bf316058 | ||
|
|
9cd38fa1ed | ||
|
|
edb0111775 | ||
|
|
de4f9e8d1a | ||
|
|
461e41cd61 | ||
|
|
716406198e | ||
|
|
68592aeddf | ||
|
|
b927ff6eef | ||
|
|
ce50e6e4fe | ||
|
|
167ed33bba | ||
|
|
0ee1abf31a | ||
|
|
6a0a1af67e | ||
|
|
f85481d51b | ||
|
|
00b6b0ac68 | ||
|
|
1546b1ae71 | ||
|
|
1e94498d9d | ||
|
|
0f7189b859 | ||
|
|
a6e0f1b75a | ||
|
|
543c22bb50 | ||
|
|
07e067cf0b | ||
|
|
6c256a34a9 | ||
|
|
6b2eb04a73 | ||
|
|
898d80ba38 | ||
|
|
ea8e4ad05b | ||
|
|
27aeac6859 | ||
|
|
8da371e324 | ||
|
|
0c59fe933d | ||
|
|
e169c67760 | ||
|
|
3a5a927dc6 | ||
|
|
2d419e4253 | ||
|
|
87869a29c9 | ||
|
|
544211f5ec | ||
|
|
f6ac95e2dd | ||
|
|
63bef2f844 | ||
|
|
4a8cd04de6 | ||
|
|
85806624db | ||
|
|
1ac2273984 | ||
|
|
a8c29c4ffe | ||
|
|
31af01c4f2 | ||
|
|
b1bba96d04 | ||
|
|
c5c730224e | ||
|
|
7441cf7d39 | ||
|
|
45c72d25df | ||
|
|
3fff631b32 | ||
|
|
bfa2891b23 | ||
|
|
5715f52fef | ||
|
|
1f2126f463 | ||
|
|
27ed0b37bf | ||
|
|
cdbd2f8507 | ||
|
|
e46ba2b4a4 | ||
|
|
1c338ba742 | ||
|
|
2b7673ad5d | ||
|
|
2f27353015 | ||
|
|
1b8c3f420a | ||
|
|
a3a070855c | ||
|
|
e84c6393b8 | ||
|
|
7413dd9f4b | ||
|
|
9cbd667eb7 | ||
|
|
37fb56c61c | ||
|
|
404a94cadb | ||
|
|
0807a8d016 | ||
|
|
4a9888157e | ||
|
|
83fbdcceac | ||
|
|
b070ef5fdb | ||
|
|
7d380dcd14 | ||
|
|
a15dbd992d | ||
|
|
52c5d235af | ||
|
|
495f6460a4 | ||
|
|
a96024d0e7 | ||
|
|
99b84d2909 | ||
|
|
24728b8b47 | ||
|
|
9750e49df8 | ||
|
|
bf31783d0c | ||
|
|
87eacf88c3 | ||
|
|
1dbfb99ead | ||
|
|
ff4020ea73 | ||
|
|
0ce7fc18a8 | ||
|
|
470a6e9d76 | ||
|
|
fc74fbeeaa | ||
|
|
9c6a5793b9 | ||
|
|
49b6b38741 | ||
|
|
a385ee9e97 | ||
|
|
f0917c62f2 | ||
|
|
94d20168da | ||
|
|
2d866e3ffa | ||
|
|
5d94d7067e | ||
|
|
7323f4c2ab | ||
|
|
eca6dfef6a | ||
|
|
761462ef93 | ||
|
|
98e83255e6 | ||
|
|
cbf3562a6f | ||
|
|
a2c41bbace | ||
|
|
2a12a3c702 | ||
|
|
2ab6a411f4 | ||
|
|
14ed10bdb0 | ||
|
|
49e6fd5bfb | ||
|
|
af872fa4d4 | ||
|
|
cec4cf014c | ||
|
|
783ad703d0 | ||
|
|
222671675c | ||
|
|
9a62d94630 | ||
|
|
c3edc6e24b | ||
|
|
119b0c55e9 | ||
|
|
c14c7edc5e | ||
|
|
e3b296c558 | ||
|
|
c2d29fb54b | ||
|
|
7aab8b0ae3 | ||
|
|
861a3bd4ae | ||
|
|
9bc7ad9cd5 | ||
|
|
a1e3fc1c23 | ||
|
|
242869db3a | ||
|
|
8924bb79e7 | ||
|
|
a0d103dac3 | ||
|
|
d52b299df8 | ||
|
|
092432f04f | ||
|
|
ea8e6634d6 | ||
|
|
3e6f90cf72 | ||
|
|
16731056ed | ||
|
|
0712894353 | ||
|
|
36fad803ed | ||
|
|
6732f01cb7 | ||
|
|
bb04e6fcfa | ||
|
|
007ee88d33 | ||
|
|
7a5bb94754 | ||
|
|
e06a0cd89b | ||
|
|
b6cba13293 | ||
|
|
d929bbfe30 | ||
|
|
bf67d64708 | ||
|
|
92aa1a6124 | ||
|
|
733ab8014b | ||
|
|
6aaa49f0bf | ||
|
|
638f27c2df | ||
|
|
84a3b55912 | ||
|
|
552d46479b | ||
|
|
fa9c066ffe | ||
|
|
e1e20b8757 | ||
|
|
2fb94a89e2 | ||
|
|
7a9604a3c9 | ||
|
|
e099088012 | ||
|
|
34e107e7d3 | ||
|
|
2254a4d0b4 | ||
|
|
9f7486f402 | ||
|
|
699602d1c5 | ||
|
|
2993ff1d75 | ||
|
|
afb3c24d5a | ||
|
|
8ef730b5fe | ||
|
|
866cfe5279 | ||
|
|
68c2eab6b9 | ||
|
|
aeda5bd260 | ||
|
|
a95cd71456 | ||
|
|
34d0dd9d6e | ||
|
|
401d9afd54 | ||
|
|
74edb936a5 | ||
|
|
c1558578d7 | ||
|
|
3597fdb7f8 | ||
|
|
43f2a379a1 | ||
|
|
69702e3a19 | ||
|
|
eb0655cf85 | ||
|
|
d8864bc92b | ||
|
|
89fc9d7c80 | ||
|
|
76aa9f7e10 | ||
|
|
abd0974897 | ||
|
|
c4e943a24f | ||
|
|
a3106bcb3d | ||
|
|
b045075a96 | ||
|
|
09d597f3ad | ||
|
|
9d4c3d83d0 | ||
|
|
95580a004f | ||
|
|
723f90755e | ||
|
|
324205f77a | ||
|
|
0a40d8ce8f | ||
|
|
168a25239e | ||
|
|
7eef46e941 | ||
|
|
50da4f8c07 | ||
|
|
2d0ebeae1b | ||
|
|
1a16491971 | ||
|
|
7f4f250970 | ||
|
|
25acb78071 | ||
|
|
e822d5a1b7 | ||
|
|
32fc0ff6d0 | ||
|
|
94dde075b3 | ||
|
|
65e92327ab | ||
|
|
0be02e67a5 | ||
|
|
7327c97e4c | ||
|
|
03b21dcf0a | ||
|
|
dc98c6739f | ||
|
|
fcb870728d | ||
|
|
7919428a1e | ||
|
|
3496a80f5a | ||
|
|
56b917a5c2 | ||
|
|
18c43aaea2 | ||
|
|
c43fc38f69 | ||
|
|
c07f0ab9c7 | ||
|
|
1c429b27bc | ||
|
|
b7019ad4f3 | ||
|
|
84e8f741ae | ||
|
|
e3a9b393c2 | ||
|
|
16aba517e4 | ||
|
|
205928e6df | ||
|
|
39ce4aa049 | ||
|
|
cef4a8296a | ||
|
|
b370ef0229 | ||
|
|
6b80f5bb35 | ||
|
|
bdae570a69 | ||
|
|
face5245a9 | ||
|
|
db1ed2a765 | ||
|
|
10982dec3c | ||
|
|
6825e75681 | ||
|
|
bd6a439e0b | ||
|
|
c90dd4902f | ||
|
|
868e5c45fe | ||
|
|
7e1852ee88 | ||
|
|
5ae98bc7c9 | ||
|
|
1624e7de7d | ||
|
|
7a98f84890 | ||
|
|
9b181280d4 | ||
|
|
38e94b9e43 | ||
|
|
dc25bc6f4d | ||
|
|
f64a205603 | ||
|
|
a8480f5492 | ||
|
|
0acf26b37c | ||
|
|
2c068a1a15 | ||
|
|
b961c95121 | ||
|
|
82693211f0 | ||
|
|
e5467462cb | ||
|
|
ab5a4102cd | ||
|
|
d5ef937b31 | ||
|
|
bce02e3f51 | ||
|
|
aad9744d85 | ||
|
|
506b781f3a | ||
|
|
267fd5b9bc | ||
|
|
c362f2869e | ||
|
|
4f7f437527 | ||
|
|
ab5da5ba28 | ||
|
|
7531bb8942 | ||
|
|
811ee8ccdc | ||
|
|
51cb485ce3 | ||
|
|
83d08b8b8f | ||
|
|
ed3d44d591 | ||
|
|
895fe23203 | ||
|
|
af04a560c8 | ||
|
|
9124a17e86 | ||
|
|
99b0d4c111 | ||
|
|
84b98c234f | ||
|
|
bcc1428ebf | ||
|
|
2133056c04 | ||
|
|
4155a9d7f9 | ||
|
|
ed4825523c | ||
|
|
fdbb2bbdab | ||
|
|
c064d6d847 | ||
|
|
d33874bd3d | ||
|
|
96d32dd11f | ||
|
|
d6bc354ff3 | ||
|
|
7325a4fb4b | ||
|
|
90f00c5b29 | ||
|
|
8539c423ea | ||
|
|
e9bfbfce84 | ||
|
|
6e4f0664cb | ||
|
|
3c920c9d94 | ||
|
|
15b67b2c6c | ||
|
|
973537fd9a | ||
|
|
d70a74479d | ||
|
|
946e4b750a | ||
|
|
9789e3fb9b | ||
|
|
6a15679d87 | ||
|
|
ad1cd5577c | ||
|
|
55668ca621 | ||
|
|
0c221a28d1 | ||
|
|
a475551b23 | ||
|
|
bad7676414 | ||
|
|
51c0470f0b | ||
|
|
4530047c76 | ||
|
|
1bf83c3bf7 | ||
|
|
bb6ab11001 | ||
|
|
e4bd1884d3 | ||
|
|
4014a48f7d | ||
|
|
97ded32415 | ||
|
|
593ac081f0 | ||
|
|
69f0a4e1cb | ||
|
|
757e7de60c | ||
|
|
1d7d5469a9 | ||
|
|
98afc3e590 | ||
|
|
ea189790f1 | ||
|
|
62987077fa | ||
|
|
3b9f7cb3f1 | ||
|
|
5882c7e344 | ||
|
|
77b6e2cd2e | ||
|
|
88b1f956c7 | ||
|
|
d9b49ca2bc | ||
|
|
4156ad5a30 | ||
|
|
ae46561648 | ||
|
|
1700a807e9 | ||
|
|
0bab15b213 | ||
|
|
38d3075554 | ||
|
|
1b124bfb87 | ||
|
|
cdf4dd0302 | ||
|
|
a13bed2db6 | ||
|
|
26318f94fe | ||
|
|
769fb0820f | ||
|
|
52380534f3 | ||
|
|
2fd2526046 | ||
|
|
61509bbd44 | ||
|
|
76499afd8d | ||
|
|
4023f328f7 | ||
|
|
4f49cb555b | ||
|
|
4dc959a3e4 | ||
|
|
518fbf562c | ||
|
|
49828d3d9d | ||
|
|
248e6a7b05 | ||
|
|
5561e6b770 | ||
|
|
ab083b86f3 | ||
|
|
4bf525222a | ||
|
|
45efcb381c | ||
|
|
07a7736c71 | ||
|
|
d2826ab7af | ||
|
|
6ab769f382 | ||
|
|
3e1cd2bdca | ||
|
|
22784b7f06 | ||
|
|
c4922615eb | ||
|
|
7307ddad3c | ||
|
|
c7ba143d03 | ||
|
|
b24206387b | ||
|
|
64d196442f | ||
|
|
5944643da6 | ||
|
|
8c5e495272 | ||
|
|
bb23685b9d | ||
|
|
940659bc14 | ||
|
|
7c5933732b | ||
|
|
89df2fcf76 | ||
|
|
174a199c30 | ||
|
|
6f1e7c3016 | ||
|
|
9f81e23f8f | ||
|
|
19fab6eea7 | ||
|
|
63161b11c3 | ||
|
|
4f886d65ec | ||
|
|
62e0e195e8 | ||
|
|
e9d4749f44 | ||
|
|
93f8e7d8e9 | ||
|
|
3dea6e0da5 | ||
|
|
6fb3b305ad | ||
|
|
7dfe891cc1 | ||
|
|
5c3966a32d |
@@ -6,7 +6,7 @@ engines:
|
||||
eslint:
|
||||
enabled: true
|
||||
config:
|
||||
config: caravel/assets/.eslintrc
|
||||
config: superset/assets/.eslintrc
|
||||
pep8:
|
||||
enabled: true
|
||||
fixme:
|
||||
@@ -19,16 +19,17 @@ engines:
|
||||
ratings:
|
||||
paths:
|
||||
- "**.py"
|
||||
- "caravel/assets/**.js"
|
||||
- "caravel/assets/**.jsx"
|
||||
- "superset/assets/**.js"
|
||||
- "superset/assets/**.jsx"
|
||||
exclude_paths:
|
||||
- ".*"
|
||||
- "**.pyc"
|
||||
- "**.gz"
|
||||
- "env/"
|
||||
- "tests/"
|
||||
- "caravel/assets/images/"
|
||||
- "caravel/assets/vendor/"
|
||||
- "caravel/assets/node_modules/"
|
||||
- "caravel/assets/javascripts/dist/"
|
||||
- "caravel/migrations"
|
||||
- "superset/assets/images/"
|
||||
- "superset/assets/vendor/"
|
||||
- "superset/assets/node_modules/"
|
||||
- "superset/assets/javascripts/dist/"
|
||||
- "superset/migrations"
|
||||
- "docs/"
|
||||
|
||||
12
.gitignore
vendored
@@ -1,21 +1,25 @@
|
||||
*.pyc
|
||||
yarn-error.log
|
||||
_modules
|
||||
superset/assets/coverage/*
|
||||
changelog.sh
|
||||
.DS_Store
|
||||
.coverage
|
||||
_build
|
||||
_static
|
||||
_images
|
||||
caravel/bin/caravelc
|
||||
_modules
|
||||
superset/bin/supersetc
|
||||
env_py3
|
||||
.eggs
|
||||
build
|
||||
*.db
|
||||
tmp
|
||||
caravel_config.py
|
||||
superset_config.py
|
||||
local_config.py
|
||||
env
|
||||
dist
|
||||
caravel.egg-info/
|
||||
superset.egg-info/
|
||||
app.db
|
||||
*.bak
|
||||
.idea
|
||||
@@ -26,3 +30,5 @@ app.db
|
||||
*.js.map
|
||||
node_modules
|
||||
npm-debug.log
|
||||
yarn.lock
|
||||
superset/assets/version_info.json
|
||||
|
||||
@@ -16,8 +16,7 @@ pep8:
|
||||
full: true
|
||||
ignore-paths:
|
||||
- docs
|
||||
- caravel/migrations/env.py
|
||||
- caravel/ascii_art.py
|
||||
- superset/migrations/env.py
|
||||
ignore-patterns:
|
||||
- ^example/doc_.*\.py$
|
||||
- (^|/)docs(/|$)
|
||||
|
||||
2
.pycodestyle
Normal file
@@ -0,0 +1,2 @@
|
||||
[pycodestyle]
|
||||
max-line-length = 90
|
||||
@@ -24,15 +24,14 @@ env:
|
||||
before_install:
|
||||
- npm install -g npm@'>=3.9.5'
|
||||
before_script:
|
||||
- mysql -e 'drop database if exists caravel; create database caravel DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci' -u root
|
||||
- mysql -e 'drop database if exists superset; create database superset DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci' -u root
|
||||
- mysql -u root -e "CREATE USER 'mysqluser'@'localhost' IDENTIFIED BY 'mysqluserpassword';"
|
||||
- mysql -u root -e "GRANT ALL ON caravel.* TO 'mysqluser'@'localhost';"
|
||||
- psql -c 'create database caravel;' -U postgres
|
||||
- mysql -u root -e "GRANT ALL ON superset.* TO 'mysqluser'@'localhost';"
|
||||
- psql -c 'create database superset;' -U postgres
|
||||
- psql -c "CREATE USER postgresuser WITH PASSWORD 'pguserpassword';" -U postgres
|
||||
- export PATH=${PATH}:/tmp/hive/bin
|
||||
install:
|
||||
- pip install --upgrade pip
|
||||
- pip install tox tox-travis
|
||||
- rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install $TRAVIS_NODE_VERSION
|
||||
- npm install
|
||||
script: tox -e $TOX_ENV
|
||||
|
||||
314
CHANGELOG.md
@@ -1,5 +1,319 @@
|
||||
## Change Log
|
||||
|
||||
### 0.15.1 (2016/12/28 21:29 +00:00)
|
||||
- [092432f](https://github.com/airbnb/superset/commit/092432f04f0033e60493f009728a7bfd6a744b22) v0.15.1 (@mistercrunch)
|
||||
- [ea8e663](https://github.com/airbnb/superset/commit/ea8e6634d6c304cde3a42c65d37ec694f76b8cec) read anon user role from config, remove reference to public role (#1878) (@willgroves)
|
||||
- [3e6f90c](https://github.com/airbnb/superset/commit/3e6f90cf722f205e1f13ef7228be6d0d767c1d1d) Upgrading pydruid version and adopt 'merge' flag during refresh_druid operation (#1879) (@dkhwangbo)
|
||||
- [1673105](https://github.com/airbnb/superset/commit/16731056edf25cd4422fa674de827215df9027b1) [sqllab] async queries - better error handling (#1853) (@mistercrunch)
|
||||
- [0712894](https://github.com/airbnb/superset/commit/0712894353825ea2faa19d8000ca031c44debf39) Improving database logging by adding duration, referrer and post data (#1830) (@mistercrunch)
|
||||
- [36fad80](https://github.com/airbnb/superset/commit/36fad803edf6666188746f270e498e03c2df363c) sqllab: don't hold database deletion because of query reference (#1863) (@xrmx)
|
||||
- [6732f01](https://github.com/airbnb/superset/commit/6732f01cb7bb08e1b180bb74f263bf50317d7462) Enable freeform-select with fetched column values for filter values (#1697) (@vera-liu)
|
||||
- [bb04e6f](https://github.com/airbnb/superset/commit/bb04e6fcfa1042b4532b50d8898555813be7fa29) Use APP_ICON in template (#1855) (@szmate1618)
|
||||
- [007ee88](https://github.com/airbnb/superset/commit/007ee88d33f92e6d052122b35ccad84d176029a7) [explorev2] improving the scrolling/scrollbars placement (#1840) (@mistercrunch)
|
||||
|
||||
### airbnb_prod.0.15.0.1 (2016/12/15 22:06 +00:00)
|
||||
- [7a5bb94](https://github.com/airbnb/superset/commit/7a5bb947542fdc20e2ec70e18b1cf418b8d1dba8) Stop ChartContainer from rendering twice on chartStatus change (#1828) (@vera-liu)
|
||||
- [e06a0cd](https://github.com/airbnb/superset/commit/e06a0cd89bc84f3a7e75ee6f03df8c9c3a2badeb) Add force_ctas_schema to query model when enabled (#1825) (@vera-liu)
|
||||
- [b6cba13](https://github.com/airbnb/superset/commit/b6cba13293101f3022c16e120e96383b59cae03e) [explorev2] enabling redux dev tools (#1842) (@mistercrunch)
|
||||
- [d929bbf](https://github.com/airbnb/superset/commit/d929bbfe3010f795cdf9ef28b48e9e249cf7ef86) [explorev2] making QueryAndSaveBtns disabled while running queries (#1841) (@mistercrunch)
|
||||
- [bf67d64](https://github.com/airbnb/superset/commit/bf67d64708834800669e8be30ffa695d7a6db022) [explorev2] making Datasource an Viz controls not clearable (#1845) (@mistercrunch)
|
||||
- [92aa1a6](https://github.com/airbnb/superset/commit/92aa1a612476fdef5f19eb68f961324a333b346c) Permissions refactoring, optimizations and unit testing. (#1798) (@bkyryliuk)
|
||||
- [733ab80](https://github.com/airbnb/superset/commit/733ab8014bd83ae4e0e4961b9c454ad238556bd8) [explorev2] using a loader to load the explorev2 specific css (#1843) (@mistercrunch)
|
||||
- [6aaa49f](https://github.com/airbnb/superset/commit/6aaa49f0bf182f449a970a1e15d86111987dde32) Change default gunicorn address (#1838) (@amancevice)
|
||||
- [638f27c](https://github.com/airbnb/superset/commit/638f27c2df6540a50d5cba92847bc7000b33cb1b) [sqllab] Fix sql expression bug with count distinct metrics (#1805) (@vera-liu)
|
||||
- [84a3b55](https://github.com/airbnb/superset/commit/84a3b559128bb7717869f5fa7339bfe1ac4003cc) [explorev2] remove unused file SqlClause.jsx (#1839) (@mistercrunch)
|
||||
- [552d464](https://github.com/airbnb/superset/commit/552d46479bbf819e4fa7fb811ec05cff104a840c) [explorev2] no bootstrap data, just metadata in exploreV2 (#1827) (@mistercrunch)
|
||||
- [fa9c066](https://github.com/airbnb/superset/commit/fa9c066ffe6b71a36f97e2e0d9fdb94b94183ca6) Add email-to option in action buttons for dashboard and slice (#1705) (@vera-liu)
|
||||
- [e1e20b8](https://github.com/airbnb/superset/commit/e1e20b875748f677312d8c5ed3daf87113143b63) Sort searched queries by recency (#1735) (@vera-liu)
|
||||
- [2fb94a8](https://github.com/airbnb/superset/commit/2fb94a89e2538710c5293404acaf6d8f0ef847ea) Add ADDITIONAL_MIDDLEWARE option to config (#1832) (@jr-minnaar)
|
||||
- [7a9604a](https://github.com/airbnb/superset/commit/7a9604a3c918ce5fad1010e9e95fbf961a95420d) Workaround for slices "Not Found" issue in IE 11 (#1821) (@rlei)
|
||||
- [e099088](https://github.com/airbnb/superset/commit/e0990880121643b63f66cc18fe917ea9fbe98554) [hotfix] fixing the build (@mistercrunch)
|
||||
- [34e107e](https://github.com/airbnb/superset/commit/34e107e7d35b7d48385a13268b2ed713a2808114) [explore-v2] add config option for explore v2 beta users, and send through v2 path (#1671) (@ascott)
|
||||
|
||||
### 0.15.0 (2016/12/12 19:30 +00:00)
|
||||
- [2254a4d](https://github.com/airbnb/superset/commit/2254a4d0b4480da7563c7c3afdeadecf66b08cf5) v0.15.0 (@mistercrunch)
|
||||
- [9f7486f](https://github.com/airbnb/superset/commit/9f7486f4029fcbc18dfdeaeb4ff3c3c07242ad2b) remove extra call to get_viz in explorev2 (#1812) (@vera-liu)
|
||||
- [699602d](https://github.com/airbnb/superset/commit/699602d1c5bdd7d15f6adb5f22e458c268bc1306) Add tooltips to RunAsync and CTAS button (#1792) (@vera-liu)
|
||||
- [2993ff1](https://github.com/airbnb/superset/commit/2993ff1d75ff2391ffab388544f198cbb78364a9) Add NVD3's bullet chart (#1775) (@darabos)
|
||||
- [afb3c24](https://github.com/airbnb/superset/commit/afb3c24d5a4951e7cc5a5714a79eb201e0b1aa24) Showing more fields in DatabaseView (@mistercrunch)
|
||||
- [8ef730b](https://github.com/airbnb/superset/commit/8ef730b5feb8e27116552ca863e8dac42b1aa6ec) Added timer to explore v2 and share it with sqllab (#1802) (@vera-liu)
|
||||
- [866cfe5](https://github.com/airbnb/superset/commit/866cfe52794d5c6df9f85f088243fa85f66eaec7) Add schema name to output column in query history (#1790) (@vera-liu)
|
||||
- [68c2eab](https://github.com/airbnb/superset/commit/68c2eab6b93a13966b19aae20fb6c82fc34e3bcf) [hotfix] handling 0% change in big number with trendline (#1801) (@mistercrunch)
|
||||
- [aeda5bd](https://github.com/airbnb/superset/commit/aeda5bd2606946a5cb6fd7dd5ecd4b73413c79a9) [sqllab] config item for SQLLAB_DEFAULT_DBID (#1793) (@mistercrunch)
|
||||
- [a95cd71](https://github.com/airbnb/superset/commit/a95cd71456a0b24c08d4d020238fd9d89b2c9cd7) Add viz thumbnails to viz_type select (#1794) (@vera-liu)
|
||||
- [34d0dd9](https://github.com/airbnb/superset/commit/34d0dd9d6e074e41968c39247d66e19fd551c57b) adjust header nav links so they are all aligned on the base line (#1786) (@ascott)
|
||||
- [401d9af](https://github.com/airbnb/superset/commit/401d9afd54ce46f5c63a821094350098f07823a1) [ui] update logo, favicon, and new primary color (#1781) (@ascott)
|
||||
- [74edb93](https://github.com/airbnb/superset/commit/74edb936a599ed54528389814ddf83e74f8452fd) [WIP] Add http to copied url and move function to componentWillReceiveProps (#1780) (@vera-liu)
|
||||
- [c155857](https://github.com/airbnb/superset/commit/c1558578d7c555fb7bb9ee30eb98bad4d28fbd07) [explorev2] Breaking down large files, fixing JS warnings (#1773) (@mistercrunch)
|
||||
|
||||
### airbnb_prod.0.13.0.3 (2016/12/06 07:18 +00:00)
|
||||
- [3597fdb](https://github.com/airbnb/superset/commit/3597fdb7f869929e0b09ff0144ff430b81e67853) Filter table list based on the user permissions. (#1769) (@bkyryliuk)
|
||||
|
||||
### airbnb_prod.0.13.0.2 (2016/12/06 01:17 +00:00)
|
||||
- [43f2a37](https://github.com/airbnb/superset/commit/43f2a379a1b88b260d0a4bdeac97b3cb4478afcf) Make cell-click filter in table viz optional (#1762) (@vera-liu)
|
||||
- [69702e3](https://github.com/airbnb/superset/commit/69702e3a1956ef5587e5aea2bffb3720b9b4cd35) Create users if not found. (#1753) (@bkyryliuk)
|
||||
- [eb0655c](https://github.com/airbnb/superset/commit/eb0655cf85daad4329cf8630fe99e2a50d0e7e5a) [sqllab] Fixed js error when results are not available (#1715) (@vera-liu)
|
||||
- [d8864bc](https://github.com/airbnb/superset/commit/d8864bc92b7566fa4ebb9d4863d75bd5eaa1e9c4) Enable overwrite sql in QueryHistory (#1731) (@vera-liu)
|
||||
- [89fc9d7](https://github.com/airbnb/superset/commit/89fc9d7c80564e2f02949ed33113a63fc1e898e4) Make entire menuitem clickable for copy query (#1747) (@vera-liu)
|
||||
- [76aa9f7](https://github.com/airbnb/superset/commit/76aa9f7e1047de5e41e8bd5bfee610fcd569b8ad) [explorev2] fix textfield and druid bug (#1732) (@vera-liu)
|
||||
- [abd0974](https://github.com/airbnb/superset/commit/abd097489738eacc37fcd5dc9605d483c84071f6) Fix superset cli for python3 (#1760) (@xrmx)
|
||||
- [c4e943a](https://github.com/airbnb/superset/commit/c4e943a24fe89824e59a105d6ce6e5d9255717c1) [sqllab] making 'click to retrieve results' a button (#1737) (@mistercrunch)
|
||||
- [a3106bc](https://github.com/airbnb/superset/commit/a3106bcb3d0bc6fb0e5df2c356d453563a07c8a2) [bugfix] bignumber comparison wrong with neg values (#1743) (@mistercrunch)
|
||||
- [b045075](https://github.com/airbnb/superset/commit/b045075a96232f1e2f2e14bdaa171eba67b0fa29) Sankey Tooltip fix (#1748) (#1750) (@ddol)
|
||||
- [09d597f](https://github.com/airbnb/superset/commit/09d597f3adf4b238dc2a43b802fdda22b451dfe5) Prevent duplicated view_menu perms (#1751) (@bkyryliuk)
|
||||
- [9d4c3d8](https://github.com/airbnb/superset/commit/9d4c3d83d0907925f1f37cbe48c5decd2c54639f) Update role based on usernames not emails. (#1749) (@bkyryliuk)
|
||||
- [95580a0](https://github.com/airbnb/superset/commit/95580a004fe3322ff1c623ac4eec72150e1355b5) [explorev2] cosmetic, smaller size for input text (#1746) (@mistercrunch)
|
||||
- [723f907](https://github.com/airbnb/superset/commit/723f90755e89e46287e212b18844c9f7abc6cf60) Fixing the sourcemap in dev mode (#1744) (@mistercrunch)
|
||||
- [324205f](https://github.com/airbnb/superset/commit/324205f77ac7a77b6546da482979842d58ce9fbb) [sqllab] bugfix where a query has the same alias twice as output (#1734) (@mistercrunch)
|
||||
- [0a40d8c](https://github.com/airbnb/superset/commit/0a40d8ce8f08ba6edc18370032aa85571495b570) Rremove unused symlinks (#1736) (@yolken)
|
||||
- [168a252](https://github.com/airbnb/superset/commit/168a25239e712fe9eccf676f26df75fd91bd126f) State that npm should be between 3.9 and 4 (@bkyryliuk)
|
||||
- [7eef46e](https://github.com/airbnb/superset/commit/7eef46e9413b01ec15be515567a9619c695d5501) Adding links pointing to the new user profile page (#1704) (@mistercrunch)
|
||||
- [50da4f8](https://github.com/airbnb/superset/commit/50da4f8c0708f91ff24c0abd7189a137a1415bf6) Support running superset via pex (#1713) (@yolken)
|
||||
|
||||
### airbnb_prod.0.13.0.1 (2016/12/01 19:59 +00:00)
|
||||
- [2d0ebea](https://github.com/airbnb/superset/commit/2d0ebeae1bfd5e385000c9aa952891cf474821c6) [explorev2] Make chart container more responsive (#1724) (@vera-liu)
|
||||
- [1a16491](https://github.com/airbnb/superset/commit/1a164919715d6eaf1a999b97daaa53acb94a827d) Display full table name (schema + name) if possible. (#1728) (@bkyryliuk)
|
||||
- [7f4f250](https://github.com/airbnb/superset/commit/7f4f25097046dac9b436ef87f041debe2713827a) Redirects to login page if user not logged in at welcome page (#1723) (@vera-liu)
|
||||
- [25acb78](https://github.com/airbnb/superset/commit/25acb78071acc2eec8b44cb7019f269c1b2a6deb) Pass schema to the select star query. (#1714) (@bkyryliuk)
|
||||
- [e822d5a](https://github.com/airbnb/superset/commit/e822d5a1b7eb8f0cabcfcc85f5201df8199db796) Make edit / add / delete perms available to all users. (#1722) (@bkyryliuk)
|
||||
- [32fc0ff](https://github.com/airbnb/superset/commit/32fc0ff6d0b437766d16db128a7b1d40a09080bb) [Bugfix] autocomplete in sqleditor doesnot use newly loaded table columns (#1712) (@vera-liu)
|
||||
|
||||
### 0.14.1 (2016/11/29 23:57 +00:00)
|
||||
- [94dde07](https://github.com/airbnb/superset/commit/94dde075b3eab41797725e1e02c7f87b6b45471a) v0.14.1 (@mistercrunch)
|
||||
- [65e9232](https://github.com/airbnb/superset/commit/65e92327abdd0d521a9dcb65319165b163da356c) Druid hotfix. (#1710) (@bkyryliuk)
|
||||
- [0be02e6](https://github.com/airbnb/superset/commit/0be02e67a554d323efe4ed119a59bba53c559477) Updating CHANGELOG 0.14.0 (@mistercrunch)
|
||||
- [7327c97](https://github.com/airbnb/superset/commit/7327c97e4c5efcf7c5b080a1e534d7b44129bb8b) v0.14.0 (@mistercrunch)
|
||||
|
||||
### 0.14.0 (2016/11/29 23:03 +00:00)
|
||||
- [03b21dc](https://github.com/airbnb/superset/commit/03b21dcf0a3fc18e1290f7770004d3b74df8cef3) [explorev2] Bug fixes in Save Modal (#1707) (@vera-liu)
|
||||
- [dc98c67](https://github.com/airbnb/superset/commit/dc98c6739fcccc8edc60ef7e761cb1491005f644) Implement table name extraction. (#1598) (@bkyryliuk)
|
||||
- [fcb8707](https://github.com/airbnb/superset/commit/fcb870728db69bbee092d20c3f78cb7785fe2e61) Add per schema permissions. (#1698) (@bkyryliuk)
|
||||
- [7919428](https://github.com/airbnb/superset/commit/7919428a1e02457a50ae00439e827f996403f71c) Vliu explorev2 bugs (#1701) (@vera-liu)
|
||||
- [3496a80](https://github.com/airbnb/superset/commit/3496a80f5a85a0b66e59ec259ed13ca9ba3d5ba0) make stack trace more readable (#1672) (@ascott)
|
||||
- [56b917a](https://github.com/airbnb/superset/commit/56b917a5c206d3083d9d9d3d0606b976c64b6044) [explore-v2] fix errors on table view (#1675) (@ascott)
|
||||
- [18c43aa](https://github.com/airbnb/superset/commit/18c43aaea2f889e50211b22f0a68269f314bcafa) make chart title larger, fix explore actions btn spacing (#1680) (@ascott)
|
||||
- [c43fc38](https://github.com/airbnb/superset/commit/c43fc38f69d6284729cd47368e796117adcc1d1b) [druid] fix having clause (#1694) (@mistercrunch)
|
||||
- [c07f0ab](https://github.com/airbnb/superset/commit/c07f0ab9c72430f5892f701d6cba35718ef322ad) Config programmatic roles in the config.py (#1664) (@bkyryliuk)
|
||||
- [1c429b2](https://github.com/airbnb/superset/commit/1c429b27bc425aa8ba0f8cc6b43887cfb91dcd15) Fixing issue #1689 (#1696) (@mistercrunch)
|
||||
- [b7019ad](https://github.com/airbnb/superset/commit/b7019ad4f343ecbd5d33ce4a5800a72a9f4301b6) [sqllab] bugfix SouthPane doesn't update as expected (#1699) (@mistercrunch)
|
||||
- [84e8f74](https://github.com/airbnb/superset/commit/84e8f741ae969888c4f2501ada132f58bdcfb249) Add 'Save As' feature for dashboards (#1669) (@the-dcruz)
|
||||
- [e3a9b39](https://github.com/airbnb/superset/commit/e3a9b393c26ab173fe3ffe3dd14191705cab7119) Missing merge_perm function. Fixes 1691. (#1692) (@niconoe)
|
||||
- [16aba51](https://github.com/airbnb/superset/commit/16aba517e4640300c9a71f6186776671540bc488) Use smaller size for node max_old_space_size (#1679) (@xrmx)
|
||||
- [205928e](https://github.com/airbnb/superset/commit/205928e6df892060cdd3ffe0af6a1217a848f301) docs: fix python-redis link markup (#1683) (@xrmx)
|
||||
- [39ce4aa](https://github.com/airbnb/superset/commit/39ce4aa049fffef3b9f6e368d64130ae85cb86d8) Added filter in ControlPanelsContainer for explore V2 (#1647) (@vera-liu)
|
||||
- [cef4a82](https://github.com/airbnb/superset/commit/cef4a8296a6a9d46503dd63e268be3a35e9e8e91) [sqllab] adding a sql preprocessor for Presto (#1670) (@mistercrunch)
|
||||
- [b370ef0](https://github.com/airbnb/superset/commit/b370ef0229377c6b85f78d9ba080d00ff6dba58e) Rerender chart without clicking query button for fields (#1658) (@vera-liu)
|
||||
- [6b80f5b](https://github.com/airbnb/superset/commit/6b80f5bb35e497c79fe458b25ba87266e3c0f3bf) Get sections to render when switching datasource (#1660) (@vera-liu)
|
||||
- [bdae570](https://github.com/airbnb/superset/commit/bdae570a69cd948987b05fed2e7653a221ef0d80) Temperary fix of a slice bug (#1648) (@vera-liu)
|
||||
- [face524](https://github.com/airbnb/superset/commit/face5245a99d13089b9fa4cfa7521ee2ca6b209c) Make explore container resize with browser window (#1608) (@vera-liu)
|
||||
- [db1ed2a](https://github.com/airbnb/superset/commit/db1ed2a765d317e55377f2550f169b78f981b4a0) Calculate height dynamically using jquery for scrollable sqllab (#1611) (@vera-liu)
|
||||
- [10982de](https://github.com/airbnb/superset/commit/10982dec3c69f1bed709b38616417eada995d2f4) Make QueryTable scrollable in Query Search page (#1656) (@vera-liu)
|
||||
- [6825e75](https://github.com/airbnb/superset/commit/6825e75681b1249d066d9fa0bf0dca9f1824bb24) Fixed bug with querylink passing sql object instead of string (#1659) (@vera-liu)
|
||||
- [bd6a439](https://github.com/airbnb/superset/commit/bd6a439e0b2a3a76f8aece91f11a7eee2ebf6d29) [QuerySearch] Add loading status to QuerySearch page (#1657) (@vera-liu)
|
||||
- [c90dd49](https://github.com/airbnb/superset/commit/c90dd4902f18bb11c46bc38b8f70bfc14cfc2171) Programatically sync the role with user list. (#1619) (@bkyryliuk)
|
||||
- [868e5c4](https://github.com/airbnb/superset/commit/868e5c45fed8e090750dffe88660f3943f373c19) Redirect URL requests with "caravel" to "superset" (#1651) (@kingo55)
|
||||
- [7e1852e](https://github.com/airbnb/superset/commit/7e1852ee883628d38b2e3bb71e2b2b03fad41ba3) User profile pages (favorites, created content, recent activity, security & access) (#1615) (@mistercrunch)
|
||||
- [5ae98bc](https://github.com/airbnb/superset/commit/5ae98bc7c9b432683d03d30a30631a6efd7a78a3) Improving jinja2 security by using SandboxedEnvironment (#1632) (@mistercrunch)
|
||||
- [1624e7d](https://github.com/airbnb/superset/commit/1624e7de7dd50f1c4f5fdd9153adac4ba5b983d2) Add all_tables endpoint to allow airpal / superset perm sync. (#1614) (@bkyryliuk)
|
||||
- [7a98f84](https://github.com/airbnb/superset/commit/7a98f848909ca2099e29d3f485fd299037142e65) Admin / Alpha permission cleanup and fixes. (#1645) (@bkyryliuk)
|
||||
- [9b18128](https://github.com/airbnb/superset/commit/9b181280d44171cb0c724a07f50488eb08f98e72) include jQuery and bootstrap (#1642) (@ascott)
|
||||
- [38e94b9](https://github.com/airbnb/superset/commit/38e94b9e43f82c682f311fe1563c8f502ae4157a) Save modal component for explore v2 (#1612) (@vera-liu)
|
||||
- [dc25bc6](https://github.com/airbnb/superset/commit/dc25bc6f4d5eeb74665dd353bafda5d97ef5faa1) Fix alpha permission checks. (#1641) (@bkyryliuk)
|
||||
- [f64a205](https://github.com/airbnb/superset/commit/f64a2056038e96883e31419df5fcd4fa396dffb6) Use Alert for visualization error (#1639) (@vera-liu)
|
||||
- [a8480f5](https://github.com/airbnb/superset/commit/a8480f54922775992a28edd7878b1cfa7690264e) Added Alert for ControlPanel and ChartContainer (#1626) (@vera-liu)
|
||||
- [0acf26b](https://github.com/airbnb/superset/commit/0acf26b37c7a59cb976cf7a929caf7cc5a1a968e) Fixed a bug with switching viz_type in exploreV2 (#1631) (@vera-liu)
|
||||
- [2c068a1](https://github.com/airbnb/superset/commit/2c068a1a1583fa61db2f1797b0fcb2618cd6dbe3) increase space between fieldsset rows (#1629) (@ascott)
|
||||
- [b961c95](https://github.com/airbnb/superset/commit/b961c95121e5e4d4342a2926746dbf8a62bd77ea) dim visualization during refresh (#1636) (@mistercrunch)
|
||||
- [8269321](https://github.com/airbnb/superset/commit/82693211f0545affbdc306561a1abb4478c2de9a) Update faq.rst (#1637) (@dodysw)
|
||||
- [e546746](https://github.com/airbnb/superset/commit/e5467462cb73630a9b487891845ab1f01245f2a8) Make nvd3 refresh smoother. (#1618) (@the-dcruz)
|
||||
- [ab5a410](https://github.com/airbnb/superset/commit/ab5a4102cd8921ca2df234bfa6133973ba83a425) [dashboard] give user feedback when there are unsaved changes (#1633) (@ascott)
|
||||
- [d5ef937](https://github.com/airbnb/superset/commit/d5ef937b315f4afc679349369b4e7ac7455748f0) Fixed bugs with viz in exploreV2 (#1609) (@vera-liu)
|
||||
- [bce02e3](https://github.com/airbnb/superset/commit/bce02e3f518237c03273e3ed4d9d1a13d9f8f6a9) [security] improving the security scheme (#1587) (@mistercrunch)
|
||||
- [aad9744](https://github.com/airbnb/superset/commit/aad9744d85b50721d55d5770aad70ba1ee397ede) add new screenshots (#1589) (@ascott)
|
||||
- [506b781](https://github.com/airbnb/superset/commit/506b781f3a6048b433c12d25c1dbce614b5bd31b) [explore-v2] add fave star and edit button to chart header (#1623) (@ascott)
|
||||
- [267fd5b](https://github.com/airbnb/superset/commit/267fd5b9bc4f21a55c4664ae8c3ee717cc1be82c) [table viz] adding support for pagination (#1616) (@mistercrunch)
|
||||
- [c362f28](https://github.com/airbnb/superset/commit/c362f2869e012a4eeb9b76ff654ee3e82a190979) More Dashboard UX unit tests (#1603) (@mistercrunch)
|
||||
- [4f7f437](https://github.com/airbnb/superset/commit/4f7f43752798f57daa8cd8b8ed8a9cbc9c948000) Vliu put datasource in store (#1610) (@vera-liu)
|
||||
- [ab5da5b](https://github.com/airbnb/superset/commit/ab5da5ba2811ac6c2350c7d0534dd209906318af) [table viz] allow sorting on any column (#1601) (@mistercrunch)
|
||||
- [7531bb8](https://github.com/airbnb/superset/commit/7531bb89429547fb541c36fe365791cd742d82a1) Fixed dashboard controls for standalone bug (#1617) (@vera-liu)
|
||||
- [811ee8c](https://github.com/airbnb/superset/commit/811ee8ccdc76a2630a4c8014df26558391b981fe) Deleted unused components in exploreV2 (#1613) (@vera-liu)
|
||||
- [51cb485](https://github.com/airbnb/superset/commit/51cb485ce3e8cb80c72ec8c732281a78441396fd) Add standalone to reactified dashboard page (#1596) (@vera-liu)
|
||||
- [83d08b8](https://github.com/airbnb/superset/commit/83d08b8b8f7c73cbf4de25cadeab93dd3fdfc2fc) Get query button working in explorev2 (#1581) (@vera-liu)
|
||||
- [ed3d44d](https://github.com/airbnb/superset/commit/ed3d44d5919fc2ba739cf8d82e75e2680630646d) Changelog entries for 0.13.2 (@mistercrunch)
|
||||
|
||||
|
||||
### 0.13.2 (2016/11/16 00:23 +00:00)
|
||||
- [895fe23](https://github.com/airbnb/superset/commit/895fe23203a85a4590f84625507849ce63d69f30) v0.13.2 (@mistercrunch)
|
||||
- [af04a56](https://github.com/airbnb/superset/commit/af04a560c887ecbcee40b53c358ee9c2ad2f44ad) Moved check to the correct place. (#1606) (@edevil)
|
||||
- [9124a17](https://github.com/airbnb/superset/commit/9124a17e864b8b2eb109af33fe1b8aad809069da) Removing ascii_art.p from code coverage analysis (@mistercrunch)
|
||||
- [99b0d4c](https://github.com/airbnb/superset/commit/99b0d4c111b66f6da0eb9991b54b375e2fbeecc4) Fix MySql time grain issue (#1590) (@mistercrunch)
|
||||
- [84b98c2](https://github.com/airbnb/superset/commit/84b98c234f852550ecf536e4a6e7ce2d7ebc5df6) Adding Greenplum to supported dbs (@mistercrunch)
|
||||
- [bcc1428](https://github.com/airbnb/superset/commit/bcc1428ebf1cf7e83c93e351858bee3cfbb2e9c2) Updating CODECLIMATE_REPO_TOKEN to new location (@mistercrunch)
|
||||
- [2133056](https://github.com/airbnb/superset/commit/2133056c04d20807ea0c503d0fed235ee20e94bb) Added different Select Fields (#1583) (@vera-liu)
|
||||
- [4155a9d](https://github.com/airbnb/superset/commit/4155a9d7f996d09ebdfc8df0db3dcbe9ccf9b529) Removing broken link to old docker image (#1591) (@kingo55)
|
||||
- [ed48255](https://github.com/airbnb/superset/commit/ed4825523ca54309272f044826f383c2606456a1) Fixed a bug with new dashboard (#1585) (@vera-liu)
|
||||
- [fdbb2bb](https://github.com/airbnb/superset/commit/fdbb2bbdab5e5dbb2f3496b67eb227dc3dc5f2a7) fixing the build (@mistercrunch)
|
||||
- [c064d6d](https://github.com/airbnb/superset/commit/c064d6d8475b07a63a3b5ca7b4dbd248a437f6ed) Correct part_fields variable name (#1586) (@geraneum)
|
||||
- [d33874b](https://github.com/airbnb/superset/commit/d33874bd3d9ffffca7f4726a29c3eb9de2a68d42) [hotfix] postgres issue when slice_id is missing (@mistercrunch)
|
||||
- [96d32dd](https://github.com/airbnb/superset/commit/96d32dd11f29afa3590b79d8683aaddd05f48a02) Improve Druid metadata fetching resilience (#1584) (@mistercrunch)
|
||||
- [d6bc354](https://github.com/airbnb/superset/commit/d6bc354ff3e2f24aeb459dbc1413371f2b072306) [hotfix] fix support for presto DATE and TIMESTAMP type (@mistercrunch)
|
||||
- [7325a4f](https://github.com/airbnb/superset/commit/7325a4fb4ba08f554454534fe9efe3d0eea5a6ce) [hotfix] table view not group by without orderby fails (@mistercrunch)
|
||||
- [90f00c5](https://github.com/airbnb/superset/commit/90f00c5b292ff83802d35bac49a26e6b257de409) Minor documentation touchups (@mistercrunch)
|
||||
|
||||
### 0.13.1 (2016/11/10 18:01 +00:00)
|
||||
- [8539c42](https://github.com/airbnb/superset/commit/8539c423ea61d84e8e0a81317275713103f99a8a) v0.13.1 (@mistercrunch)
|
||||
- [e9bfbfc](https://github.com/airbnb/superset/commit/e9bfbfce84b5ab851c839c70adf5298b2538e9dc) Removing boat pic from README (@mistercrunch)
|
||||
- [6e4f066](https://github.com/airbnb/superset/commit/6e4f0664cb49d5e7144dadba7ccda548cf58e905) [hotfix] lint (@mistercrunch)
|
||||
- [3c920c9](https://github.com/airbnb/superset/commit/3c920c9d943540cc8ed0d6e3dfd2ae0eba3acb70) [hotfix] datatables import issues (@mistercrunch)
|
||||
- [15b67b2](https://github.com/airbnb/superset/commit/15b67b2c6c3c2982f6620fce5d30bd05951458f7) [WiP] rename project from Caravel to Superset (#1576) (@mistercrunch)
|
||||
|
||||
### 0.13.0 (2016/11/10 05:37 +00:00)
|
||||
- [973537f](https://github.com/airbnb/superset/commit/973537fd9a60766a6ee99bd2e7080aa7db21f540) [hotfix] resizing widgets (@mistercrunch)
|
||||
- [d70a744](https://github.com/airbnb/superset/commit/d70a74479df87908de7a7b4df7c24c6b267bf9e3) Make Sqllab a one-page app -- body not scrollable (#1551) (@vera-liu)
|
||||
- [946e4b7](https://github.com/airbnb/superset/commit/946e4b750afeebbfa16e6ce7e9fc61575136b237) Reactifying the dashboard (#1572) (@mistercrunch)
|
||||
- [9789e3f](https://github.com/airbnb/superset/commit/9789e3fb9b658e1f38080915132ae43c541e68e9) Bind data preview tabs to sql editor (#1573) (@vera-liu)
|
||||
- [6a15679](https://github.com/airbnb/superset/commit/6a15679d876c5c76d177b624a5d69da80ac75a3f) [hotfix] encode csv to utf-8 (@mistercrunch)
|
||||
- [ad1cd55](https://github.com/airbnb/superset/commit/ad1cd5577c231e4100f5a214fe7a4d372de96a04) Pass values from global store to fields in exploreV2 (#1561) (@vera-liu)
|
||||
- [55668ca](https://github.com/airbnb/superset/commit/55668ca6217f52924288f3849a0aa54c28d40ce1) Link to database-urls in databaseadd view (#1480) (@dirkkelly)
|
||||
- [0c221a2](https://github.com/airbnb/superset/commit/0c221a28d10abc8cf4e929e50fd788a56136a9cf) add slice_name and table_name for title (#1567) (@ascott)
|
||||
- [a475551](https://github.com/airbnb/superset/commit/a475551b23d5830ab2945f615328f31d48df36ca) [sqllab] bind alt+enter shortcut in AceEditor to run a query (#1554) (@mistercrunch)
|
||||
- [bad7676](https://github.com/airbnb/superset/commit/bad7676414662b28a4b72eb680fbf42a5c6281a5) Bump cryptography dependency to 1.5.3 (#1569) (@xrmx)
|
||||
|
||||
### airbnb_prod.0.12.0.1 (2016/11/08 23:55 +00:00)
|
||||
- [51c0470](https://github.com/airbnb/superset/commit/51c0470f0be438312e90f2efb1f2e37291a30ce4) [explore v2] populate dynamic select field options (#1543) (@ascott)
|
||||
- [4530047](https://github.com/airbnb/superset/commit/4530047c769ba6d5953ef1547b8507c62f657942) Added action buttons to Chart Container of explore V2 (#1562) (@vera-liu)
|
||||
- [1bf83c3](https://github.com/airbnb/superset/commit/1bf83c3bf78de422df1b21c3049d106b1cb29385) [explore-v2] render columns based on length of fieldSets array (#1559) (@ascott)
|
||||
- [bb6ab11](https://github.com/airbnb/superset/commit/bb6ab110013f2e29933fb9a70d6891d4486eb49d) Vliu link form data explore v2 (#1540) (@vera-liu)
|
||||
- [e4bd188](https://github.com/airbnb/superset/commit/e4bd1884d34a1949b986410ebc385b75945afff8) [druid] adding support for dimensionspecs (#1545) (@mistercrunch)
|
||||
- [4014a48](https://github.com/airbnb/superset/commit/4014a48f7df9fe166c77132f3d8d83cf615ac176) Added cache prop to ResultSet (#1552) (@vera-liu)
|
||||
- [97ded32](https://github.com/airbnb/superset/commit/97ded32415e9e32ba2c6a7e4556a0ed96034244a) Update linting instructions. (#1478) (@pinkythalli97)
|
||||
- [593ac08](https://github.com/airbnb/superset/commit/593ac081f06af5e95dc784597473d42aea52cf28) Added scroll bar and option to collapse for Sql Editor tool bar (#1532) (@vera-liu)
|
||||
- [69f0a4e](https://github.com/airbnb/superset/commit/69f0a4e1cb2c7775617a15a5ba709836a568210c) Put data preview in south pane (#1486) (@vera-liu)
|
||||
- [757e7de](https://github.com/airbnb/superset/commit/757e7de60cf7aa04f15160942ceaadb46daa15b5) add oracle time_grains (#1544) (@gschrader)
|
||||
- [1d7d546](https://github.com/airbnb/superset/commit/1d7d5469a925690d4b4fd1e7a3cdf37779acffb6) [hotfix] remove failing Druid test (@mistercrunch)
|
||||
- [98afc3e](https://github.com/airbnb/superset/commit/98afc3e590ef25d08fecf159bf4411ac292e95f0) Added setFilter(), containerID and getFilter() to (#1360) (@vera-liu)
|
||||
- [ea18979](https://github.com/airbnb/superset/commit/ea189790f160dc6419ac97d8f6740afa62ba6e78) [hotfix] druid dist_bar viz issues with non-str x values (@mistercrunch)
|
||||
- [6298707](https://github.com/airbnb/superset/commit/62987077fa31ebbedafbaa745b36527b7df0c9f7) Read the user origin specification. (#1541) (@bkyryliuk)
|
||||
- [3b9f7cb](https://github.com/airbnb/superset/commit/3b9f7cb3f1d0dd1387d01204259b5ce7f2469793) [hotfix] groupby may be a set (@mistercrunch)
|
||||
- [5882c7e](https://github.com/airbnb/superset/commit/5882c7e3447a06875e63b4424994a0a5e5fc58a5) Added jquery methods to ChartContainer to get world_map viz working in exploreV2 (#1443) (@vera-liu)
|
||||
- [77b6e2c](https://github.com/airbnb/superset/commit/77b6e2cd2e796d063dbc6d42934143bf45bba8b0) Get pivot table working in explore v2 (#1432) (@vera-liu)
|
||||
- [88b1f95](https://github.com/airbnb/superset/commit/88b1f956c7cd8bd457d04648447fb23700b859ae) [explore-v2] handle field overrides (#1535) (@ascott)
|
||||
- [d9b49ca](https://github.com/airbnb/superset/commit/d9b49ca2bc1eab3221fc329d0ab5a9b421746110) [exploreV2] remove /exploreV2 endpoint, add v2 bootstrap data to /explore endpoint (#1536) (@ascott)
|
||||
- [4156ad5](https://github.com/airbnb/superset/commit/4156ad5a3054f5194ea94b6fc146e53e4ccb0b57) [explore-v2] control panel fixes (#1529) (@ascott)
|
||||
- [ae46561](https://github.com/airbnb/superset/commit/ae465616482c8e76634728bcd6990fcfb3b752ad) Support week_ending_saturday for Druid. (#1491) (@bkyryliuk)
|
||||
- [1700a80](https://github.com/airbnb/superset/commit/1700a807e9a8ebc4cfb2749293308be733e42473) [sqllab] templating refactor (#1504) (@mistercrunch)
|
||||
- [0bab15b](https://github.com/airbnb/superset/commit/0bab15b2132ae769ce81519f2d27858ad6d0b187) Update INTHEWILD.md (#1526) (@shashanksingh)
|
||||
- [38d3075](https://github.com/airbnb/superset/commit/38d307555487d84b60dd6169006bbb6aa9fc87a5) [explore V2] render all control panels and fields dynamically for each vis type (#1493) (@ascott)
|
||||
- [1b124bf](https://github.com/airbnb/superset/commit/1b124bfb87862a25a7b904e05929c23f0c61d0e2) [druid] optimize Druid queries where possible (#1517) (@mistercrunch)
|
||||
- [cdf4dd0](https://github.com/airbnb/superset/commit/cdf4dd03024cff40f17845ab9b57ae6a6070e197) Add yearly and quarterly granularities to mysql engine backend (#1518) (@plumbeo)
|
||||
- [a13bed2](https://github.com/airbnb/superset/commit/a13bed2db685a9fa913676bd71b570c81a190c6e) Moved sqllab tests from core_tests to sqllab_tests (#1502) (@vera-liu)
|
||||
- [26318f9](https://github.com/airbnb/superset/commit/26318f94fef888a4b54a022bbbd44f269cfbf9b3) Moved queriesArray from render() to local state (#1505) (@vera-liu)
|
||||
- [769fb08](https://github.com/airbnb/superset/commit/769fb0820fbefd1e58ced40fa79d2d7c923091e4) Strip sql and remove ; for csv download. (#1508) (@bkyryliuk)
|
||||
- [5238053](https://github.com/airbnb/superset/commit/52380534f360cbb0e3d7ab46889066cd2df47440) Moved ajax call for fetching table metadata from SqlEditorLeftBar to actions (#1494) (@vera-liu)
|
||||
- [2fd2526](https://github.com/airbnb/superset/commit/2fd252604689693c957a1b6875a18872a59d5ec8) Add support for jinja templates in WHERE/HAVING clauses (#1442) (@mistercrunch)
|
||||
- [61509bb](https://github.com/airbnb/superset/commit/61509bbd446bbcc21f4f79229c88d82441d1fb98) [sqllab] surfacing more table metadata (indices, pk, fks) (#1485) (@mistercrunch)
|
||||
- [76499af](https://github.com/airbnb/superset/commit/76499afd8d28289cac0cf0f2d7316e6e64bab089) [pep8] allowing 90 chars per line (@mistercrunch)
|
||||
- [4023f32](https://github.com/airbnb/superset/commit/4023f328f7893fbe4b0e0af1612d598e1c931f72) [sqllab] run only the part of the query that is selected (#1479) (@mistercrunch)
|
||||
- [4f49cb5](https://github.com/airbnb/superset/commit/4f49cb555be68c5e1daed817d1137fbd1c3f4e3f) Celery uses separate db engine with NullPool. (#1492) (@bkyryliuk)
|
||||
- [4dc959a](https://github.com/airbnb/superset/commit/4dc959a3e4464ed38d4b5e580ae1fc009178e185) Revert "NullPool for the celery worker." (#1488) (@bkyryliuk)
|
||||
- [518fbf5](https://github.com/airbnb/superset/commit/518fbf562cca638cf14d97dfae92756091630eb2) Minor Fixes (#1484) (@ronbak)
|
||||
- [49828d3](https://github.com/airbnb/superset/commit/49828d3d9d51c02b13ce916a85cfe912bee43c54) add step to pypi build/push (@mistercrunch)
|
||||
- [248e6a7](https://github.com/airbnb/superset/commit/248e6a7b05fd4ef30c4f2c006d0df935da84e052) fix name for postgresql (#1482) (@willgroves)
|
||||
- [5561e6b](https://github.com/airbnb/superset/commit/5561e6b77086ffafb58c363a22e97a5800590e47) Fix celery module import in comments. (#1474) (@bkyryliuk)
|
||||
- [ab083b8](https://github.com/airbnb/superset/commit/ab083b86f35c6f01dfa16bf84c8c212bf21743cf) [sqllab] slide animations when adding/removing/toggling TableElement (#1472) (@mistercrunch)
|
||||
- [4bf5252](https://github.com/airbnb/superset/commit/4bf525222a609320acc28232f25b7651c54cfda0) [sqllab] add autocomplete to AceEditor for table and column names (#1475) (@mistercrunch)
|
||||
- [45efcb3](https://github.com/airbnb/superset/commit/45efcb381c0d0b53b9de72a3437ec980b201bab0) Added time filter to query search page (#1329) (@vera-liu)
|
||||
- [07a7736](https://github.com/airbnb/superset/commit/07a7736c71050c20dc04661ab3f1d21f58cb3b39) NullPool for the celery worker. (#1465) (@bkyryliuk)
|
||||
- [d2826ab](https://github.com/airbnb/superset/commit/d2826ab7af4da1b36816150b71002ed966c553cd) Added checkbox in dist_bar viz to enable sorting of bars based on x axis labels (#1379) (@vera-liu)
|
||||
- [6ab769f](https://github.com/airbnb/superset/commit/6ab769f38227788fbffb1eadb0e43c656f7c1da0) CHANGELOG for 0.12.0 (@mistercrunch)
|
||||
|
||||
### 0.12.0 (2016/10/28 16:40 +00:00)
|
||||
- [3e1cd2b](https://github.com/airbnb/caravel/commit/3e1cd2bdcabce219dc01c6ce7b80850ecd50f9ba) v0.12.0 (@mistercrunch)
|
||||
- [22784b7](https://github.com/airbnb/caravel/commit/22784b7f069d59e3fa7df03cfea84df9e147af13) run_specific_test: take the test as parameter (#1469) (@xrmx)
|
||||
- [c492261](https://github.com/airbnb/caravel/commit/c4922615eb707228cdc1badd49bf2d293c74a699) [sqllab] add column sort feature to TableElement (#1467) (@mistercrunch)
|
||||
- [7307dda](https://github.com/airbnb/caravel/commit/7307ddad3c9b60ec2178286cbc27a302e158de83) Highlight affected slices for filter change in dashboard view (#1439) (@vera-liu)
|
||||
- [c7ba143](https://github.com/airbnb/caravel/commit/c7ba143d039aff61302017329860cd3db432b0f6) Fix typo in sqllab docs (@mistercrunch)
|
||||
- [b242063](https://github.com/airbnb/caravel/commit/b24206387b09946df40a834db2d3b1deae21f865) [sqllab] optimizing React (#1438) (@mistercrunch)
|
||||
- [64d1964](https://github.com/airbnb/caravel/commit/64d196442fcd17f098ba6d71da8bad1f84bdbb5a) Added dashboard standalone page (#1429) (@vera-liu)
|
||||
- [5944643](https://github.com/airbnb/caravel/commit/5944643da67acb1ec38ed8d52f9ae6f58d7549ac) [sqllab] add support for Jinja templating (#1426) (@mistercrunch)
|
||||
- [8c5e495](https://github.com/airbnb/caravel/commit/8c5e4952727d7ff6e96e61c1a38de2e600b02208) Add github issue template (#1436) (@xrmx)
|
||||
- [bb23685](https://github.com/airbnb/caravel/commit/bb23685b9db623be89714153361ee3b03b46a39d) Added average metric AVG() to default metrics (#1413) (@vera-liu)
|
||||
- [940659b](https://github.com/airbnb/caravel/commit/940659bc14ed276a68478b66055356cf3b48ba29) [sqllab] some frontend tests (#1400) (@mistercrunch)
|
||||
- [7c59337](https://github.com/airbnb/caravel/commit/7c5933732bcb199e486ac8c4bf7ecec71013117d) Filter immune slices array stores strings. (#1402) (@bkyryliuk)
|
||||
- [89df2fc](https://github.com/airbnb/caravel/commit/89df2fcf76bdaca34dfef19a9d5bb752a9859c6d) Adjusted top margin of heatmap plot to get it working in V2 (#1361) (@vera-liu)
|
||||
- [174a199](https://github.com/airbnb/caravel/commit/174a199c30ad6c24505092975b814688e8a37292) [hotfix] Query search is unreachable (@mistercrunch)
|
||||
- [6f1e7c3](https://github.com/airbnb/caravel/commit/6f1e7c3016b53cf8d3c0e28dba3538861fe56086) Added url shortner for sharing query link (#1314) (@vera-liu)
|
||||
- [9f81e23](https://github.com/airbnb/caravel/commit/9f81e23f8f0698c94a9f4c34b49cbf3dda1e2e87) Fixed css class not being used by slice container (#1359) (@vera-liu)
|
||||
- [19fab6e](https://github.com/airbnb/caravel/commit/19fab6eea71bbd97974a0ef8dd74451011f3862f) Get table viz work in explore v2: Added d3 format to mock slice (#1353) (@vera-liu)
|
||||
- [63161b1](https://github.com/airbnb/caravel/commit/63161b11c347d5a6d62f7ae7dc91fa3c30b5dc93) [sqllab] proper, quoted, select * on the server side (#1404) (@mistercrunch)
|
||||
- [4f886d6](https://github.com/airbnb/caravel/commit/4f886d65ecc208149cf9b7663492a81df868dcc2) Fix None view_menues in permissions. (#1409) (@bkyryliuk)
|
||||
- [62e0e19](https://github.com/airbnb/caravel/commit/62e0e195e8eaa53afa41a5fec89cc4486a7114a3) [docfix] d3.format docs have moved (@mistercrunch)
|
||||
- [e9d4749](https://github.com/airbnb/caravel/commit/e9d4749f4470aee294f1351baa9422173a2a62c8) [hotfix] sqllab presto (@mistercrunch)
|
||||
- [93f8e7d](https://github.com/airbnb/caravel/commit/93f8e7d8e9b25a1aaf07e2b0c3772c327e523f0e) Fix the js build running out of heap space (#1408) (@mistercrunch)
|
||||
- [3dea6e0](https://github.com/airbnb/caravel/commit/3dea6e0da538ed1c0a7761ccbf97bd8f12ac7f2f) [sqllab] adding more descriptive labels to left panel (#1407) (@mistercrunch)
|
||||
- [6fb3b30](https://github.com/airbnb/caravel/commit/6fb3b305ad23c27d0555ded2ab80820000fdec50) [sqllab] add support for results backends (#1377) (@mistercrunch)
|
||||
- [7dfe891](https://github.com/airbnb/caravel/commit/7dfe891cc1d294bd55982d63c9c1eb8f9e9e4c25) [hotfix] timeseries_limit_metric: Not a valid choice (@mistercrunch)
|
||||
- [5c3966a](https://github.com/airbnb/caravel/commit/5c3966a32d454a2fc7fcabd75c66594662361586) Override the role with perms for given datasources. (#1399) (@bkyryliuk)
|
||||
- [c198535](https://github.com/airbnb/caravel/commit/c198535292869ae1ced6d66d08de536188be7e05) Change slice ids in the position json during dashboard import. (#1380) (@bkyryliuk)
|
||||
- [ece69fb](https://github.com/airbnb/caravel/commit/ece69fbb75086cb8855789881a32c6daca4be483) Fix migration for make creator owners (#1262) (@ShengyaoQian)
|
||||
- [458651f](https://github.com/airbnb/caravel/commit/458651fa3e0098f35412772b622e7c0b63d34299) Add parens for custom where and having (#1337) (@yejianye)
|
||||
- [b2f7081](https://github.com/airbnb/caravel/commit/b2f7081c6f3dde80846493330f1ec1e5fa3a414c) bumping versions of JS packages to latest (#1352) (@mistercrunch)
|
||||
- [c255e89](https://github.com/airbnb/caravel/commit/c255e89219c209b5a5371134fbb9a9b90036ded4) [sqllab] show partition metadata for Presto (#1342) (@mistercrunch)
|
||||
- [2edce5b](https://github.com/airbnb/caravel/commit/2edce5bf8afa7b74fdafd8ab8a2e6394d46e6391) Enable "Run Query in New Tab" in SQL Lab (#1343) (@nickbarnwell)
|
||||
- [8f29944](https://github.com/airbnb/caravel/commit/8f299448ea954f75f576399f6bc113dd8ac1c824) [bugfix] text as subquery fails with 'Series Limit' (#1347) (@mistercrunch)
|
||||
- [ecb951b](https://github.com/airbnb/caravel/commit/ecb951bb7474b9c829d0eef12792f6c146757dba) Specify the metric to order by for Series Limit (#1351) (@mistercrunch)
|
||||
- [0dff6a9](https://github.com/airbnb/caravel/commit/0dff6a9030d8f2ff7a03654258f3b02f01d9d57e) Add quarter time grain to postgresql (#1362) (@xrmx)
|
||||
- [2095095](https://github.com/airbnb/caravel/commit/2095095895d9cf5bc8131fb51fbb4868fae124dc) Fixed big number issue (#1355) (@vera-liu)
|
||||
- [4fc8a17](https://github.com/airbnb/caravel/commit/4fc8a17f2ae18dcc2be778e52b3d3ed7be29e95a) [hotfix] use instead of prod for Travis build, take 3 (@mistercrunch)
|
||||
- [3cb737f](https://github.com/airbnb/caravel/commit/3cb737f8c890b018bd6b3275047a02f79abb7444) [hotfix] use instead of prod for Travis build, take2 (@mistercrunch)
|
||||
- [7449aa8](https://github.com/airbnb/caravel/commit/7449aa813b8b695e6dc3b06eb25004bc2a55614f) [hotfix] use instead of prod for Travis build (@mistercrunch)
|
||||
- [7a3bcc2](https://github.com/airbnb/caravel/commit/7a3bcc227cccdec756404ab6140a6ba0d3882419) [bugfix] NaN issue in Big Number viz (#1346) (@mistercrunch)
|
||||
- [b669a14](https://github.com/airbnb/caravel/commit/b669a140816b950237b7d66b73ac6af392f28d8a) [explore-v2] make chart container work with existing visualization files (#1333) (@ascott)
|
||||
- [9db4cc8](https://github.com/airbnb/caravel/commit/9db4cc8c6d0422f28357dc4283089ee3dde9a08e) add node/npm versions to contributing.md (#1344) (@ascott)
|
||||
- [65c744f](https://github.com/airbnb/caravel/commit/65c744f2424d111e950b385630bd8a13b3fa003e) Fix utc time calculation if provided datetime has tz info (#1287) (@labeneator)
|
||||
- [82bcadf](https://github.com/airbnb/caravel/commit/82bcadf7f829183fbf6feca81dcc6c69f08c358c) Moving 'CSS TEmplates' to the Manage menu category (#1336) (@mistercrunch)
|
||||
- [f0f8478](https://github.com/airbnb/caravel/commit/f0f8478922cbe8e4ac754fddcfd6183032bbb893) Revert "Override the role with perms for give datasources." (#1345) (@bkyryliuk)
|
||||
- [40e7057](https://github.com/airbnb/caravel/commit/40e7057bcec55421275d0121d365a2e4f2d38a29) Override the role with perms for give datasources. (#1335) (@bkyryliuk)
|
||||
- [11a8e35](https://github.com/airbnb/caravel/commit/11a8e3591d281a8cde6fb0ab57ed54ec97035e23) Some dashboard import/export fixes. (#1340) (@bkyryliuk)
|
||||
- [5bea398](https://github.com/airbnb/caravel/commit/5bea3986b26359eb98c62246c34729e2d0cc5a1b) [hotfix] handling json errors in explore view (@mistercrunch)
|
||||
- [89cb726](https://github.com/airbnb/caravel/commit/89cb726284b2a4f2707f26d3004ef9ae59b33970) [hotfix] explore errors are not raise properly 2 (@mistercrunch)
|
||||
- [4e9392d](https://github.com/airbnb/caravel/commit/4e9392d21bf7f43e8189eb4478cf9d737c3616bd) [hotfix] explore errors are not raise properly (@mistercrunch)
|
||||
- [b785d27](https://github.com/airbnb/caravel/commit/b785d27241e9f22dcb6a880414500ce8f9e67eb3) Taking out object spread operator (#1311) (@vera-liu)
|
||||
- [451860a](https://github.com/airbnb/caravel/commit/451860afcab971a0a16b521a7c884d1c4d910986) remove #app styling (#1312) (@ascott)
|
||||
- [4cf4e38](https://github.com/airbnb/caravel/commit/4cf4e3805c4d0a217fa6232ba1d6ee3a68c86ed2) Bugfix when there's only date filter in FilterBox (no groupby) (#1326) (@yejianye)
|
||||
- [ef2670c](https://github.com/airbnb/caravel/commit/ef2670ca32e8ba0a04f9dc7899db55812e2f9b46) Using inheritance scheme to organize db specific code (#1294) (@mistercrunch)
|
||||
- [8626c80](https://github.com/airbnb/caravel/commit/8626c80d3a491b842f8262c5807154903c107747) Stop duplicating datasources (#1321) (@bkyryliuk)
|
||||
- [5cb3cc2](https://github.com/airbnb/caravel/commit/5cb3cc2ed8c8c2f0295da7ecdf8ec33a526acee1) polyfill es2015 in older browsers and for phantomjs (#1323) (@ascott)
|
||||
- [73cd2ea](https://github.com/airbnb/caravel/commit/73cd2ea3b17574f8fef1112aa5e5b39f843882f6) Import / export of the dashboards. (#1197) (@bkyryliuk)
|
||||
- [cd2ab42](https://github.com/airbnb/caravel/commit/cd2ab42abcd2ea5f93dd285321c448c97abf4580) do not overwrite the stored password with the masked password (#1209) (@dennisobrien)
|
||||
- [bf1f5ea](https://github.com/airbnb/caravel/commit/bf1f5ea3de23a62eca4f823474119e4c15270738) [sqllab] use encodeURIComponent for copy query URL (#1317) (@mistercrunch)
|
||||
- [79460ab](https://github.com/airbnb/caravel/commit/79460abdd230b2d53a9f2da15ac5160ba6db400b) [SQLLab] Fix the usage of Redux DevTools Enhancer (#1278) (@zalmoxisus)
|
||||
- [1e6e144](https://github.com/airbnb/caravel/commit/1e6e144d24da38aef0d306889dc3b8f962ca4b32) Fixed viewing dashboards as anonymous (#1320) (@Rapsutin)
|
||||
- [fe66557](https://github.com/airbnb/caravel/commit/fe66557bbb4848feed762e859abcd5884d72d369) [explore-v2] hook up ExploreViewContainer to state and add specs (#1300) (@ascott)
|
||||
- [f8e2ce6](https://github.com/airbnb/caravel/commit/f8e2ce6ff367047405be60d7cbcfd68e4f4521d8) Change status color in tab to match with success (#1247) (@vera-liu)
|
||||
- [1967743](https://github.com/airbnb/caravel/commit/19677438c23a9beab907b5b51d99f54ef62c8f92) Add cascade delete to the 1 to composite relationships. (#1295) (@bkyryliuk)
|
||||
- [9012b11](https://github.com/airbnb/caravel/commit/9012b11101805c9810745f049363223f110bc93e) add ImmutableMultiDict back to views.py (#1298) (@ascott)
|
||||
- [f70d301](https://github.com/airbnb/caravel/commit/f70d301f0d37fe6c4f94cd7d6026a72a56087d34) Refactor the explore view (#1252) (@mistercrunch)
|
||||
- [b7d1f78](https://github.com/airbnb/caravel/commit/b7d1f78f5e38bdcc99c7b6a15645092f5d76ca4a) Put formData in store (#1281) (@vera-liu)
|
||||
- [3384e75](https://github.com/airbnb/caravel/commit/3384e7598ee9e82f8b83ae71e3e5b587c6546443) Fixing explore actions & slice controller interactions (#1292) (@mistercrunch)
|
||||
- [382b8e8](https://github.com/airbnb/caravel/commit/382b8e85da36ed1a8f235775b883e54f5db90ab7) [explore v2] add scrollbar to control panel container (#1284) (@ascott)
|
||||
- [0a3121c](https://github.com/airbnb/caravel/commit/0a3121c2438f74ab1bd3730eac7a11bad0b317f5) [doc] installation, load examples before init (@mistercrunch)
|
||||
- [ecfe1a2](https://github.com/airbnb/caravel/commit/ecfe1a241786bc1fae1d5d3c45b45e21703e33a7) Updated eslinter for object rest spread (#1289) (@vera-liu)
|
||||
- [609ae22](https://github.com/airbnb/caravel/commit/609ae22bdabfb9a9cb99d1a4bfba62367f03a8d5) less number of default workers. (#1206) (@StefanoOrdine)
|
||||
- [94578cb](https://github.com/airbnb/caravel/commit/94578cb6a7e91cb4ca741a8f437636d264b56322) reduce chunk size for countries table (#1279) (@vivo75)
|
||||
- [8a5f050](https://github.com/airbnb/caravel/commit/8a5f050f6cbf2a4eca7f2799a244f94218bb11c1) [explore v2] fix explorev2 chart errors (#1277) (@ascott)
|
||||
- [5c5b393](https://github.com/airbnb/caravel/commit/5c5b393f2fd7be5a4496c0ea0a0808f720fdc904) Change userId, dbId to username and dbname (#1274) (@vera-liu)
|
||||
- [f837733](https://github.com/airbnb/caravel/commit/f837733d858643152304469c899c096e31d2bf24) [explorev2] chart and controls (#1251) (@ascott)
|
||||
- [66b498d](https://github.com/airbnb/caravel/commit/66b498de25b6bb3145eb441f64ef7fdaffc5a747) Added controls for Table Viz (#1253) (@vera-liu)
|
||||
- [659bf6d](https://github.com/airbnb/caravel/commit/659bf6d7e80d70def5b6c46a9d8a3a262e37a13e) Moved time column and grains to models.py (#1255) (@vera-liu)
|
||||
- [a8a1690](https://github.com/airbnb/caravel/commit/a8a16900e7497083f14de3b6dc50063f845401b7) docs: add libsasl as system requirement on linux (#1257) (@xrmx)
|
||||
- [e50b59e](https://github.com/airbnb/caravel/commit/e50b59e5535bc653e3321a2630287540ef99df86) docs: document that gunicorn does not work on windows (#1258) (@xrmx)
|
||||
- [231804e](https://github.com/airbnb/caravel/commit/231804e2b47771ed6b31cb48e36c2f417973cad1) CHANGELOG: Add proper credit to tan31989 for #744 (#1259) (@xrmx)
|
||||
- [421a86a](https://github.com/airbnb/caravel/commit/421a86ade5ac5ec4d147bdfb913ce32a7004b455) Some polish on query search (#1222) (@vera-liu)
|
||||
- [140a055](https://github.com/airbnb/caravel/commit/140a055e4ef85080bbe3e7defdfeb2dd7b828b58) [docs] add line in installation instructions (@mistercrunch)
|
||||
- [5bf86d9](https://github.com/airbnb/caravel/commit/5bf86d91ec9911e6155335ff1f97dc48c2c16a21) [docs] suggest to upgrade pip and setuptools (@mistercrunch)
|
||||
- [715cdd9](https://github.com/airbnb/caravel/commit/715cdd98fb213e06c687ee8587ded9437039fb2d) Changelog for 0.11.0 (@mistercrunch)
|
||||
|
||||
### 0.11.0 (2016/10/05 04:27 +00:00)
|
||||
- [7a01d9d](https://github.com/airbnb/caravel/commit/7a01d9dbcbbd7678960c33c8cca1b13680290ac7) v0.11.0 (@mistercrunch)
|
||||
- [58dfa43](https://github.com/airbnb/caravel/commit/58dfa436ee53aac185b457e721bfe2303b8d006a) Do not shadow _ function. (#1254) (@bkyryliuk)
|
||||
|
||||
152
CONTRIBUTING.md
@@ -30,8 +30,8 @@ Look through the GitHub issues for features. Anything tagged with
|
||||
|
||||
### Documentation
|
||||
|
||||
Caravel could always use better documentation,
|
||||
whether as part of the official Caravel docs,
|
||||
Superset could always use better documentation,
|
||||
whether as part of the official Superset docs,
|
||||
in docstrings, `docs/*.rst` or even on the web as blog posts or
|
||||
articles.
|
||||
|
||||
@@ -47,18 +47,105 @@ If you are proposing a feature:
|
||||
- Remember that this is a volunteer-driven project, and that
|
||||
contributions are welcome :)
|
||||
|
||||
## Latest Documentation
|
||||
## Documentation
|
||||
|
||||
Latest documentation and tutorial are available [here](http://airbnb.io/caravel)
|
||||
The latest documentation and tutorial are available [here](http://airbnb.io/superset).
|
||||
|
||||
Contributing to the official documentation is relatively easy, once you've setup
|
||||
your environment and done an edit end-to-end. The docs can be found in the
|
||||
`docs/` subdirectory of the repository, and are written in the
|
||||
[reStructuredText format](https://en.wikipedia.org/wiki/ReStructuredText) (.rst).
|
||||
If you've written Markdown before, you'll find the reStructuredText format familiar.
|
||||
|
||||
Superset uses [Sphinx](http://www.sphinx-doc.org/en/1.5.1/) to convert the rst files
|
||||
in `docs/` to the final HTML output users see.
|
||||
|
||||
Before you start changing the docs, you'll want to
|
||||
[fork the Superset project on Github](https://help.github.com/articles/fork-a-repo/).
|
||||
Once that new repository has been created, clone it on your local machine:
|
||||
|
||||
git clone git@github.com:your_username/superset.git
|
||||
|
||||
At this point, you may also want to create a
|
||||
[Python virtual environment](http://docs.python-guide.org/en/latest/dev/virtualenvs/)
|
||||
to manage the Python packages you're about to install:
|
||||
|
||||
virtualenv superset-dev
|
||||
source superset-dev/bin/activate
|
||||
|
||||
Finally, to make changes to the rst files and build the docs using Sphinx,
|
||||
you'll need to install a handful of dependencies from the repo you cloned:
|
||||
|
||||
cd superset
|
||||
pip install -r dev-reqs-for-docs.txt
|
||||
|
||||
To get the feel for how to edit and build the docs, let's edit a file, build
|
||||
the docs and see our changes in action. First, you'll want to
|
||||
[create a new branch](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging)
|
||||
to work on your changes:
|
||||
|
||||
git checkout -b changes-to-docs
|
||||
|
||||
Now, go ahead and edit one of the files under `docs/`, say `docs/tutorial.rst`
|
||||
- change it however you want. Check out the
|
||||
[ReStructuredText Primer](http://docutils.sourceforge.net/docs/user/rst/quickstart.html)
|
||||
for a reference on the formatting of the rst files.
|
||||
|
||||
Once you've made your changes, run this command from the root of the Superset
|
||||
repo to convert the docs into HTML:
|
||||
|
||||
python setup.py build_sphinx
|
||||
|
||||
You'll see a lot of output as Sphinx handles the conversion. After it's done, the
|
||||
HTML Sphinx generated should be in `docs/_build/html`. Go ahead and navigate there
|
||||
and start a simple web server so we can check out the docs in a browser:
|
||||
|
||||
cd docs/_build/html
|
||||
python -m SimpleHTTPServer
|
||||
|
||||
This will start a small Python web server listening on port 8000. Point your
|
||||
browser to [http://localhost:8000/](http://localhost:8000/), find the file
|
||||
you edited earlier, and check out your changes!
|
||||
|
||||
If you've made a change you'd like to contribute to the actual docs, just commit
|
||||
your code, push your new branch to Github:
|
||||
|
||||
git add docs/tutorial.rst
|
||||
git commit -m 'Awesome new change to tutorial'
|
||||
git push origin changes-to-docs
|
||||
|
||||
Then, [open a pull request](https://help.github.com/articles/about-pull-requests/).
|
||||
|
||||
If you're adding new images to the documentation, you'll notice that the images
|
||||
referenced in the rst, e.g.
|
||||
|
||||
.. image:: _static/img/tutorial/tutorial_01_sources_database.png
|
||||
|
||||
aren't actually included in that directory. _Instead_, you'll want to add and commit
|
||||
images (and any other static assets) to the _superset/assets/images_ directory.
|
||||
When the docs are being pushed to [airbnb.io](http://airbnb.io/superset/), images
|
||||
will be moved from there to the _\_static/img_ directory, just like they're referenced
|
||||
in the docs.
|
||||
|
||||
For example, the image referenced above actually lives in
|
||||
|
||||
superset/assets/images/tutorial
|
||||
|
||||
Since the image is moved during the documentation build process, the docs reference the
|
||||
image in
|
||||
|
||||
_static/img/tutorial
|
||||
|
||||
instead.
|
||||
|
||||
## Setting up a Python development environment
|
||||
|
||||
Check the [OS dependencies](http://airbnb.io/caravel/installation.html#os-dependencies) before follows these steps.
|
||||
Check the [OS dependencies](http://airbnb.io/superset/installation.html#os-dependencies) before follows these steps.
|
||||
|
||||
# fork the repo on GitHub and then clone it
|
||||
# alternatively you may want to clone the main repo but that won't work
|
||||
# so well if you are planning on sending PRs
|
||||
# git clone git@github.com:airbnb/caravel.git
|
||||
# git clone git@github.com:airbnb/superset.git
|
||||
|
||||
# [optional] setup a virtual env and activate it
|
||||
virtualenv env
|
||||
@@ -68,30 +155,30 @@ Check the [OS dependencies](http://airbnb.io/caravel/installation.html#os-depend
|
||||
python setup.py develop
|
||||
|
||||
# Create an admin user
|
||||
fabmanager create-admin --app caravel
|
||||
fabmanager create-admin --app superset
|
||||
|
||||
# Initialize the database
|
||||
caravel db upgrade
|
||||
superset db upgrade
|
||||
|
||||
# Create default roles and permissions
|
||||
caravel init
|
||||
superset init
|
||||
|
||||
# Load some data to play with
|
||||
caravel load_examples
|
||||
superset load_examples
|
||||
|
||||
# start a dev web server
|
||||
caravel runserver -d
|
||||
superset runserver -d
|
||||
|
||||
|
||||
## Setting up the node / npm javascript environment
|
||||
|
||||
`caravel/assets` contains all npm-managed, front end assets.
|
||||
`superset/assets` contains all npm-managed, front end assets.
|
||||
Flask-Appbuilder itself comes bundled with jQuery and bootstrap.
|
||||
While these may be phased out over time, these packages are currently not
|
||||
managed with npm.
|
||||
|
||||
### Node/npm versions
|
||||
Make sure you are using recent versions of node and npm. No problems have been found with node>=5.10 and npm>=3.9.
|
||||
Make sure you are using recent versions of node and npm. No problems have been found with node>=5.10 and 4.0. > npm>=3.9.
|
||||
|
||||
### Using npm to generate bundled files
|
||||
|
||||
@@ -112,18 +199,21 @@ export PATH="$HOME/.npm-packages/bin:$PATH"
|
||||
|
||||
#### npm packages
|
||||
To install third party libraries defined in `package.json`, run the
|
||||
following within the `caravel/assets/` directory which will install them in a
|
||||
following within the `superset/assets/` directory which will install them in a
|
||||
new `node_modules/` folder within `assets/`.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
To parse and generate bundled files for caravel, run either of the
|
||||
To parse and generate bundled files for superset, run either of the
|
||||
following commands. The `dev` flag will keep the npm script running and
|
||||
re-run it upon any changes within the assets directory.
|
||||
|
||||
```
|
||||
# Copies a conf file from the frontend to the backend
|
||||
npm run sync-backend
|
||||
|
||||
# Compiles the production / optimized js & css
|
||||
npm run prod
|
||||
|
||||
@@ -135,7 +225,7 @@ For every development session you will have to start a flask dev server
|
||||
as well as an npm watcher
|
||||
|
||||
```
|
||||
caravel runserver -d -p 8081
|
||||
superset runserver -d -p 8081
|
||||
npm run dev
|
||||
```
|
||||
|
||||
@@ -147,7 +237,7 @@ Python tests can be run with:
|
||||
|
||||
We use [Mocha](https://mochajs.org/), [Chai](http://chaijs.com/) and [Enzyme](http://airbnb.io/enzyme/) to test Javascript. Tests can be run with:
|
||||
|
||||
cd /caravel/caravel/assets/javascripts
|
||||
cd /superset/superset/assets/javascripts
|
||||
npm i
|
||||
npm run test
|
||||
|
||||
@@ -157,13 +247,13 @@ Lint the project with:
|
||||
|
||||
# for python changes
|
||||
flake8 changes tests
|
||||
flake8 changes caravel
|
||||
flake8 changes superset
|
||||
|
||||
# for javascript
|
||||
npm run lint
|
||||
|
||||
## Linting with codeclimate
|
||||
Codeclimate is a service we use to measure code quality and test coverage. To get codeclimate's report on your branch, ideally before sending your PR, you can setup codeclimate against your Caravel fork. After you push to your fork, you should be able to get the report at http://codeclimate.com . Alternatively, if you prefer to work locally, you can install the codeclimate cli tool.
|
||||
Codeclimate is a service we use to measure code quality and test coverage. To get codeclimate's report on your branch, ideally before sending your PR, you can setup codeclimate against your Superset fork. After you push to your fork, you should be able to get the report at http://codeclimate.com . Alternatively, if you prefer to work locally, you can install the codeclimate cli tool.
|
||||
|
||||
*Install the codeclimate cli tool*
|
||||
```
|
||||
@@ -193,12 +283,12 @@ Generate the documentation with:
|
||||
cd docs && ./build.sh
|
||||
|
||||
## CSS Themes
|
||||
As part of the npm build process, CSS for Caravel is compiled from `Less`, a dynamic stylesheet language.
|
||||
As part of the npm build process, CSS for Superset is compiled from `Less`, a dynamic stylesheet language.
|
||||
|
||||
It's possible to customize or add your own theme to Caravel, either by overriding CSS rules or preferably
|
||||
It's possible to customize or add your own theme to Superset, either by overriding CSS rules or preferably
|
||||
by modifying the Less variables or files in `assets/stylesheets/less/`.
|
||||
|
||||
The `variables.less` and `bootswatch.less` files that ship with Caravel are derived from
|
||||
The `variables.less` and `bootswatch.less` files that ship with Superset are derived from
|
||||
[Bootswatch](https://bootswatch.com) and thus extend Bootstrap. Modify variables in these files directly, or
|
||||
swap them out entirely with the equivalent files from other Bootswatch (themes)[https://github.com/thomaspark/bootswatch.git]
|
||||
|
||||
@@ -221,14 +311,14 @@ meets these guidelines:
|
||||
|
||||
## Translations
|
||||
|
||||
We use [Babel](http://babel.pocoo.org/en/latest/) to translate Caravel. The
|
||||
We use [Babel](http://babel.pocoo.org/en/latest/) to translate Superset. The
|
||||
key is to instrument the strings that need translation using
|
||||
`from flask_babel import lazy_gettext as _`. Once this is imported in
|
||||
a module, all you have to do is to `_("Wrap your strings")` using the
|
||||
underscore `_` "function".
|
||||
|
||||
To enable changing language in your environment, you can simply add the
|
||||
`LANGUAGES` parameter to your `caravel_config.py`. Having more than one
|
||||
`LANGUAGES` parameter to your `superset_config.py`. Having more than one
|
||||
options here will add a language selection dropdown on the right side of the
|
||||
navigation bar.
|
||||
|
||||
@@ -241,23 +331,23 @@ navigation bar.
|
||||
As per the [Flask AppBuilder documentation] about translation, to create a
|
||||
new language dictionary, run the following command:
|
||||
|
||||
pybabel init -i ./babel/messages.pot -d caravel/translations -l es
|
||||
pybabel init -i ./babel/messages.pot -d superset/translations -l es
|
||||
|
||||
Then it's a matter of running the statement below to gather all stings that
|
||||
need translation
|
||||
|
||||
fabmanager babel-extract --target caravel/translations/
|
||||
fabmanager babel-extract --target superset/translations/
|
||||
|
||||
You can then translate the strings gathered in files located under
|
||||
`caravel/translation`, where there's one per language. For the translations
|
||||
`superset/translation`, where there's one per language. For the translations
|
||||
to take effect, they need to be compiled using this command:
|
||||
|
||||
fabmanager babel-compile --target caravel/translations/
|
||||
fabmanager babel-compile --target superset/translations/
|
||||
|
||||
|
||||
## Adding new datasources
|
||||
|
||||
1. Create Models and Views for the datasource, add them under caravel folder, like a new my_models.py
|
||||
1. Create Models and Views for the datasource, add them under superset folder, like a new my_models.py
|
||||
with models for cluster, datasources, columns and metrics and my_views.py with clustermodelview
|
||||
and datasourcemodelview.
|
||||
|
||||
@@ -267,6 +357,6 @@ to take effect, they need to be compiled using this command:
|
||||
|
||||
For example:
|
||||
|
||||
`ADDITIONAL_MODULE_DS_MAP = {'caravel.my_models': ['MyDatasource', 'MyOtherDatasource']}`
|
||||
`ADDITIONAL_MODULE_DS_MAP = {'superset.my_models': ['MyDatasource', 'MyOtherDatasource']}`
|
||||
|
||||
This means it'll register MyDatasource and MyOtherDatasource in caravel.my_models module in the source registry.
|
||||
This means it'll register MyDatasource and MyOtherDatasource in superset.my_models module in the source registry.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Please use [pull requests](https://github.com/airbnb/caravel/pull/new/master)
|
||||
Please use [pull requests](https://github.com/airbnb/superset/pull/new/master)
|
||||
to add your organization and/or project to this document!
|
||||
|
||||
Organizations
|
||||
@@ -8,6 +8,12 @@ Organizations
|
||||
- [Maieutical Labs] (https://cloudschooling.it)
|
||||
- [Shopkick] (https://www.shopkick.com)
|
||||
- [Amino] (https://amino.com)
|
||||
- [Faasos] (http://faasos.com/)
|
||||
- [Clark.de] (http://clark.de/)
|
||||
- [Yahoo!] (www.yahoo.com)
|
||||
- [Digit Game Studios] (https://www.digitgaming.com/)
|
||||
- [Brilliant.org] (https://brilliant.org/)
|
||||
- [Qunar] (https://www.qunar.com/)
|
||||
|
||||
Projects
|
||||
----------
|
||||
|
||||
19
ISSUE_TEMPLATE.md
Normal file
@@ -0,0 +1,19 @@
|
||||
Make sure these boxes are checked before submitting your issue - thank you!
|
||||
|
||||
- [ ] I have checked the superset logs for python stacktraces and included it here as text if any
|
||||
- [ ] I have reproduced the issue with at least the latest released version of superset
|
||||
- [ ] I have checked the issue tracker for the same issue and I haven't found one similar
|
||||
|
||||
|
||||
### Superset version
|
||||
|
||||
|
||||
### Expected results
|
||||
|
||||
|
||||
### Actual results
|
||||
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
|
||||
16
MANIFEST.in
@@ -1,9 +1,9 @@
|
||||
recursive-include caravel/templates *
|
||||
recursive-include caravel/static *
|
||||
recursive-exclude caravel/static/assets/node_modules *
|
||||
recursive-include caravel/static/assets/node_modules/font-awesome *
|
||||
recursive-exclude caravel/static/docs *
|
||||
recursive-exclude caravel/static/spec *
|
||||
recursive-include superset/templates *
|
||||
recursive-include superset/static *
|
||||
recursive-exclude superset/static/assets/node_modules *
|
||||
recursive-include superset/static/assets/node_modules/font-awesome *
|
||||
recursive-exclude superset/static/docs *
|
||||
recursive-exclude superset/static/spec *
|
||||
recursive-exclude tests *
|
||||
recursive-include caravel/data *
|
||||
recursive-include caravel/migrations *
|
||||
recursive-include superset/data *
|
||||
recursive-include superset/migrations *
|
||||
|
||||
105
README.md
@@ -1,47 +1,57 @@
|
||||
Caravel
|
||||
Superset
|
||||
=========
|
||||
<img src="http://i.imgur.com/H0Kyvyi.jpg" style="border-radius: 20px; box-shadow:5px 5px 5px gray;" alt="Caravel" width="500"/>
|
||||
|
||||
[](https://travis-ci.org/airbnb/caravel)
|
||||
[](https://badge.fury.io/py/caravel)
|
||||
[](https://coveralls.io/github/airbnb/caravel?branch=master)
|
||||
[](https://codeclimate.com/github/airbnb/caravel/coverage)
|
||||
[](https://landscape.io/github/airbnb/caravel/master)
|
||||
[](https://codeclimate.com/github/airbnb/caravel)
|
||||
[](https://pypi.python.org/pypi/caravel)
|
||||
[](https://requires.io/github/airbnb/caravel/requirements/?branch=master)
|
||||
[](https://gitter.im/airbnb/caravel?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](http://airbnb.io/caravel/)
|
||||
[](https://david-dm.org/airbnb/caravel?path=caravel/assets)
|
||||
[](https://travis-ci.org/airbnb/superset)
|
||||
[](https://badge.fury.io/py/superset)
|
||||
[](https://coveralls.io/github/airbnb/superset?branch=master)
|
||||
[](https://codeclimate.com/github/airbnb/superset/coverage)
|
||||
[](https://landscape.io/github/airbnb/superset/master)
|
||||
[](https://codeclimate.com/github/airbnb/superset)
|
||||
[](https://pypi.python.org/pypi/superset)
|
||||
[](https://requires.io/github/airbnb/superset/requirements/?branch=master)
|
||||
[](https://gitter.im/airbnb/superset?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](http://airbnb.io/superset/)
|
||||
[](https://david-dm.org/airbnb/superset?path=superset/assets)
|
||||
|
||||
Caravel is a data exploration platform designed to be visual, intuitive
|
||||
<img
|
||||
src="https://cloud.githubusercontent.com/assets/130878/20946612/49a8a25c-bbc0-11e6-8314-10bef902af51.png"
|
||||
alt="Superset"
|
||||
width="500"
|
||||
/>
|
||||
|
||||
**Superset** is a data exploration platform designed to be visual, intuitive
|
||||
and interactive.
|
||||
|
||||
[this project used to be named **Panoramix**]
|
||||
[this project used to be named **Caravel**, and **Panoramix** in the past]
|
||||
|
||||
|
||||
Screenshots & Gifs
|
||||
------------------
|
||||

|
||||
|
||||
---
|
||||

|
||||
**View Dashboards**
|
||||

|
||||
|
||||
---
|
||||

|
||||
<br/>
|
||||
**View/Edit a Slice**
|
||||

|
||||
|
||||
---
|
||||

|
||||
<br/>
|
||||
**Query and Visualize with SQL Lab**
|
||||

|
||||
|
||||
---
|
||||

|
||||
<br/>
|
||||

|
||||
|
||||
Caravel
|
||||

|
||||
|
||||

|
||||
|
||||
Superset
|
||||
---------
|
||||
Caravel's main goal is to make it easy to slice, dice and visualize data.
|
||||
Superset's main goal is to make it easy to slice, dice and visualize data.
|
||||
It empowers users to perform **analytics at the speed of thought**.
|
||||
|
||||
Caravel provides:
|
||||
Superset provides:
|
||||
* A quick way to intuitively visualize datasets by allowing users to create
|
||||
and share interactive dashboards
|
||||
* A rich set of visualizations to analyze your data, as well as a flexible
|
||||
@@ -54,7 +64,7 @@ Caravel provides:
|
||||
displayed in the UI, by defining which fields should show up in
|
||||
which dropdown and which aggregation and function (metrics) are
|
||||
made available to the user
|
||||
* Deep integration with Druid allows for Caravel to stay blazing fast while
|
||||
* Deep integration with Druid allows for Superset to stay blazing fast while
|
||||
slicing and dicing large, realtime datasets
|
||||
* Fast loading dashboards with configurable caching
|
||||
|
||||
@@ -62,7 +72,7 @@ Caravel provides:
|
||||
Database Support
|
||||
----------------
|
||||
|
||||
Caravel was originally designed on top of Druid.io, but quickly broadened
|
||||
Superset was originally designed on top of Druid.io, but quickly broadened
|
||||
its scope to support other databases through the use of SQLAlchemy, a Python
|
||||
ORM that is compatible with
|
||||
[most common databases](http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html).
|
||||
@@ -83,55 +93,50 @@ power analytic dashboards and applications.*
|
||||
Installation & Configuration
|
||||
----------------------------
|
||||
|
||||
[See in the documentation](http://airbnb.io/caravel/installation.html)
|
||||
[See in the documentation](http://airbnb.io/superset/installation.html)
|
||||
|
||||
|
||||
More screenshots
|
||||
----------------
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||

|
||||

|
||||
|
||||
---
|
||||

|
||||

|
||||
|
||||
---
|
||||

|
||||

|
||||
|
||||
---
|
||||

|
||||

|
||||
|
||||
---
|
||||

|
||||

|
||||
|
||||
---
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
Resources
|
||||
-------------
|
||||
* [Caravel Google Group](https://groups.google.com/forum/#!forum/airbnb_caravel)
|
||||
* [Gitter (live chat) Channel](https://gitter.im/airbnb/caravel)
|
||||
* [Docker image 1](https://hub.docker.com/r/kochalex/caravel/)
|
||||
[Docker image 2](https://hub.docker.com/r/amancevice/caravel/) (community contributed)
|
||||
* [Superset Google Group](https://groups.google.com/forum/#!forum/airbnb_superset)
|
||||
* [Gitter (live chat) Channel](https://gitter.im/airbnb/superset)
|
||||
* [Docker image](https://hub.docker.com/r/amancevice/superset/) (community contributed)
|
||||
* [Slides from Strata (March 2016)](https://drive.google.com/open?id=0B5PVE0gzO81oOVJkdF9aNkJMSmM)
|
||||
|
||||
|
||||
Tip of the Hat
|
||||
--------------
|
||||
|
||||
Caravel would not be possible without these great frameworks / libs
|
||||
Superset would not be possible without these great frameworks / libs
|
||||
|
||||
* Flask App Builder - Allowing us to focus on building the app quickly while
|
||||
getting the foundation for free
|
||||
* The Flask ecosystem - Simply amazing. So much Plug, easy play.
|
||||
* NVD3 - One of the best charting libraries out there
|
||||
* Much more, check out the `install_requires` section in the [setup.py](https://github.com/airbnb/caravel/blob/master/setup.py) file!
|
||||
* Much more, check out the `install_requires` section in the [setup.py](https://github.com/airbnb/superset/blob/master/setup.py) file!
|
||||
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
Interested in contributing? Casual hacking? Check out [Contributing.MD](https://github.com/airbnb/caravel/blob/master/CONTRIBUTING.md)
|
||||
Interested in contributing? Casual hacking? Check out [Contributing.MD](https://github.com/airbnb/superset/blob/master/CONTRIBUTING.md)
|
||||
|
||||
4
TODO.md
@@ -1,5 +1,5 @@
|
||||
# TODO
|
||||
List of TODO items for Caravel
|
||||
List of TODO items for Superset
|
||||
|
||||
## Important
|
||||
* **Getting proper JS testing:** unit tests on the Python side are pretty
|
||||
@@ -7,7 +7,7 @@ List of TODO items for Caravel
|
||||
testing all the ajax-type calls
|
||||
* **Viz Plugins:** Allow people to define and share visualization plugins.
|
||||
ideally one would only need to drop in a set of files in a folder and
|
||||
Caravel would discover and expose the plugins
|
||||
Superset would discover and expose the plugins
|
||||
|
||||
## Features
|
||||
* **Dashboard URL filters:** `{dash_url}#fltin__fieldname__value1,value2`
|
||||
|
||||
@@ -29,7 +29,7 @@ script_location = migrations
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = scheme://localhost/caravel
|
||||
sqlalchemy.url = scheme://localhost/superset
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[ignore: caravel/assets/node_modules/**]
|
||||
[python: caravel/**.py]
|
||||
[jinja2: caravel/**/templates/**.html]
|
||||
[ignore: superset/assets/node_modules/**]
|
||||
[python: superset/**.py]
|
||||
[jinja2: superset/**/templates/**.html]
|
||||
encoding = utf-8
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
error = (
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMM8OI++=~~~~~~=+?IODMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMD$~~~~~~~~~~~~~~~~~~~~~~~=$MMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMMMN8?:~~~~~~~~~~~~~~~~~~~~~~~~~~=+8NMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMO=~~~~~~~~~~~~~~~~~+I??~~~~~~~~~~~~~+DMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMNI~~~~~~~~~~~~~~~~~~IIIII=~~~~~~~~~~~~~~=NMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMM+=~~~~~~~~~~~~~~~~~~~=III+~~~~~~~~~~~~~~~~~?8MMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMM?~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+++=~~~~8MMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMI=~~~~~~~~~~~~~~~~~~~~~~~~~III?I~~~~~~~~,:++++++~~8MMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMN7~~~~~~~~~~~~~~~~==+=~~~~~~=IIIII~~~~~~:. ..:=++=~=MMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMO=~~~~~~~~~~~~~~~~+++=~~~~~~~~??I?I~~~~~~. ...,~~~~IMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMM~~~~~~~~~~~~~~~~~+++:,~~~~~~~~~~~?=~~~~~:. ..~~~~~OMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMM$=~~~~~~~~~~~~~~~=++:.. ..~~~~~~~~~~~~~~~~,. . . :~~~~~OMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMM~~~~~~~~~~~~~~~~+++,. .~~~~~~~~~~~~~~~.. .. . .~~~~~=OMMMMMMMMMM\n"+
|
||||
"MMMMMMMM?~~~~~~~~~~~~~~~=+~. .~~~~~~~~~~~~~~. ,MMMMM,=~~~~~~NMMMMMMMMM\n"+
|
||||
"MMMMMMMN~~~~~~~~~~~~~~~~~,. .,~~~~~~~~~~~~~.. ZMMM,+Z:~~~~~~$MMMMMMMMM\n"+
|
||||
"MMMMMM8?~~~~~~~~~~~~~~~~~.. ..~~~~~~~~~~~~~:. DMMM,+D~~~~~~~~IMMMMMMMM\n"+
|
||||
"MMMMMMI~~~~~~~~~~~~~~~~~~.. :MMMO~~~~~~~~~~~~~~~,.. ?MMMMMI~~~~~~~~~MMMMMMMM\n"+
|
||||
"MMMMMM=~~~~~~~~~~~~~~~~~~.. MMM+=M:~~~~~~~~~~~~~:. .:IM$~~~~~~~~~~~8MMMMMMM\n"+
|
||||
"MMMMMD~~~~~~~~~~~~~~~~~~~:. MMM:,M:~~~~~~~~~~~~~~~.......:~~~~~~~~~~$MMMMMMM\n"+
|
||||
"MMMMMI~~~~~~~~~~~~~~~~~~~~, MMMMMM~~~~~~~~~~~~~~~~~~,..:~~~~~~~~~~~~+MMMMMMM\n"+
|
||||
"MMMMD+~~~~~~~~~~~~~~~~~~~~~. $MMMM$~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=MMMMMMM\n"+
|
||||
"MMMM8~~~~~~~~~~~~~~~~~~~~~~:. . .:~~~~~~,..:. .=~~~~~~~~~~~~~~~~~~~~MMMMMMM\n"+
|
||||
"MMMMO~~~~~~~~~~~~~~~~~~~~~~~:, .:~~~~~=8.. .+ . =8ZI~~~~~~~~~~~~~~~~=MMMMMMM\n"+
|
||||
"MMMMZ=~~~~~~~~~~~~~~~~~~~~~~~~:,,,:~~~~~~IZ8:. .O....888?~~~~~~~~~~~~~~~+MMMMMMM\n"+
|
||||
"MMMMO=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~?888=...I~I88888O?~~~~~~~~~~~~~~7MMMMMMM\n"+
|
||||
"MMMMO~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Z888OO88888888888O?~~~~~~~~~~~~~OMMMMMMM\n"+
|
||||
"MMMMD+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=8888888888888888888~~~~~~~~~~~~+MMMMMMMM\n"+
|
||||
"MMMMM7~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~?8888888888888888888?~~~~~~~~~~=$MMMMMMMM\n"+
|
||||
"MMMMMD~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=$8888888888888888888O~~~~~~~~~~8MMMMMMMMM\n"+
|
||||
"MMMMMN=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+Z88888888888888888ZZ7=~~~~~~~~?MMMMMMMMMM\n"+
|
||||
"MMMMMMZ=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+Z88888888Z7I===~~~~~~~~~~~~~=OMMMMMMMMMMM\n"+
|
||||
"MMMMMMN$~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=$88888O7?=~~~~~~~~~~~~~~~~~~OMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMM?~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~I8OZ+~~~~~~~~~~~~~~~~~~~~=DMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMM8=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+$+=~~~~~~~~~~~~~~~~~~~~+MMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMD7~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=$DMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMM?~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=$OMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMD7=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ZMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMZ7=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~78MMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMM8OI=~~~~~~~~~~~~~~~~~~~=+?ZDNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMNDZ7?++~=~==~+?IONMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM")
|
||||
|
||||
stacktrace="""
|
||||
-------------------------------------------------------------------------------------------------------
|
||||
=======================================================================================================
|
||||
-------------------------------------------------------------------------------------------------------
|
||||
___ ___ ___
|
||||
( ) ( ) ( )
|
||||
.--. | |_ .---. .--. | | ___ | |_ ___ .-. .---. .--. .--.
|
||||
/ _ \ ( __) / .-, \ / \ | | ( ) ( __) ( ) \ / .-, \ / \ / \\
|
||||
. .' `. ; | | (__) ; | | .-. ; | | ' / | | | ' .-. ; (__) ; | | .-. ; | .-. ;
|
||||
| ' | | | | ___ .'` | | |(___) | |,' / | | ___ | / (___) .'` | | |(___) | | | |
|
||||
_\_`.(___) | |( ) / .'| | | | | . '. | |( ) | | / .'| | | | | |/ |
|
||||
( ). '. | | | | | / | | | | ___ | | `. \ | | | | | | | / | | | | ___ | ' _.'
|
||||
| | `\ | | ' | | ; | ; | | '( ) | | \ \ | ' | | | | ; | ; | | '( ) | .'.-.
|
||||
; '._,' ' ' `-' ; ' `-' | ' `-' | | | \ . ' `-' ; | | ' `-' | ' `-' | ' `-' /
|
||||
'.___.' `.__. `.__.'_. `.__,' (___ ) (___) `.__. (___) `.__.'_. `.__,' `.__.'
|
||||
|
||||
-------------------------------------------------------------------------------------------------------
|
||||
=======================================================================================================
|
||||
-------------------------------------------------------------------------------------------------------
|
||||
"""
|
||||
|
||||
boat = """\
|
||||
+ +
|
||||
)`.).
|
||||
)``)``) .~~
|
||||
).-'.-')|)
|
||||
|-).-).-'_'-/
|
||||
~~~\ `o-o-o' /~~~~
|
||||
~~~'---.____/~~~"""
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"presets" : ["airbnb", "es2015", "react"]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 106 KiB |
@@ -1,130 +0,0 @@
|
||||
export const RESET_STATE = 'RESET_STATE';
|
||||
export const ADD_QUERY_EDITOR = 'ADD_QUERY_EDITOR';
|
||||
export const CLONE_QUERY_TO_NEW_TAB = 'CLONE_QUERY_TO_NEW_TAB';
|
||||
export const REMOVE_QUERY_EDITOR = 'REMOVE_QUERY_EDITOR';
|
||||
export const MERGE_TABLE = 'MERGE_TABLE';
|
||||
export const REMOVE_TABLE = 'REMOVE_TABLE';
|
||||
export const START_QUERY = 'START_QUERY';
|
||||
export const STOP_QUERY = 'STOP_QUERY';
|
||||
export const END_QUERY = 'END_QUERY';
|
||||
export const REMOVE_QUERY = 'REMOVE_QUERY';
|
||||
export const EXPAND_TABLE = 'EXPAND_TABLE';
|
||||
export const COLLAPSE_TABLE = 'COLLAPSE_TABLE';
|
||||
export const QUERY_SUCCESS = 'QUERY_SUCCESS';
|
||||
export const QUERY_FAILED = 'QUERY_FAILED';
|
||||
export const QUERY_EDITOR_SETDB = 'QUERY_EDITOR_SETDB';
|
||||
export const QUERY_EDITOR_SET_SCHEMA = 'QUERY_EDITOR_SET_SCHEMA';
|
||||
export const QUERY_EDITOR_SET_TITLE = 'QUERY_EDITOR_SET_TITLE';
|
||||
export const QUERY_EDITOR_SET_AUTORUN = 'QUERY_EDITOR_SET_AUTORUN';
|
||||
export const QUERY_EDITOR_SET_SQL = 'QUERY_EDITOR_SET_SQL';
|
||||
export const SET_DATABASES = 'SET_DATABASES';
|
||||
export const ADD_WORKSPACE_QUERY = 'ADD_WORKSPACE_QUERY';
|
||||
export const REMOVE_WORKSPACE_QUERY = 'REMOVE_WORKSPACE_QUERY';
|
||||
export const SET_ACTIVE_QUERY_EDITOR = 'SET_ACTIVE_QUERY_EDITOR';
|
||||
export const ADD_ALERT = 'ADD_ALERT';
|
||||
export const REMOVE_ALERT = 'REMOVE_ALERT';
|
||||
export const REFRESH_QUERIES = 'REFRESH_QUERIES';
|
||||
export const SET_NETWORK_STATUS = 'SET_NETWORK_STATUS';
|
||||
|
||||
export function resetState() {
|
||||
return { type: RESET_STATE };
|
||||
}
|
||||
|
||||
export function setDatabases(databases) {
|
||||
return { type: SET_DATABASES, databases };
|
||||
}
|
||||
|
||||
export function addQueryEditor(queryEditor) {
|
||||
return { type: ADD_QUERY_EDITOR, queryEditor };
|
||||
}
|
||||
|
||||
export function cloneQueryToNewTab(query) {
|
||||
return { type: CLONE_QUERY_TO_NEW_TAB, query };
|
||||
}
|
||||
|
||||
export function setNetworkStatus(networkOn) {
|
||||
return { type: SET_NETWORK_STATUS, networkOn };
|
||||
}
|
||||
|
||||
export function addAlert(alert) {
|
||||
return { type: ADD_ALERT, alert };
|
||||
}
|
||||
|
||||
export function removeAlert(alert) {
|
||||
return { type: REMOVE_ALERT, alert };
|
||||
}
|
||||
|
||||
export function setActiveQueryEditor(queryEditor) {
|
||||
return { type: SET_ACTIVE_QUERY_EDITOR, queryEditor };
|
||||
}
|
||||
|
||||
export function removeQueryEditor(queryEditor) {
|
||||
return { type: REMOVE_QUERY_EDITOR, queryEditor };
|
||||
}
|
||||
|
||||
export function removeQuery(query) {
|
||||
return { type: REMOVE_QUERY, query };
|
||||
}
|
||||
|
||||
export function queryEditorSetDb(queryEditor, dbId) {
|
||||
return { type: QUERY_EDITOR_SETDB, queryEditor, dbId };
|
||||
}
|
||||
|
||||
export function queryEditorSetSchema(queryEditor, schema) {
|
||||
return { type: QUERY_EDITOR_SET_SCHEMA, queryEditor, schema };
|
||||
}
|
||||
|
||||
export function queryEditorSetAutorun(queryEditor, autorun) {
|
||||
return { type: QUERY_EDITOR_SET_AUTORUN, queryEditor, autorun };
|
||||
}
|
||||
|
||||
export function queryEditorSetTitle(queryEditor, title) {
|
||||
return { type: QUERY_EDITOR_SET_TITLE, queryEditor, title };
|
||||
}
|
||||
|
||||
export function queryEditorSetSql(queryEditor, sql) {
|
||||
return { type: QUERY_EDITOR_SET_SQL, queryEditor, sql };
|
||||
}
|
||||
|
||||
export function mergeTable(table) {
|
||||
return { type: MERGE_TABLE, table };
|
||||
}
|
||||
|
||||
export function expandTable(table) {
|
||||
return { type: EXPAND_TABLE, table };
|
||||
}
|
||||
|
||||
export function collapseTable(table) {
|
||||
return { type: COLLAPSE_TABLE, table };
|
||||
}
|
||||
|
||||
export function removeTable(table) {
|
||||
return { type: REMOVE_TABLE, table };
|
||||
}
|
||||
|
||||
export function startQuery(query) {
|
||||
return { type: START_QUERY, query };
|
||||
}
|
||||
|
||||
export function stopQuery(query) {
|
||||
return { type: STOP_QUERY, query };
|
||||
}
|
||||
|
||||
export function querySuccess(query, results) {
|
||||
return { type: QUERY_SUCCESS, query, results };
|
||||
}
|
||||
|
||||
export function queryFailed(query, msg) {
|
||||
return { type: QUERY_FAILED, query, msg };
|
||||
}
|
||||
|
||||
export function addWorkspaceQuery(query) {
|
||||
return { type: ADD_WORKSPACE_QUERY, query };
|
||||
}
|
||||
|
||||
export function removeWorkspaceQuery(query) {
|
||||
return { type: REMOVE_WORKSPACE_QUERY, query };
|
||||
}
|
||||
export function refreshQueries(alteredQueries) {
|
||||
return { type: REFRESH_QUERIES, alteredQueries };
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export const STATE_BSSTYLE_MAP = {
|
||||
failed: 'danger',
|
||||
pending: 'info',
|
||||
running: 'warning',
|
||||
success: 'success',
|
||||
};
|
||||
|
||||
export const STATUS_OPTIONS = ['success', 'failed', 'running'];
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
|
||||
const ButtonWithTooltip = (props) => {
|
||||
let tooltip = (
|
||||
<Tooltip id="tooltip">
|
||||
{props.tooltip}
|
||||
</Tooltip>
|
||||
);
|
||||
return (
|
||||
<OverlayTrigger
|
||||
overlay={tooltip}
|
||||
delayShow={300}
|
||||
placement={props.placement}
|
||||
delayHide={150}
|
||||
>
|
||||
<Button
|
||||
onClick={props.onClick}
|
||||
bsStyle={props.bsStyle}
|
||||
bsSize={props.bsSize}
|
||||
disabled={props.disabled}
|
||||
className={props.className}
|
||||
>
|
||||
{props.children}
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonWithTooltip.defaultProps = {
|
||||
onClick: () => {},
|
||||
disabled: false,
|
||||
placement: 'top',
|
||||
bsStyle: 'default',
|
||||
};
|
||||
|
||||
ButtonWithTooltip.propTypes = {
|
||||
bsSize: React.PropTypes.string,
|
||||
bsStyle: React.PropTypes.string,
|
||||
children: React.PropTypes.element,
|
||||
className: React.PropTypes.string,
|
||||
disabled: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func,
|
||||
placement: React.PropTypes.string,
|
||||
tooltip: React.PropTypes.string,
|
||||
};
|
||||
|
||||
export default ButtonWithTooltip;
|
||||
@@ -1,55 +0,0 @@
|
||||
import React from 'react';
|
||||
import CopyToClipboard from '../../components/CopyToClipboard';
|
||||
|
||||
const propTypes = {
|
||||
qe: React.PropTypes.object,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
qe: null,
|
||||
};
|
||||
|
||||
export default class CopyQueryTabUrl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const uri = window.location.toString();
|
||||
const search = window.location.search;
|
||||
const cleanUri = search ? uri.substring(0, uri.indexOf('?')) : uri;
|
||||
const query = search.substring(1);
|
||||
this.state = {
|
||||
uri,
|
||||
cleanUri,
|
||||
query,
|
||||
};
|
||||
}
|
||||
|
||||
getQueryLink() {
|
||||
const params = [];
|
||||
const qe = this.props.qe;
|
||||
if (qe.dbId) params.push('dbid=' + qe.dbId);
|
||||
if (qe.title) params.push('title=' + encodeURIComponent(qe.title));
|
||||
if (qe.schema) params.push('schema=' + encodeURIComponent(qe.schema));
|
||||
if (qe.autorun) params.push('autorun=' + qe.autorun);
|
||||
if (qe.sql) params.push('sql=' + encodeURIComponent(qe.sql));
|
||||
|
||||
const queryString = params.join('&');
|
||||
const queryLink = this.state.cleanUri + '?' + queryString;
|
||||
|
||||
return queryLink;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CopyToClipboard
|
||||
inMenu
|
||||
text={this.getQueryLink()}
|
||||
copyNode={<span>share query</span>}
|
||||
tooltipText="copy URL to clipboard"
|
||||
shouldShowText={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CopyQueryTabUrl.propTypes = propTypes;
|
||||
CopyQueryTabUrl.defaultProps = defaultProps;
|
||||
@@ -1,65 +0,0 @@
|
||||
const $ = window.$ = require('jquery');
|
||||
import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import Select from 'react-select';
|
||||
import { connect } from 'react-redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
class DatabaseSelect extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
databaseLoading: false,
|
||||
databaseOptions: [],
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
this.fetchDatabaseOptions();
|
||||
}
|
||||
changeDb(db) {
|
||||
this.props.onChange(db);
|
||||
}
|
||||
fetchDatabaseOptions() {
|
||||
this.setState({ databaseLoading: true });
|
||||
const url = '/databaseasync/api/read?_flt_0_expose_in_sqllab=1';
|
||||
$.get(url, (data) => {
|
||||
const options = data.result.map((db) => ({ value: db.id, label: db.database_name }));
|
||||
this.setState({ databaseOptions: options, databaseLoading: false });
|
||||
this.props.actions.setDatabases(data.result);
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
name="select-db"
|
||||
placeholder={`Select a database (${this.state.databaseOptions.length})`}
|
||||
options={this.state.databaseOptions}
|
||||
value={this.props.databaseId}
|
||||
isLoading={this.state.databaseLoading}
|
||||
autosize={false}
|
||||
onChange={this.changeDb.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DatabaseSelect.propTypes = {
|
||||
onChange: React.PropTypes.func,
|
||||
actions: React.PropTypes.object,
|
||||
databaseId: React.PropTypes.number,
|
||||
};
|
||||
|
||||
DatabaseSelect.defaultProps = {
|
||||
onChange: () => {},
|
||||
databaseId: null,
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(DatabaseSelect);
|
||||
@@ -1,58 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
import QueryTable from './QueryTable';
|
||||
import { Alert } from 'react-bootstrap';
|
||||
|
||||
const QueryHistory = (props) => {
|
||||
const activeQeId = props.tabHistory[props.tabHistory.length - 1];
|
||||
const queriesArray = [];
|
||||
for (const id in props.queries) {
|
||||
if (props.queries[id].sqlEditorId === activeQeId) {
|
||||
queriesArray.push(props.queries[id]);
|
||||
}
|
||||
}
|
||||
if (queriesArray.length > 0) {
|
||||
return (
|
||||
<QueryTable
|
||||
columns={[
|
||||
'state', 'started', 'duration', 'progress',
|
||||
'rows', 'sql', 'output', 'actions',
|
||||
]}
|
||||
queries={queriesArray}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Alert bsStyle="info">
|
||||
No query history yet...
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
QueryHistory.defaultProps = {
|
||||
queries: {},
|
||||
};
|
||||
|
||||
QueryHistory.propTypes = {
|
||||
queries: React.PropTypes.object,
|
||||
tabHistory: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
queries: state.queries,
|
||||
tabHistory: state.tabHistory,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QueryHistory);
|
||||
@@ -1,62 +0,0 @@
|
||||
import React from 'react';
|
||||
import Link from './Link';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import shortid from 'shortid';
|
||||
|
||||
class QueryLink extends React.Component {
|
||||
popTab() {
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: this.props.query.title,
|
||||
dbId: this.props.query.dbId,
|
||||
autorun: false,
|
||||
sql: this.props.query.sql,
|
||||
};
|
||||
this.props.actions.addQueryEditor(qe);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="clearfix">
|
||||
<div className="pull-left">
|
||||
<a
|
||||
href="#"
|
||||
tooltip="Pop this query in a new tab"
|
||||
onClick={this.popTab.bind(this)}
|
||||
>
|
||||
{this.props.query.title}
|
||||
</a>
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
<Link
|
||||
onClick={this.props.actions.removeWorkspaceQuery.bind(this, this.props.query)}
|
||||
tooltip="Remove query from workspace"
|
||||
href="#"
|
||||
>
|
||||
×
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueryLink.propTypes = {
|
||||
query: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
QueryLink.defaultProps = {
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(QueryLink);
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
const $ = window.$ = require('jquery');
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from 'react-bootstrap';
|
||||
import Select from 'react-select';
|
||||
import QueryTable from './QueryTable';
|
||||
import DatabaseSelect from './DatabaseSelect';
|
||||
import { STATUS_OPTIONS } from '../common';
|
||||
|
||||
class QuerySearch extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
userLoading: false,
|
||||
userOptions: [],
|
||||
databaseId: null,
|
||||
userId: null,
|
||||
searchText: null,
|
||||
status: 'success',
|
||||
queriesArray: [],
|
||||
};
|
||||
}
|
||||
componentWillMount() {
|
||||
this.fetchUsers();
|
||||
this.refreshQueries();
|
||||
}
|
||||
onUserClicked(userId) {
|
||||
this.setState({ userId }, () => { this.refreshQueries(); });
|
||||
}
|
||||
onDbClicked(dbId) {
|
||||
this.setState({ databaseId: dbId }, () => { this.refreshQueries(); });
|
||||
}
|
||||
onChange(db) {
|
||||
const val = (db) ? db.value : null;
|
||||
this.setState({ databaseId: val });
|
||||
}
|
||||
insertParams(baseUrl, params) {
|
||||
return baseUrl + '?' + params.join('&');
|
||||
}
|
||||
changeUser(user) {
|
||||
const val = (user) ? user.value : null;
|
||||
this.setState({ userId: val });
|
||||
}
|
||||
changeStatus(status) {
|
||||
const val = (status) ? status.value : null;
|
||||
this.setState({ status: val });
|
||||
}
|
||||
changeSearch(event) {
|
||||
this.setState({ searchText: event.target.value });
|
||||
}
|
||||
fetchUsers() {
|
||||
this.setState({ userLoading: true });
|
||||
const url = '/users/api/read';
|
||||
$.getJSON(url, (data, status) => {
|
||||
if (status === 'success') {
|
||||
const options = [];
|
||||
for (let i = 0; i < data.pks.length; i++) {
|
||||
options.push({ value: data.pks[i], label: data.result[i].username });
|
||||
}
|
||||
this.setState({ userOptions: options, userLoading: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
refreshQueries() {
|
||||
const params = [
|
||||
`userId=${this.state.userId}`,
|
||||
`databaseId=${this.state.databaseId}`,
|
||||
`searchText=${this.state.searchText}`,
|
||||
`status=${this.state.status}`,
|
||||
];
|
||||
|
||||
const url = this.insertParams('/caravel/search_queries', params);
|
||||
$.getJSON(url, (data, status) => {
|
||||
if (status === 'success') {
|
||||
const newQueriesArray = [];
|
||||
for (const id in data) {
|
||||
newQueriesArray.push(data[id]);
|
||||
}
|
||||
this.setState({ queriesArray: newQueriesArray });
|
||||
}
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="row space-1">
|
||||
<div className="col-sm-2">
|
||||
<Select
|
||||
name="select-user"
|
||||
placeholder="[User]"
|
||||
options={this.state.userOptions}
|
||||
value={this.state.userId}
|
||||
isLoading={this.state.userLoading}
|
||||
autosize={false}
|
||||
onChange={this.changeUser.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-2">
|
||||
<DatabaseSelect
|
||||
onChange={this.onChange.bind(this)}
|
||||
databaseId={this.state.databaseId}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.changeSearch.bind(this)}
|
||||
className="form-control input-sm"
|
||||
placeholder="Search Results"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-2">
|
||||
<Select
|
||||
name="select-state"
|
||||
placeholder="[Query Status]"
|
||||
options={STATUS_OPTIONS.map((s) => ({ value: s, label: s }))}
|
||||
value={this.state.status}
|
||||
isLoading={false}
|
||||
autosize={false}
|
||||
onChange={this.changeStatus.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<Button bsSize="small" bsStyle="success" onClick={this.refreshQueries.bind(this)}>
|
||||
Search
|
||||
</Button>
|
||||
</div>
|
||||
<QueryTable
|
||||
columns={[
|
||||
'state', 'db', 'user',
|
||||
'progress', 'rows', 'sql', 'querylink',
|
||||
]}
|
||||
onUserClicked={this.onUserClicked.bind(this)}
|
||||
onDbClicked={this.onDbClicked.bind(this)}
|
||||
queries={this.state.queriesArray}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default QuerySearch;
|
||||
@@ -1,95 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Alert, Button, ButtonGroup } from 'react-bootstrap';
|
||||
import { Table } from 'reactable';
|
||||
|
||||
import VisualizeModal from './VisualizeModal';
|
||||
|
||||
|
||||
class ResultSet extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
searchText: '',
|
||||
showModal: false,
|
||||
};
|
||||
}
|
||||
changeSearch(event) {
|
||||
this.setState({ searchText: event.target.value });
|
||||
}
|
||||
showModal() {
|
||||
this.setState({ showModal: true });
|
||||
}
|
||||
hideModal() {
|
||||
this.setState({ showModal: false });
|
||||
}
|
||||
render() {
|
||||
const results = this.props.query.results;
|
||||
let controls = <div className="noControls" />;
|
||||
if (this.props.showControls) {
|
||||
controls = (
|
||||
<div className="ResultSetControls">
|
||||
<div className="clearfix">
|
||||
<div className="pull-left">
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
bsSize="small"
|
||||
onClick={this.showModal.bind(this)}
|
||||
>
|
||||
<i className="fa fa-line-chart m-l-1" /> Visualize
|
||||
</Button>
|
||||
<Button bsSize="small" href={'/caravel/csv/' + this.props.query.id}>
|
||||
<i className="fa fa-file-text-o" /> .CSV
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.changeSearch.bind(this)}
|
||||
className="form-control input-sm"
|
||||
placeholder="Search Results"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (results && results.data && results.data.length > 0) {
|
||||
return (
|
||||
<div>
|
||||
<VisualizeModal
|
||||
show={this.state.showModal}
|
||||
query={this.props.query}
|
||||
onHide={this.hideModal.bind(this)}
|
||||
/>
|
||||
{controls}
|
||||
<div className="ResultSet">
|
||||
<Table
|
||||
data={results.data}
|
||||
columns={results.columns.map((col) => col.name)}
|
||||
sortable
|
||||
className="table table-condensed table-bordered"
|
||||
filterBy={this.state.searchText}
|
||||
filterable={results.columns}
|
||||
hideFilterInput
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (<Alert bsStyle="warning">The query returned no data</Alert>);
|
||||
}
|
||||
}
|
||||
ResultSet.propTypes = {
|
||||
query: React.PropTypes.object,
|
||||
showControls: React.PropTypes.bool,
|
||||
search: React.PropTypes.bool,
|
||||
searchText: React.PropTypes.string,
|
||||
};
|
||||
ResultSet.defaultProps = {
|
||||
showControls: true,
|
||||
search: true,
|
||||
searchText: '',
|
||||
};
|
||||
|
||||
export default ResultSet;
|
||||
@@ -1,85 +0,0 @@
|
||||
import { Alert, Button, Tab, Tabs } from 'react-bootstrap';
|
||||
import QueryHistory from './QueryHistory';
|
||||
import ResultSet from './ResultSet';
|
||||
import React from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import shortid from 'shortid';
|
||||
|
||||
class SouthPane extends React.Component {
|
||||
popSelectStar() {
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: this.props.latestQuery.tempTable,
|
||||
autorun: false,
|
||||
dbId: this.props.latestQuery.dbId,
|
||||
sql: `SELECT * FROM ${this.props.latestQuery.tempTable}`,
|
||||
};
|
||||
this.props.actions.addQueryEditor(qe);
|
||||
}
|
||||
render() {
|
||||
let results = <div />;
|
||||
const latestQuery = this.props.latestQuery;
|
||||
if (latestQuery) {
|
||||
if (['running', 'pending'].includes(latestQuery.state)) {
|
||||
results = (
|
||||
<img className="loading" alt="Loading.." src="/static/assets/images/loading.gif" />
|
||||
);
|
||||
} else if (latestQuery.state === 'failed') {
|
||||
results = <Alert bsStyle="danger">{latestQuery.errorMessage}</Alert>;
|
||||
} else if (latestQuery.state === 'success' && latestQuery.ctas) {
|
||||
results = (
|
||||
<div>
|
||||
<Alert bsStyle="info">
|
||||
Table [<strong>{latestQuery.tempTable}</strong>] was created
|
||||
</Alert>
|
||||
<p>
|
||||
<Button
|
||||
bsSize="small"
|
||||
className="m-r-5"
|
||||
onClick={this.popSelectStar.bind(this)}
|
||||
>
|
||||
Query in a new tab
|
||||
</Button>
|
||||
<Button bsSize="small">Visualize</Button>
|
||||
</p>
|
||||
</div>);
|
||||
} else if (latestQuery.state === 'success') {
|
||||
results = <ResultSet showControls search query={latestQuery} />;
|
||||
}
|
||||
} else {
|
||||
results = <Alert bsStyle="info">Run a query to display results here</Alert>;
|
||||
}
|
||||
return (
|
||||
<div className="SouthPane">
|
||||
<Tabs bsStyle="tabs" id={shortid.generate()}>
|
||||
<Tab title="Results" eventKey={1}>
|
||||
<div style={{ overflow: 'auto' }}>
|
||||
{results}
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab title="Query History" eventKey={2}>
|
||||
<QueryHistory />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SouthPane.propTypes = {
|
||||
latestQuery: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
SouthPane.defaultProps = {
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(SouthPane);
|
||||
@@ -1,301 +0,0 @@
|
||||
const $ = require('jquery');
|
||||
import { now } from '../../modules/dates';
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Col,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
Form,
|
||||
FormControl,
|
||||
Label,
|
||||
OverlayTrigger,
|
||||
Row,
|
||||
Tooltip,
|
||||
} from 'react-bootstrap';
|
||||
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/sql';
|
||||
import 'brace/theme/github';
|
||||
import 'brace/ext/language_tools';
|
||||
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
import shortid from 'shortid';
|
||||
import SouthPane from './SouthPane';
|
||||
import Timer from './Timer';
|
||||
|
||||
import SqlEditorLeftBar from './SqlEditorLeftBar';
|
||||
|
||||
class SqlEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
autorun: props.queryEditor.autorun,
|
||||
sql: props.queryEditor.sql,
|
||||
ctas: '',
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
this.onMount();
|
||||
}
|
||||
onMount() {
|
||||
if (this.state.autorun) {
|
||||
this.setState({ autorun: false });
|
||||
this.props.actions.queryEditorSetAutorun(this.props.queryEditor, false);
|
||||
this.startQuery();
|
||||
}
|
||||
}
|
||||
runQuery(runAsync = false) {
|
||||
this.startQuery(runAsync);
|
||||
}
|
||||
startQuery(runAsync = false, ctas = false) {
|
||||
const that = this;
|
||||
const query = {
|
||||
dbId: this.props.queryEditor.dbId,
|
||||
id: shortid.generate(),
|
||||
progress: 0,
|
||||
sql: this.props.queryEditor.sql,
|
||||
sqlEditorId: this.props.queryEditor.id,
|
||||
startDttm: now(),
|
||||
state: 'running',
|
||||
tab: this.props.queryEditor.title,
|
||||
};
|
||||
if (runAsync) {
|
||||
query.state = 'pending';
|
||||
}
|
||||
|
||||
// Execute the Query
|
||||
that.props.actions.startQuery(query);
|
||||
|
||||
const sqlJsonUrl = '/caravel/sql_json/';
|
||||
const sqlJsonRequest = {
|
||||
client_id: query.id,
|
||||
database_id: this.props.queryEditor.dbId,
|
||||
json: true,
|
||||
runAsync,
|
||||
schema: this.props.queryEditor.schema,
|
||||
select_as_cta: ctas,
|
||||
sql: this.props.queryEditor.sql,
|
||||
sql_editor_id: this.props.queryEditor.id,
|
||||
tab: this.props.queryEditor.title,
|
||||
tmp_table_name: this.state.ctas,
|
||||
};
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
url: sqlJsonUrl,
|
||||
data: sqlJsonRequest,
|
||||
success(results) {
|
||||
if (!runAsync) {
|
||||
that.props.actions.querySuccess(query, results);
|
||||
}
|
||||
},
|
||||
error(err, textStatus, errorThrown) {
|
||||
let msg;
|
||||
try {
|
||||
msg = err.responseJSON.error;
|
||||
} catch (e) {
|
||||
if (err.responseText !== undefined) {
|
||||
msg = err.responseText;
|
||||
}
|
||||
}
|
||||
if (textStatus === 'error' && errorThrown === '') {
|
||||
msg = 'Could not connect to server';
|
||||
} else if (msg === null) {
|
||||
msg = `[${textStatus}] ${errorThrown}`;
|
||||
}
|
||||
that.props.actions.queryFailed(query, msg);
|
||||
},
|
||||
});
|
||||
}
|
||||
stopQuery() {
|
||||
this.props.actions.stopQuery(this.props.latestQuery);
|
||||
}
|
||||
createTableAs() {
|
||||
this.startQuery(true, true);
|
||||
}
|
||||
textChange(text) {
|
||||
this.setState({ sql: text });
|
||||
this.props.actions.queryEditorSetSql(this.props.queryEditor, text);
|
||||
}
|
||||
addWorkspaceQuery() {
|
||||
this.props.actions.addWorkspaceQuery({
|
||||
id: shortid.generate(),
|
||||
sql: this.state.sql,
|
||||
dbId: this.props.queryEditor.dbId,
|
||||
schema: this.props.queryEditor.schema,
|
||||
title: this.props.queryEditor.title,
|
||||
});
|
||||
}
|
||||
ctasChange() {}
|
||||
visualize() {}
|
||||
ctasChanged(event) {
|
||||
this.setState({ ctas: event.target.value });
|
||||
}
|
||||
|
||||
sqlEditorHeight() {
|
||||
// quick hack to make the white bg of the tab stretch full height.
|
||||
const tabNavHeight = 40;
|
||||
const navBarHeight = 56;
|
||||
const mysteryVerticalHeight = 50;
|
||||
return window.innerHeight - tabNavHeight - navBarHeight - mysteryVerticalHeight;
|
||||
}
|
||||
|
||||
render() {
|
||||
let runButtons = [];
|
||||
if (this.props.database && this.props.database.allow_run_sync) {
|
||||
runButtons.push(
|
||||
<Button
|
||||
bsSize="small"
|
||||
bsStyle="primary"
|
||||
style={{ width: '100px' }}
|
||||
onClick={this.runQuery.bind(this, false)}
|
||||
disabled={!(this.props.queryEditor.dbId)}
|
||||
key={shortid.generate()}
|
||||
>
|
||||
<i className="fa fa-table" /> Run Query
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
if (this.props.database && this.props.database.allow_run_async) {
|
||||
runButtons.push(
|
||||
<Button
|
||||
bsSize="small"
|
||||
bsStyle="primary"
|
||||
style={{ width: '100px' }}
|
||||
onClick={this.runQuery.bind(this, true)}
|
||||
disabled={!(this.props.queryEditor.dbId)}
|
||||
key={shortid.generate()}
|
||||
>
|
||||
<i className="fa fa-table" /> Run Async
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
runButtons = (
|
||||
<ButtonGroup bsSize="small" className="inline m-r-5 pull-left">
|
||||
{runButtons}
|
||||
</ButtonGroup>
|
||||
);
|
||||
if (this.props.latestQuery && this.props.latestQuery.state === 'running') {
|
||||
runButtons = (
|
||||
<ButtonGroup bsSize="small" className="inline m-r-5 pull-left">
|
||||
<Button
|
||||
bsStyle="primary"
|
||||
bsSize="small"
|
||||
style={{ width: '100px' }}
|
||||
onClick={this.stopQuery.bind(this)}
|
||||
>
|
||||
<a className="fa fa-stop" /> Stop
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
let limitWarning = null;
|
||||
if (this.props.latestQuery && this.props.latestQuery.limit_reached) {
|
||||
const tooltip = (
|
||||
<Tooltip id="tooltip">
|
||||
It appears that the number of rows in the query results displayed
|
||||
was limited on the server side to
|
||||
the {this.props.latestQuery.rows} limit.
|
||||
</Tooltip>
|
||||
);
|
||||
limitWarning = (
|
||||
<OverlayTrigger placement="left" overlay={tooltip}>
|
||||
<Label bsStyle="warning" className="m-r-5">LIMIT</Label>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
let ctasControls;
|
||||
if (this.props.database && this.props.database.allow_ctas) {
|
||||
ctasControls = (
|
||||
<FormGroup>
|
||||
<InputGroup>
|
||||
<FormControl
|
||||
type="text"
|
||||
bsSize="small"
|
||||
className="input-sm"
|
||||
placeholder="new table name"
|
||||
onChange={this.ctasChanged.bind(this)}
|
||||
/>
|
||||
<InputGroup.Button>
|
||||
<Button
|
||||
bsSize="small"
|
||||
disabled={this.state.ctas.length === 0}
|
||||
onClick={this.createTableAs.bind(this)}
|
||||
>
|
||||
<i className="fa fa-table" /> CTAS
|
||||
</Button>
|
||||
</InputGroup.Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
const editorBottomBar = (
|
||||
<div className="sql-toolbar clearfix">
|
||||
<div className="pull-left">
|
||||
<Form inline>
|
||||
{runButtons}
|
||||
{ctasControls}
|
||||
</Form>
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
{limitWarning}
|
||||
<Timer query={this.props.latestQuery} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="SqlEditor" style={{ minHeight: this.sqlEditorHeight() }}>
|
||||
<Row>
|
||||
<Col md={3}>
|
||||
<SqlEditorLeftBar queryEditor={this.props.queryEditor} />
|
||||
</Col>
|
||||
<Col md={9}>
|
||||
<AceEditor
|
||||
mode="sql"
|
||||
name={this.props.queryEditor.id}
|
||||
theme="github"
|
||||
minLines={7}
|
||||
maxLines={30}
|
||||
onChange={this.textChange.bind(this)}
|
||||
height="200px"
|
||||
width="100%"
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
enableBasicAutocompletion
|
||||
value={this.props.queryEditor.sql}
|
||||
/>
|
||||
{editorBottomBar}
|
||||
<br />
|
||||
<SouthPane latestQuery={this.props.latestQuery} sqlEditor={this} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SqlEditor.propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
database: React.PropTypes.object,
|
||||
latestQuery: React.PropTypes.object,
|
||||
queryEditor: React.PropTypes.object,
|
||||
};
|
||||
|
||||
SqlEditor.defaultProps = {
|
||||
};
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SqlEditor);
|
||||
@@ -1,195 +0,0 @@
|
||||
const $ = window.$ = require('jquery');
|
||||
import React from 'react';
|
||||
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import * as Actions from '../actions';
|
||||
import Select from 'react-select';
|
||||
import { Label, Button } from 'react-bootstrap';
|
||||
import TableElement from './TableElement';
|
||||
import DatabaseSelect from './DatabaseSelect';
|
||||
|
||||
|
||||
class SqlEditorLeftBar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
schemaLoading: false,
|
||||
schemaOptions: [],
|
||||
tableLoading: false,
|
||||
tableOptions: [],
|
||||
};
|
||||
}
|
||||
componentWillMount() {
|
||||
this.fetchSchemas();
|
||||
this.fetchTables();
|
||||
}
|
||||
onChange(db) {
|
||||
const val = (db) ? db.value : null;
|
||||
this.setState({ schemaOptions: [] });
|
||||
this.props.actions.queryEditorSetDb(this.props.queryEditor, val);
|
||||
if (!(db)) {
|
||||
this.setState({ tableOptions: [] });
|
||||
} else {
|
||||
this.fetchTables(val, this.props.queryEditor.schema);
|
||||
this.fetchSchemas(val);
|
||||
}
|
||||
}
|
||||
resetState() {
|
||||
this.props.actions.resetState();
|
||||
}
|
||||
fetchTables(dbId, schema) {
|
||||
const actualDbId = dbId || this.props.queryEditor.dbId;
|
||||
if (actualDbId) {
|
||||
const actualSchema = schema || this.props.queryEditor.schema;
|
||||
this.setState({ tableLoading: true });
|
||||
this.setState({ tableOptions: [] });
|
||||
const url = `/caravel/tables/${actualDbId}/${actualSchema}`;
|
||||
$.get(url, (data) => {
|
||||
let tableOptions = data.tables.map((s) => ({ value: s, label: s }));
|
||||
const views = data.views.map((s) => ({ value: s, label: '[view] ' + s }));
|
||||
tableOptions = [...tableOptions, ...views];
|
||||
this.setState({ tableOptions });
|
||||
this.setState({ tableLoading: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
changeSchema(schemaOpt) {
|
||||
const schema = (schemaOpt) ? schemaOpt.value : null;
|
||||
this.props.actions.queryEditorSetSchema(this.props.queryEditor, schema);
|
||||
this.fetchTables(this.props.queryEditor.dbId, schema);
|
||||
}
|
||||
fetchSchemas(dbId) {
|
||||
const actualDbId = dbId || this.props.queryEditor.dbId;
|
||||
if (actualDbId) {
|
||||
this.setState({ schemaLoading: true });
|
||||
const url = `/databasetablesasync/api/read?_flt_0_id=${actualDbId}`;
|
||||
$.get(url, (data) => {
|
||||
const schemas = data.result[0].all_schema_names;
|
||||
const schemaOptions = schemas.map((s) => ({ value: s, label: s }));
|
||||
this.setState({ schemaOptions });
|
||||
this.setState({ schemaLoading: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
closePopover(ref) {
|
||||
this.refs[ref].hide();
|
||||
}
|
||||
changeTable(tableOpt) {
|
||||
const tableName = tableOpt.value;
|
||||
const qe = this.props.queryEditor;
|
||||
let url = `/caravel/table/${qe.dbId}/${tableName}/${qe.schema}/`;
|
||||
|
||||
this.setState({ tableLoading: true });
|
||||
$.get(url, (data) => {
|
||||
this.props.actions.mergeTable({
|
||||
dbId: this.props.queryEditor.dbId,
|
||||
queryEditorId: this.props.queryEditor.id,
|
||||
name: data.name,
|
||||
indexes: data.indexes,
|
||||
schema: qe.schema,
|
||||
columns: data.columns,
|
||||
expanded: true,
|
||||
});
|
||||
this.setState({ tableLoading: false });
|
||||
})
|
||||
.fail(() => {
|
||||
this.props.actions.addAlert({
|
||||
msg: 'Error occurred while fetching metadata',
|
||||
bsStyle: 'danger',
|
||||
});
|
||||
this.setState({ tableLoading: false });
|
||||
});
|
||||
|
||||
url = `/caravel/extra_table_metadata/${qe.dbId}/${tableName}/${qe.schema}/`;
|
||||
$.get(url, (data) => {
|
||||
const table = {
|
||||
dbId: this.props.queryEditor.dbId,
|
||||
queryEditorId: this.props.queryEditor.id,
|
||||
schema: qe.schema,
|
||||
name: tableName,
|
||||
};
|
||||
Object.assign(table, data);
|
||||
this.props.actions.mergeTable(table);
|
||||
});
|
||||
}
|
||||
render() {
|
||||
let networkAlert = null;
|
||||
if (!this.props.networkOn) {
|
||||
networkAlert = <p><Label bsStyle="danger">OFFLINE</Label></p>;
|
||||
}
|
||||
const tables = this.props.tables.filter((t) => (t.queryEditorId === this.props.queryEditor.id));
|
||||
const shouldShowReset = window.location.search === '?reset=1';
|
||||
return (
|
||||
<div className="clearfix sql-toolbar">
|
||||
{networkAlert}
|
||||
<div>
|
||||
<DatabaseSelect
|
||||
onChange={this.onChange.bind(this)}
|
||||
databaseId={this.props.queryEditor.dbId}
|
||||
/>
|
||||
</div>
|
||||
<div className="m-t-5">
|
||||
<Select
|
||||
name="select-schema"
|
||||
placeholder={`Select a schema (${this.state.schemaOptions.length})`}
|
||||
options={this.state.schemaOptions}
|
||||
value={this.props.queryEditor.schema}
|
||||
isLoading={this.state.schemaLoading}
|
||||
autosize={false}
|
||||
onChange={this.changeSchema.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<div className="m-t-5">
|
||||
<Select
|
||||
name="select-table"
|
||||
ref="selectTable"
|
||||
isLoading={this.state.tableLoading}
|
||||
placeholder={`Add a table (${this.state.tableOptions.length})`}
|
||||
autosize={false}
|
||||
value={this.state.tableName}
|
||||
onChange={this.changeTable.bind(this)}
|
||||
options={this.state.tableOptions}
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="m-t-5">
|
||||
{tables.map((table) => (
|
||||
<TableElement table={table} queryEditor={this.props.queryEditor} key={table.id} />
|
||||
))}
|
||||
</div>
|
||||
{shouldShowReset &&
|
||||
<Button bsSize="small" bsStyle="danger" onClick={this.resetState.bind(this)}>
|
||||
<i className="fa fa-bomb" /> Reset State
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SqlEditorLeftBar.propTypes = {
|
||||
queryEditor: React.PropTypes.object,
|
||||
tables: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
networkOn: React.PropTypes.bool,
|
||||
};
|
||||
|
||||
SqlEditorLeftBar.defaultProps = {
|
||||
tables: [],
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
tables: state.tables,
|
||||
networkOn: state.networkOn,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SqlEditorLeftBar);
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import { github } from 'react-syntax-highlighter/dist/styles';
|
||||
|
||||
const SqlShrink = (props) => {
|
||||
const sql = props.sql || '';
|
||||
let lines = sql.split('\n');
|
||||
if (lines.length >= props.maxLines) {
|
||||
lines = lines.slice(0, props.maxLines);
|
||||
lines.push('{...}');
|
||||
}
|
||||
const shrunk = lines.map((line) => {
|
||||
if (line.length > props.maxWidth) {
|
||||
return line.slice(0, props.maxWidth) + '{...}';
|
||||
}
|
||||
return line;
|
||||
})
|
||||
.join('\n');
|
||||
return (
|
||||
<div>
|
||||
<SyntaxHighlighter language="sql" style={github}>
|
||||
{shrunk}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SqlShrink.defaultProps = {
|
||||
maxWidth: 60,
|
||||
maxLines: 6,
|
||||
};
|
||||
|
||||
SqlShrink.propTypes = {
|
||||
sql: React.PropTypes.string,
|
||||
maxWidth: React.PropTypes.number,
|
||||
maxLines: React.PropTypes.number,
|
||||
};
|
||||
|
||||
export default SqlShrink;
|
||||
@@ -1,170 +0,0 @@
|
||||
import React from 'react';
|
||||
import { DropdownButton, MenuItem, Tab, Tabs } from 'react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import SqlEditor from './SqlEditor';
|
||||
import shortid from 'shortid';
|
||||
import { getParamFromQuery, getLink } from '../../../utils/common';
|
||||
import CopyQueryTabUrl from './CopyQueryTabUrl';
|
||||
|
||||
let queryCount = 1;
|
||||
|
||||
class TabbedSqlEditors extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const uri = window.location.toString();
|
||||
const search = window.location.search;
|
||||
const cleanUri = search ? uri.substring(0, uri.indexOf('?')) : uri;
|
||||
const query = search.substring(1);
|
||||
this.state = {
|
||||
uri,
|
||||
cleanUri,
|
||||
query,
|
||||
};
|
||||
}
|
||||
componentWillMount() {
|
||||
if (this.state.query) {
|
||||
queryCount++;
|
||||
const queryEditorProps = {
|
||||
id: shortid.generate(),
|
||||
title: getParamFromQuery(this.state.query, 'title'),
|
||||
dbId: getParamFromQuery(this.state.query, 'dbid'),
|
||||
schema: getParamFromQuery(this.state.query, 'schema'),
|
||||
autorun: getParamFromQuery(this.state.query, 'autorun'),
|
||||
sql: getParamFromQuery(this.state.query, 'sql'),
|
||||
};
|
||||
this.props.actions.addQueryEditor(queryEditorProps);
|
||||
// Clean the url in browser history
|
||||
window.history.replaceState({}, document.title, this.state.cleanUri);
|
||||
}
|
||||
}
|
||||
getQueryLink(qe) {
|
||||
const params = [];
|
||||
if (qe.dbId) params.push('dbid=' + qe.dbId);
|
||||
if (qe.title) params.push('title=' + qe.title);
|
||||
if (qe.schema) params.push('schema=' + qe.schema);
|
||||
if (qe.autorun) params.push('autorun=' + qe.autorun);
|
||||
if (qe.sql) params.push('sql=' + qe.sql);
|
||||
|
||||
return getLink(this.state.cleanUri, params);
|
||||
}
|
||||
renameTab(qe) {
|
||||
/* eslint no-alert: 0 */
|
||||
const newTitle = prompt('Enter a new title for the tab');
|
||||
if (newTitle) {
|
||||
this.props.actions.queryEditorSetTitle(qe, newTitle);
|
||||
}
|
||||
}
|
||||
activeQueryEditor() {
|
||||
const qeid = this.props.tabHistory[this.props.tabHistory.length - 1];
|
||||
for (let i = 0; i < this.props.queryEditors.length; i++) {
|
||||
const qe = this.props.queryEditors[i];
|
||||
if (qe.id === qeid) {
|
||||
return qe;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
newQueryEditor() {
|
||||
queryCount++;
|
||||
const activeQueryEditor = this.activeQueryEditor();
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: `Untitled Query ${queryCount}`,
|
||||
dbId: (activeQueryEditor) ? activeQueryEditor.dbId : null,
|
||||
schema: (activeQueryEditor) ? activeQueryEditor.schema : null,
|
||||
autorun: false,
|
||||
sql: 'SELECT ...',
|
||||
};
|
||||
this.props.actions.addQueryEditor(qe);
|
||||
}
|
||||
handleSelect(key) {
|
||||
if (key === 'add_tab') {
|
||||
this.newQueryEditor();
|
||||
} else {
|
||||
this.props.actions.setActiveQueryEditor({ id: key });
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const editors = this.props.queryEditors.map((qe, i) => {
|
||||
let latestQuery = this.props.queries[qe.latestQueryId];
|
||||
const database = this.props.databases[qe.dbId];
|
||||
const state = (latestQuery) ? latestQuery.state : '';
|
||||
const tabTitle = (
|
||||
<div>
|
||||
<div className={'circle ' + state} /> {qe.title} {' '}
|
||||
<DropdownButton
|
||||
bsSize="small"
|
||||
id={'ddbtn-tab-' + i}
|
||||
title=""
|
||||
>
|
||||
<MenuItem eventKey="1" onClick={this.props.actions.removeQueryEditor.bind(this, qe)}>
|
||||
<i className="fa fa-close" /> close tab
|
||||
</MenuItem>
|
||||
<MenuItem eventKey="2" onClick={this.renameTab.bind(this, qe)}>
|
||||
<i className="fa fa-i-cursor" /> rename tab
|
||||
</MenuItem>
|
||||
<MenuItem eventKey="3">
|
||||
<i className="fa fa-clipboard" /> <CopyQueryTabUrl qe={qe} />
|
||||
</MenuItem>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Tab
|
||||
key={qe.id}
|
||||
title={tabTitle}
|
||||
eventKey={qe.id}
|
||||
>
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-body">
|
||||
<SqlEditor
|
||||
queryEditor={qe}
|
||||
latestQuery={latestQuery}
|
||||
database={database}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>);
|
||||
});
|
||||
return (
|
||||
<Tabs
|
||||
bsStyle="tabs"
|
||||
activeKey={this.props.tabHistory[this.props.tabHistory.length - 1]}
|
||||
onSelect={this.handleSelect.bind(this)}
|
||||
id="a11y-query-editor-tabs"
|
||||
>
|
||||
{editors}
|
||||
<Tab title={<div><i className="fa fa-plus-circle" /> </div>} eventKey="add_tab" />
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
}
|
||||
TabbedSqlEditors.propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
databases: React.PropTypes.object,
|
||||
queries: React.PropTypes.object,
|
||||
queryEditors: React.PropTypes.array,
|
||||
tabHistory: React.PropTypes.array,
|
||||
};
|
||||
TabbedSqlEditors.defaultProps = {
|
||||
tabHistory: [],
|
||||
queryEditors: [],
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
databases: state.databases,
|
||||
queryEditors: state.queryEditors,
|
||||
queries: state.queries,
|
||||
tabHistory: state.tabHistory,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TabbedSqlEditors);
|
||||
@@ -1,214 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ButtonGroup, Well } from 'react-bootstrap';
|
||||
import Link from './Link';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import shortid from 'shortid';
|
||||
import ModalTrigger from '../../components/ModalTrigger';
|
||||
import CopyToClipboard from '../../components/CopyToClipboard';
|
||||
|
||||
const propTypes = {
|
||||
table: React.PropTypes.object,
|
||||
queryEditor: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
table: null,
|
||||
actions: {},
|
||||
};
|
||||
|
||||
class TableElement extends React.Component {
|
||||
setSelectStar() {
|
||||
this.props.actions.queryEditorSetSql(this.props.queryEditor, this.selectStar());
|
||||
}
|
||||
|
||||
selectStar() {
|
||||
let cols = '';
|
||||
this.props.table.columns.forEach((col, i) => {
|
||||
cols += col.name;
|
||||
if (i < this.props.table.columns.length - 1) {
|
||||
cols += ', ';
|
||||
}
|
||||
});
|
||||
let tableName = this.props.table.name;
|
||||
if (this.props.table.schema) {
|
||||
tableName = this.props.table.schema + '.' + tableName;
|
||||
}
|
||||
return `SELECT ${cols}\nFROM ${tableName}`;
|
||||
}
|
||||
|
||||
popSelectStar() {
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: this.props.table.name,
|
||||
dbId: this.props.table.dbId,
|
||||
autorun: true,
|
||||
sql: this.selectStar(),
|
||||
};
|
||||
this.props.actions.addQueryEditor(qe);
|
||||
}
|
||||
|
||||
collapseTable(e) {
|
||||
e.preventDefault();
|
||||
this.props.actions.collapseTable(this.props.table);
|
||||
}
|
||||
|
||||
expandTable(e) {
|
||||
e.preventDefault();
|
||||
this.props.actions.expandTable(this.props.table);
|
||||
}
|
||||
|
||||
removeTable() {
|
||||
this.props.actions.removeTable(this.props.table);
|
||||
}
|
||||
|
||||
render() {
|
||||
const table = this.props.table;
|
||||
let metadata = null;
|
||||
let buttonToggle;
|
||||
|
||||
let header;
|
||||
if (table.partitions) {
|
||||
let partitionQuery;
|
||||
let partitionClipBoard;
|
||||
if (table.partitions.partitionQuery) {
|
||||
partitionQuery = table.partitions.partitionQuery;
|
||||
const tt = 'Copy partition query to clipboard';
|
||||
partitionClipBoard = (
|
||||
<CopyToClipboard
|
||||
text={partitionQuery}
|
||||
shouldShowText={false}
|
||||
tooltipText={tt}
|
||||
copyNode={<i className="fa fa-clipboard" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
let latest = [];
|
||||
for (const k in table.partitions.latest) {
|
||||
latest.push(`${k}=${table.partitions.latest[k]}`);
|
||||
}
|
||||
latest = latest.join('/');
|
||||
header = (
|
||||
<Well bsSize="small">
|
||||
<div>
|
||||
<small>
|
||||
latest partition: {latest}
|
||||
</small> {partitionClipBoard}
|
||||
</div>
|
||||
</Well>
|
||||
);
|
||||
}
|
||||
if (table.expanded) {
|
||||
buttonToggle = (
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => { this.collapseTable(e); }}
|
||||
>
|
||||
<strong>{table.name}</strong>
|
||||
<small className="m-l-5"><i className="fa fa-minus" /></small>
|
||||
</a>
|
||||
);
|
||||
metadata = (
|
||||
<div>
|
||||
{header}
|
||||
<div className="table-columns">
|
||||
{table.columns.map((col) => {
|
||||
let name = col.name;
|
||||
if (col.indexed) {
|
||||
name = <strong>{col.name}</strong>;
|
||||
}
|
||||
return (
|
||||
<div className="clearfix table-column" key={shortid.generate()}>
|
||||
<div className="pull-left m-l-10">
|
||||
{name}
|
||||
</div>
|
||||
<div className="pull-right text-muted">
|
||||
<small> {col.type}</small>
|
||||
</div>
|
||||
</div>);
|
||||
})}
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
buttonToggle = (
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => { this.expandTable(e); }}
|
||||
>
|
||||
{table.name}
|
||||
<small className="m-l-5"><i className="fa fa-plus" /></small>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
let keyLink;
|
||||
if (table.indexes && table.indexes.length > 0) {
|
||||
keyLink = (
|
||||
<ModalTrigger
|
||||
modalTitle={
|
||||
<div>
|
||||
Keys for table <strong>{table.name}</strong>
|
||||
</div>
|
||||
}
|
||||
modalBody={
|
||||
<pre>{JSON.stringify(table.indexes, null, 4)}</pre>
|
||||
}
|
||||
triggerNode={
|
||||
<Link
|
||||
className="fa fa-key pull-left m-l-2"
|
||||
tooltip={`View indexes (${table.indexes.length})`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="TableElement">
|
||||
<div className="clearfix">
|
||||
<div className="pull-left">
|
||||
{buttonToggle}
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
<ButtonGroup className="ws-el-controls pull-right">
|
||||
{keyLink}
|
||||
<Link
|
||||
className="fa fa-pencil pull-left m-l-2"
|
||||
onClick={this.setSelectStar.bind(this)}
|
||||
tooltip="Run query in this tab"
|
||||
href="#"
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-plus-circle pull-left m-l-2"
|
||||
onClick={this.popSelectStar.bind(this)}
|
||||
tooltip="Run query in a new tab"
|
||||
href="#"
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-trash pull-left m-l-2"
|
||||
onClick={this.removeTable.bind(this)}
|
||||
tooltip="Remove from workspace"
|
||||
href="#"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{metadata}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
TableElement.propTypes = propTypes;
|
||||
TableElement.defaultProps = defaultProps;
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(TableElement);
|
||||
export { TableElement };
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
|
||||
|
||||
const TableMetadata = function (props) {
|
||||
return (
|
||||
<BootstrapTable
|
||||
condensed
|
||||
data={props.table.columns}
|
||||
>
|
||||
<TableHeaderColumn dataField="id" isKey hidden>
|
||||
id
|
||||
</TableHeaderColumn>
|
||||
<TableHeaderColumn dataField="name">Name</TableHeaderColumn>
|
||||
<TableHeaderColumn dataField="type">Type</TableHeaderColumn>
|
||||
</BootstrapTable>
|
||||
);
|
||||
};
|
||||
|
||||
TableMetadata.propTypes = {
|
||||
table: React.PropTypes.object,
|
||||
};
|
||||
|
||||
export default TableMetadata;
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from 'react';
|
||||
import { now, fDuration } from '../../modules/dates';
|
||||
|
||||
import { STATE_BSSTYLE_MAP } from '../common.js';
|
||||
|
||||
class Timer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
clockStr: '',
|
||||
};
|
||||
}
|
||||
componentWillMount() {
|
||||
this.startTimer();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.stopTimer();
|
||||
}
|
||||
startTimer() {
|
||||
if (!(this.timer)) {
|
||||
this.timer = setInterval(this.stopwatch.bind(this), 30);
|
||||
}
|
||||
}
|
||||
stopTimer() {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
stopwatch() {
|
||||
if (this.props && this.props.query) {
|
||||
const endDttm = this.props.query.endDttm || now();
|
||||
const clockStr = fDuration(this.props.query.startDttm, endDttm);
|
||||
this.setState({ clockStr });
|
||||
if (this.props.query.state !== 'running') {
|
||||
this.stopTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
render() {
|
||||
if (this.props.query && this.props.query.state === 'running') {
|
||||
this.startTimer();
|
||||
}
|
||||
let timerSpan = null;
|
||||
if (this.props && this.props.query) {
|
||||
const bsStyle = STATE_BSSTYLE_MAP[this.props.query.state];
|
||||
timerSpan = (
|
||||
<span className={'inlineBlock m-r-5 label label-' + bsStyle}>
|
||||
{this.state.clockStr}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return timerSpan;
|
||||
}
|
||||
}
|
||||
Timer.propTypes = {
|
||||
query: React.PropTypes.object,
|
||||
};
|
||||
Timer.defaultProps = {
|
||||
query: null,
|
||||
};
|
||||
|
||||
export default Timer;
|
||||
@@ -1,27 +0,0 @@
|
||||
const $ = window.$ = require('jquery');
|
||||
const jQuery = window.jQuery = $; // eslint-disable-line
|
||||
require('bootstrap');
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { initialState, sqlLabReducer } from './reducers';
|
||||
import { enhancer } from '../reduxUtils';
|
||||
import { createStore } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import App from './components/App';
|
||||
|
||||
|
||||
require('./main.css');
|
||||
|
||||
let store = createStore(sqlLabReducer, initialState, enhancer());
|
||||
|
||||
// jquery hack to highlight the navbar menu
|
||||
$('a:contains("SQL Lab")').parent().addClass('active');
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
document.getElementById('app')
|
||||
);
|
||||
@@ -1,5 +0,0 @@
|
||||
require('../node_modules/select2/select2.css');
|
||||
require('../node_modules/select2-bootstrap-css/select2-bootstrap.min.css');
|
||||
require('../node_modules/jquery-ui/themes/base/jquery-ui.css');
|
||||
require('select2');
|
||||
require('../vendor/select2.sortable.js');
|
||||
@@ -1,59 +0,0 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Modal } from 'react-bootstrap';
|
||||
import cx from 'classnames';
|
||||
|
||||
const propTypes = {
|
||||
triggerNode: PropTypes.node.isRequired,
|
||||
modalTitle: PropTypes.node.isRequired,
|
||||
modalBody: PropTypes.node.isRequired,
|
||||
beforeOpen: PropTypes.func,
|
||||
isButton: PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
beforeOpen: () => {},
|
||||
isButton: false,
|
||||
};
|
||||
|
||||
export default class ModalTrigger extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showModal: false,
|
||||
};
|
||||
this.open = this.open.bind(this);
|
||||
this.close = this.close.bind(this);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.setState({ showModal: false });
|
||||
}
|
||||
|
||||
open(e) {
|
||||
e.preventDefault();
|
||||
this.props.beforeOpen();
|
||||
this.setState({ showModal: true });
|
||||
}
|
||||
|
||||
render() {
|
||||
const classNames = cx({
|
||||
'btn btn-default btn-sm': this.props.isButton,
|
||||
});
|
||||
return (
|
||||
<a href="#" className={classNames} onClick={this.open}>
|
||||
{this.props.triggerNode}
|
||||
<Modal show={this.state.showModal} onHide={this.close}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{this.props.modalTitle}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{this.props.modalBody}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ModalTrigger.propTypes = propTypes;
|
||||
ModalTrigger.defaultProps = defaultProps;
|
||||
@@ -1,42 +0,0 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
|
||||
const propTypes = {
|
||||
modalId: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
modalContent: PropTypes.node,
|
||||
customButton: PropTypes.node,
|
||||
};
|
||||
|
||||
function Modal({ modalId, title, modalContent, customButton }) {
|
||||
return (
|
||||
<div className="modal fade" id={modalId} role="dialog">
|
||||
<div className="modal-dialog" role="document">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 className="modal-title">{title}</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{modalContent}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-default"
|
||||
data-dismiss="modal"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{customButton}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Modal.propTypes = propTypes;
|
||||
|
||||
export default Modal;
|
||||
@@ -1,37 +0,0 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import ModalTrigger from './../../components/ModalTrigger';
|
||||
|
||||
const propTypes = {
|
||||
slice: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default class DisplayQueryButton extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
viewSqlQuery: '',
|
||||
};
|
||||
this.beforeOpen = this.beforeOpen.bind(this);
|
||||
}
|
||||
|
||||
beforeOpen() {
|
||||
this.setState({
|
||||
viewSqlQuery: this.props.slice.viewSqlQuery,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const modalBody = (<pre>{this.state.viewSqlQuery}</pre>);
|
||||
return (
|
||||
<ModalTrigger
|
||||
isButton
|
||||
triggerNode={<span>Query</span>}
|
||||
modalTitle="Query"
|
||||
modalBody={modalBody}
|
||||
beforeOpen={this.beforeOpen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DisplayQueryButton.propTypes = propTypes;
|
||||
@@ -1,45 +0,0 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import cx from 'classnames';
|
||||
import URLShortLinkButton from './URLShortLinkButton';
|
||||
import EmbedCodeButton from './EmbedCodeButton';
|
||||
import DisplayQueryButton from './DisplayQueryButton';
|
||||
|
||||
const propTypes = {
|
||||
canDownload: PropTypes.string.isRequired,
|
||||
slice: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default function ExploreActionButtons({ canDownload, slice }) {
|
||||
const exportToCSVClasses = cx('btn btn-default btn-sm', {
|
||||
'disabled disabledButton': !canDownload,
|
||||
});
|
||||
return (
|
||||
<div className="btn-group results" role="group">
|
||||
<URLShortLinkButton slice={slice} />
|
||||
|
||||
<EmbedCodeButton slice={slice} />
|
||||
|
||||
<a
|
||||
href={slice.data.json_endpoint}
|
||||
className="btn btn-default btn-sm"
|
||||
title="Export to .json"
|
||||
target="_blank"
|
||||
>
|
||||
<i className="fa fa-file-code-o"></i> .json
|
||||
</a>
|
||||
|
||||
<a
|
||||
href={slice.data.csv_endpoint}
|
||||
className={exportToCSVClasses}
|
||||
title="Export to .csv format"
|
||||
target="_blank"
|
||||
>
|
||||
<i className="fa fa-file-text-o"></i> .csv
|
||||
</a>
|
||||
|
||||
<DisplayQueryButton slice={slice} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ExploreActionButtons.propTypes = propTypes;
|
||||
@@ -1,31 +0,0 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const propTypes = {
|
||||
canAdd: PropTypes.string.isRequired,
|
||||
onQuery: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default function QueryAndSaveBtns({ canAdd, onQuery }) {
|
||||
const saveClasses = classnames('btn btn-default btn-sm', {
|
||||
'disabled disabledButton': canAdd !== 'True',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="btn-group query-and-save">
|
||||
<button type="button" className="btn btn-primary btn-sm" onClick={onQuery}>
|
||||
<i className="fa fa-bolt"></i> Query
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={saveClasses}
|
||||
data-target="#save_modal"
|
||||
data-toggle="modal"
|
||||
>
|
||||
<i className="fa fa-plus-circle"></i> Save as
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
QueryAndSaveBtns.propTypes = propTypes;
|
||||
@@ -1,401 +0,0 @@
|
||||
// Javascript for the explorer page
|
||||
// Init explorer view -> load vis dependencies -> read data (from dynamic html) -> render slice
|
||||
// nb: to add a new vis, you must also add a Python fn in viz.py
|
||||
//
|
||||
// js
|
||||
const $ = window.$ = require('jquery');
|
||||
const px = require('./../modules/caravel.js');
|
||||
const utils = require('./../modules/utils.js');
|
||||
const jQuery = window.jQuery = require('jquery'); // eslint-disable-line
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import QueryAndSaveBtns from './components/QueryAndSaveBtns.jsx';
|
||||
import ExploreActionButtons from './components/ExploreActionButtons.jsx';
|
||||
|
||||
require('jquery-ui');
|
||||
$.widget.bridge('uitooltip', $.ui.tooltip); // Shutting down jq-ui tooltips
|
||||
require('bootstrap');
|
||||
|
||||
require('./../caravel-select2.js');
|
||||
|
||||
// css
|
||||
require('../../vendor/pygments.css');
|
||||
require('../../stylesheets/explore.css');
|
||||
|
||||
let slice;
|
||||
|
||||
const getPanelClass = function (fieldPrefix) {
|
||||
return (fieldPrefix === 'flt' ? 'filter' : 'having') + '_panel';
|
||||
};
|
||||
|
||||
function prepForm() {
|
||||
// Assigning the right id to form elements in filters
|
||||
const fixId = function ($filter, fieldPrefix, i) {
|
||||
$filter.attr('id', function () {
|
||||
return fieldPrefix + '_' + i;
|
||||
});
|
||||
|
||||
['col', 'op', 'eq'].forEach(function (fieldMiddle) {
|
||||
const fieldName = fieldPrefix + '_' + fieldMiddle;
|
||||
$filter.find('[id^=' + fieldName + '_]')
|
||||
.attr('id', function () {
|
||||
return fieldName + '_' + i;
|
||||
})
|
||||
.attr('name', function () {
|
||||
return fieldName + '_' + i;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
['flt', 'having'].forEach(function (fieldPrefix) {
|
||||
let i = 1;
|
||||
$('#' + getPanelClass(fieldPrefix) + ' #filters > div').each(function () {
|
||||
fixId($(this), fieldPrefix, i);
|
||||
i++;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function query(forceUpdate, pushState) {
|
||||
let force = forceUpdate;
|
||||
if (force === undefined) {
|
||||
force = false;
|
||||
}
|
||||
$('.query-and-save button').attr('disabled', 'disabled');
|
||||
if (force) { // Don't hide the alert message when the page is just loaded
|
||||
$('div.alert').remove();
|
||||
}
|
||||
$('#is_cached').hide();
|
||||
prepForm();
|
||||
|
||||
if (pushState !== false) {
|
||||
// update the url after prepForm() fix the field ids
|
||||
history.pushState({}, document.title, slice.querystring());
|
||||
}
|
||||
slice.render(force);
|
||||
}
|
||||
|
||||
function saveSlice() {
|
||||
const action = $('input[name=rdo_save]:checked').val();
|
||||
if (action === 'saveas') {
|
||||
const sliceName = $('input[name=new_slice_name]').val();
|
||||
if (sliceName === '') {
|
||||
utils.showModal({
|
||||
title: 'Error',
|
||||
body: 'You must pick a name for the new slice',
|
||||
});
|
||||
return;
|
||||
}
|
||||
document.getElementById('slice_name').value = sliceName;
|
||||
}
|
||||
const addToDash = $('input[name=addToDash]:checked').val();
|
||||
if (addToDash === 'existing' && $('#save_to_dashboard_id').val() === '') {
|
||||
utils.showModal({
|
||||
title: 'Error',
|
||||
body: 'You must pick an existing dashboard',
|
||||
});
|
||||
return;
|
||||
} else if (addToDash === 'new' && $('input[name=new_dashboard_name]').val() === '') {
|
||||
utils.showModal({
|
||||
title: 'Error',
|
||||
body: 'Please enter a name for the new dashboard',
|
||||
});
|
||||
return;
|
||||
}
|
||||
$('#action').val(action);
|
||||
prepForm();
|
||||
$('#query').submit();
|
||||
}
|
||||
|
||||
function initExploreView() {
|
||||
function getCollapsedFieldsets() {
|
||||
let collapsedFieldsets = $('#collapsedFieldsets').val();
|
||||
|
||||
if (collapsedFieldsets !== undefined && collapsedFieldsets !== '') {
|
||||
collapsedFieldsets = collapsedFieldsets.split('||');
|
||||
} else {
|
||||
collapsedFieldsets = [];
|
||||
}
|
||||
return collapsedFieldsets;
|
||||
}
|
||||
|
||||
function toggleFieldset(legend, animation) {
|
||||
const parent = legend.parent();
|
||||
const fieldset = parent.find('.legend_label').text();
|
||||
const collapsedFieldsets = getCollapsedFieldsets();
|
||||
let index;
|
||||
|
||||
if (parent.hasClass('collapsed')) {
|
||||
if (animation) {
|
||||
parent.find('.panel-body').slideDown();
|
||||
} else {
|
||||
parent.find('.panel-body').show();
|
||||
}
|
||||
parent.removeClass('collapsed');
|
||||
parent.find('span.collapser').text('[-]');
|
||||
|
||||
// removing from array, js is overcomplicated
|
||||
index = collapsedFieldsets.indexOf(fieldset);
|
||||
if (index !== -1) {
|
||||
collapsedFieldsets.splice(index, 1);
|
||||
}
|
||||
} else { // not collapsed
|
||||
if (animation) {
|
||||
parent.find('.panel-body').slideUp();
|
||||
} else {
|
||||
parent.find('.panel-body').hide();
|
||||
}
|
||||
|
||||
parent.addClass('collapsed');
|
||||
parent.find('span.collapser').text('[+]');
|
||||
index = collapsedFieldsets.indexOf(fieldset);
|
||||
if (index === -1 && fieldset !== '' && fieldset !== undefined) {
|
||||
collapsedFieldsets.push(fieldset);
|
||||
}
|
||||
}
|
||||
|
||||
$('#collapsedFieldsets').val(collapsedFieldsets.join('||'));
|
||||
}
|
||||
|
||||
px.initFavStars();
|
||||
|
||||
$('#viz_type').change(function () {
|
||||
$('#query').submit();
|
||||
});
|
||||
|
||||
$('#datasource_id').change(function () {
|
||||
window.location = $(this).find('option:selected').attr('url');
|
||||
});
|
||||
|
||||
const collapsedFieldsets = getCollapsedFieldsets();
|
||||
for (let i = 0; i < collapsedFieldsets.length; i++) {
|
||||
toggleFieldset($('legend:contains("' + collapsedFieldsets[i] + '")'), false);
|
||||
}
|
||||
function formatViz(viz) {
|
||||
const url = `/static/assets/images/viz_thumbnails/${viz.id}.png`;
|
||||
const noImg = '/static/assets/images/noimg.png';
|
||||
return $(
|
||||
`<img class="viz-thumb-option" src="${url}" onerror="this.src='${noImg}';">` +
|
||||
`<span>${viz.text}</span>`
|
||||
);
|
||||
}
|
||||
|
||||
$('.select2').select2({
|
||||
dropdownAutoWidth: true,
|
||||
});
|
||||
$('.select2Sortable').select2({
|
||||
dropdownAutoWidth: true,
|
||||
});
|
||||
$('.select2-with-images').select2({
|
||||
dropdownAutoWidth: true,
|
||||
dropdownCssClass: 'bigdrop',
|
||||
formatResult: formatViz,
|
||||
});
|
||||
$('.select2Sortable').select2Sortable({
|
||||
bindOrder: 'sortableStop',
|
||||
});
|
||||
$('form').show();
|
||||
$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
|
||||
$('.ui-helper-hidden-accessible').remove(); // jQuery-ui 1.11+ creates a div for every tooltip
|
||||
|
||||
function addFilter(i, fieldPrefix) {
|
||||
const cp = $('#' + fieldPrefix + '0').clone();
|
||||
$(cp).appendTo('#' + getPanelClass(fieldPrefix) + ' #filters');
|
||||
$(cp).show();
|
||||
if (i !== undefined) {
|
||||
$(cp).find('#' + fieldPrefix + '_eq_0').val(px.getParam(fieldPrefix + '_eq_' + i));
|
||||
$(cp).find('#' + fieldPrefix + '_op_0').val(px.getParam(fieldPrefix + '_op_' + i));
|
||||
$(cp).find('#' + fieldPrefix + '_col_0').val(px.getParam(fieldPrefix + '_col_' + i));
|
||||
}
|
||||
$(cp).find('select').select2();
|
||||
$(cp).find('.remove').click(function () {
|
||||
$(this)
|
||||
.parent()
|
||||
.parent()
|
||||
.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function setFilters() {
|
||||
['flt', 'having'].forEach(function (prefix) {
|
||||
for (let i = 1; i < 10; i++) {
|
||||
const col = px.getParam(prefix + '_col_' + i);
|
||||
if (col !== '') {
|
||||
addFilter(i, prefix);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
setFilters();
|
||||
|
||||
$(window).bind('popstate', function () {
|
||||
// Browser back button
|
||||
const returnLocation = history.location || document.location;
|
||||
// Could do something more lightweight here, but we're not optimizing
|
||||
// for the use of the back button anyways
|
||||
returnLocation.reload();
|
||||
});
|
||||
|
||||
$('#filter_panel #plus').click(function () {
|
||||
addFilter(undefined, 'flt');
|
||||
});
|
||||
$('#having_panel #plus').click(function () {
|
||||
addFilter(undefined, 'having');
|
||||
});
|
||||
|
||||
function createChoices(term, data) {
|
||||
const filtered = $(data).filter(function () {
|
||||
return this.text.localeCompare(term) === 0;
|
||||
});
|
||||
if (filtered.length === 0) {
|
||||
return {
|
||||
id: term,
|
||||
text: term,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function initSelectionToValue(element, callback) {
|
||||
callback({
|
||||
id: element.val(),
|
||||
text: element.val(),
|
||||
});
|
||||
}
|
||||
|
||||
$('.select2_freeform').each(function () {
|
||||
const parent = $(this).parent();
|
||||
const name = $(this).attr('name');
|
||||
const l = [];
|
||||
let selected = '';
|
||||
for (let i = 0; i < this.options.length; i++) {
|
||||
l.push({
|
||||
id: this.options[i].value,
|
||||
text: this.options[i].text,
|
||||
});
|
||||
if (this.options[i].selected) {
|
||||
selected = this.options[i].value;
|
||||
}
|
||||
}
|
||||
parent.append(
|
||||
`<input class="${$(this).attr('class')}" ` +
|
||||
`name="${name}" type="text" value="${selected}">`
|
||||
);
|
||||
$(`input[name='${name}']`).select2({
|
||||
createSearchChoice: createChoices,
|
||||
initSelection: initSelectionToValue,
|
||||
dropdownAutoWidth: true,
|
||||
multiple: false,
|
||||
data: l,
|
||||
});
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
function prepSaveDialog() {
|
||||
const setButtonsState = function () {
|
||||
const addToDash = $('input[name=addToDash]:checked').val();
|
||||
if (addToDash === 'existing' || addToDash === 'new') {
|
||||
$('.gotodash').removeAttr('disabled');
|
||||
} else {
|
||||
$('.gotodash').prop('disabled', true);
|
||||
}
|
||||
};
|
||||
const url = '/dashboardmodelviewasync/api/read?_flt_0_owners=' + $('#userid').val();
|
||||
$.get(url, function (data) {
|
||||
const choices = [];
|
||||
for (let i = 0; i < data.pks.length; i++) {
|
||||
choices.push({ id: data.pks[i], text: data.result[i].dashboard_title });
|
||||
}
|
||||
$('#save_to_dashboard_id').select2({
|
||||
data: choices,
|
||||
dropdownAutoWidth: true,
|
||||
}).on('select2-selecting', function () {
|
||||
$('#addToDash_existing').prop('checked', true);
|
||||
setButtonsState();
|
||||
});
|
||||
});
|
||||
|
||||
$('input[name=addToDash]').change(setButtonsState);
|
||||
$("input[name='new_dashboard_name']").on('focus', function () {
|
||||
$('#add_to_new_dash').prop('checked', true);
|
||||
setButtonsState();
|
||||
});
|
||||
$("input[name='new_slice_name']").on('focus', function () {
|
||||
$('#save_as_new').prop('checked', true);
|
||||
setButtonsState();
|
||||
});
|
||||
|
||||
$('#btn_modal_save').on('click', () => saveSlice());
|
||||
|
||||
$('#btn_modal_save_goto_dash').click(() => {
|
||||
document.getElementById('goto_dash').value = 'true';
|
||||
saveSlice();
|
||||
});
|
||||
}
|
||||
prepSaveDialog();
|
||||
}
|
||||
|
||||
function renderExploreActions() {
|
||||
const exploreActionsEl = document.getElementById('js-explore-actions');
|
||||
ReactDOM.render(
|
||||
<ExploreActionButtons
|
||||
canDownload={exploreActionsEl.getAttribute('data-can-download')}
|
||||
slice={slice}
|
||||
/>,
|
||||
exploreActionsEl
|
||||
);
|
||||
}
|
||||
|
||||
function initComponents() {
|
||||
const queryAndSaveBtnsEl = document.getElementById('js-query-and-save-btns');
|
||||
ReactDOM.render(
|
||||
<QueryAndSaveBtns
|
||||
canAdd={queryAndSaveBtnsEl.getAttribute('data-can-add')}
|
||||
onQuery={() => query(true)}
|
||||
/>,
|
||||
queryAndSaveBtnsEl
|
||||
);
|
||||
renderExploreActions();
|
||||
}
|
||||
|
||||
let exploreController = {
|
||||
type: 'slice',
|
||||
done: (sliceObj) => {
|
||||
slice = sliceObj;
|
||||
renderExploreActions();
|
||||
const cachedSelector = $('#is_cached');
|
||||
if (slice.data !== undefined && slice.data.is_cached) {
|
||||
cachedSelector
|
||||
.attr(
|
||||
'title',
|
||||
`Served from data cached at ${slice.data.cached_dttm}. Click [Query] to force refresh`)
|
||||
.show()
|
||||
.tooltip('fixTitle');
|
||||
} else {
|
||||
cachedSelector.hide();
|
||||
}
|
||||
},
|
||||
error: (sliceObj) => {
|
||||
slice = sliceObj;
|
||||
renderExploreActions();
|
||||
},
|
||||
};
|
||||
exploreController = Object.assign({}, utils.controllerInterface, exploreController);
|
||||
|
||||
|
||||
$(document).ready(function () {
|
||||
const data = $('.slice').data('slice');
|
||||
|
||||
initExploreView();
|
||||
|
||||
slice = px.Slice(data, exploreController);
|
||||
|
||||
// call vis render method, which issues ajax
|
||||
// calls render on the slice for the first time
|
||||
query(false, false);
|
||||
|
||||
slice.bindResizeToWindowResize();
|
||||
|
||||
initComponents();
|
||||
});
|
||||
@@ -1,148 +0,0 @@
|
||||
const $ = window.$ = require('jquery');
|
||||
export const SET_DATASOURCE = 'SET_DATASOURCE';
|
||||
export const SET_TIME_COLUMN_OPTS = 'SET_TIME_COLUMN_OPTS';
|
||||
export const SET_TIME_GRAIN_OPTS = 'SET_TIME_GRAIN_OPTS';
|
||||
export const SET_GROUPBY_COLUMN_OPTS = 'SET_GROUPBY_COLUMN_OPTS';
|
||||
export const SET_METRICS_OPTS = 'SET_METRICS_OPTS';
|
||||
export const SET_COLUMN_OPTS = 'SET_COLUMN_OPTS';
|
||||
export const SET_ORDERING_OPTS = 'SET_ORDERING_OPTS';
|
||||
export const TOGGLE_SEARCHBOX = 'TOGGLE_SEARCHBOX';
|
||||
export const SET_FILTER_COLUMN_OPTS = 'SET_FILTER_COLUMN_OPTS';
|
||||
export const ADD_FILTER = 'ADD_FILTER';
|
||||
export const SET_FILTER = 'SET_FILTER';
|
||||
export const REMOVE_FILTER = 'REMOVE_FILTER';
|
||||
export const CHANGE_FILTER_FIELD = 'CHANGE_FILTER_FIELD';
|
||||
export const CHANGE_FILTER_OP = 'CHANGE_FILTER_OP';
|
||||
export const CHANGE_FILTER_VALUE = 'CHANGE_FILTER_VALUE';
|
||||
export const RESET_FORM_DATA = 'RESET_FORM_DATA';
|
||||
export const CLEAR_ALL_OPTS = 'CLEAR_ALL_OPTS';
|
||||
export const SET_DATASOURCE_TYPE = 'SET_DATASOURCE_TYPE';
|
||||
export const SET_FORM_DATA = 'SET_FORM_DATA';
|
||||
|
||||
export function setTimeColumnOpts(timeColumnOpts) {
|
||||
return { type: SET_TIME_COLUMN_OPTS, timeColumnOpts };
|
||||
}
|
||||
|
||||
export function setTimeGrainOpts(timeGrainOpts) {
|
||||
return { type: SET_TIME_GRAIN_OPTS, timeGrainOpts };
|
||||
}
|
||||
|
||||
export function setGroupByColumnOpts(groupByColumnOpts) {
|
||||
return { type: SET_GROUPBY_COLUMN_OPTS, groupByColumnOpts };
|
||||
}
|
||||
|
||||
export function setMetricsOpts(metricsOpts) {
|
||||
return { type: SET_METRICS_OPTS, metricsOpts };
|
||||
}
|
||||
|
||||
export function setColumnOpts(columnOpts) {
|
||||
return { type: SET_COLUMN_OPTS, columnOpts };
|
||||
}
|
||||
|
||||
export function setOrderingOpts(orderingOpts) {
|
||||
return { type: SET_ORDERING_OPTS, orderingOpts };
|
||||
}
|
||||
|
||||
export function setFilterColumnOpts(filterColumnOpts) {
|
||||
return { type: SET_FILTER_COLUMN_OPTS, filterColumnOpts };
|
||||
}
|
||||
|
||||
export function resetFormData() {
|
||||
// Clear all form data when switching datasource
|
||||
return { type: RESET_FORM_DATA };
|
||||
}
|
||||
|
||||
export function clearAllOpts() {
|
||||
return { type: CLEAR_ALL_OPTS };
|
||||
}
|
||||
|
||||
export function setDatasourceType(datasourceType) {
|
||||
return { type: SET_DATASOURCE_TYPE, datasourceType };
|
||||
}
|
||||
|
||||
export function setFormOpts(datasourceId, datasourceType) {
|
||||
return function (dispatch) {
|
||||
const timeColumnOpts = [];
|
||||
const groupByColumnOpts = [];
|
||||
const metricsOpts = [];
|
||||
const filterColumnOpts = [];
|
||||
const timeGrainOpts = [];
|
||||
const columnOpts = [];
|
||||
const orderingOpts = [];
|
||||
|
||||
if (datasourceId) {
|
||||
const params = [`datasource_id=${datasourceId}`, `datasource_type=${datasourceType}`];
|
||||
const url = '/caravel/fetch_datasource_metadata?' + params.join('&');
|
||||
|
||||
$.get(url, (data, status) => {
|
||||
if (status === 'success') {
|
||||
data.time_columns.forEach((d) => {
|
||||
if (d) timeColumnOpts.push({ value: d, label: d });
|
||||
});
|
||||
data.groupby_cols.forEach((d) => {
|
||||
if (d) groupByColumnOpts.push({ value: d, label: d });
|
||||
});
|
||||
data.metrics.forEach((d) => {
|
||||
if (d) metricsOpts.push({ value: d[1], label: d[0] });
|
||||
});
|
||||
data.filter_cols.forEach((d) => {
|
||||
if (d) filterColumnOpts.push({ value: d, label: d });
|
||||
});
|
||||
data.time_grains.forEach((d) => {
|
||||
if (d) timeGrainOpts.push({ value: d, label: d });
|
||||
});
|
||||
data.columns.forEach((d) => {
|
||||
if (d) columnOpts.push({ value: d, label: d });
|
||||
});
|
||||
data.ordering_cols.forEach((d) => {
|
||||
if (d) orderingOpts.push({ value: d, label: d });
|
||||
});
|
||||
|
||||
// Repopulate options for controls
|
||||
dispatch(setTimeColumnOpts(timeColumnOpts));
|
||||
dispatch(setTimeGrainOpts(timeGrainOpts));
|
||||
dispatch(setGroupByColumnOpts(groupByColumnOpts));
|
||||
dispatch(setMetricsOpts(metricsOpts));
|
||||
dispatch(setFilterColumnOpts(filterColumnOpts));
|
||||
dispatch(setColumnOpts(columnOpts));
|
||||
dispatch(setOrderingOpts(orderingOpts));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Clear all Select options
|
||||
dispatch(clearAllOpts());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function setDatasource(datasourceId) {
|
||||
return { type: SET_DATASOURCE, datasourceId };
|
||||
}
|
||||
|
||||
export function toggleSearchBox(searchBox) {
|
||||
return { type: TOGGLE_SEARCHBOX, searchBox };
|
||||
}
|
||||
|
||||
export function addFilter(filter) {
|
||||
return { type: ADD_FILTER, filter };
|
||||
}
|
||||
|
||||
export function removeFilter(filter) {
|
||||
return { type: REMOVE_FILTER, filter };
|
||||
}
|
||||
|
||||
export function changeFilterField(filter, field) {
|
||||
return { type: CHANGE_FILTER_FIELD, filter, field };
|
||||
}
|
||||
|
||||
export function changeFilterOp(filter, op) {
|
||||
return { type: CHANGE_FILTER_OP, filter, op };
|
||||
}
|
||||
|
||||
export function changeFilterValue(filter, value) {
|
||||
return { type: CHANGE_FILTER_VALUE, filter, value };
|
||||
}
|
||||
|
||||
export function setFormData(key, value) {
|
||||
return { type: SET_FORM_DATA, key, value };
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Panel } from 'react-bootstrap';
|
||||
import visMap from '../../../visualizations/main';
|
||||
|
||||
const propTypes = {
|
||||
sliceName: PropTypes.string.isRequired,
|
||||
vizType: PropTypes.string.isRequired,
|
||||
height: PropTypes.string.isRequired,
|
||||
sliceContainerId: PropTypes.string.isRequired,
|
||||
jsonEndpoint: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
class ChartContainer extends React.Component {
|
||||
componentDidMount() {
|
||||
this.renderVis();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.renderVis();
|
||||
}
|
||||
|
||||
getMockedSliceObject() {
|
||||
return {
|
||||
jsonEndpoint: () => this.props.jsonEndpoint,
|
||||
|
||||
container: {
|
||||
html: () => {
|
||||
// this should be a callback to clear the contents of the slice container
|
||||
},
|
||||
|
||||
css: () => {
|
||||
// dimension can be 'height'
|
||||
// pixel string can be '300px'
|
||||
// should call callback to adjust height of chart
|
||||
},
|
||||
},
|
||||
|
||||
width: () => this.chartContainerRef.getBoundingClientRect().width,
|
||||
|
||||
height: () => parseInt(this.props.height, 10) - 100,
|
||||
|
||||
selector: `#${this.props.sliceContainerId}`,
|
||||
|
||||
done: () => {
|
||||
// finished rendering callback
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
renderVis() {
|
||||
const slice = this.getMockedSliceObject();
|
||||
visMap[this.props.vizType](slice).render();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="chart-container">
|
||||
<Panel
|
||||
style={{ height: this.props.height }}
|
||||
header={
|
||||
<div className="panel-title">{this.props.sliceName}</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
id={this.props.sliceContainerId}
|
||||
ref={(ref) => { this.chartContainerRef = ref; }}
|
||||
/>
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChartContainer.propTypes = propTypes;
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
sliceName: state.sliceName,
|
||||
vizType: state.viz.formData.vizType,
|
||||
sliceContainerId: `slice-container-${state.viz.formData.sliceId}`,
|
||||
jsonEndpoint: state.viz.jsonEndPoint,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChartContainer);
|
||||
@@ -1,89 +0,0 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as actions from '../actions/exploreActions';
|
||||
import { connect } from 'react-redux';
|
||||
import { VIZ_TYPES } from '../constants';
|
||||
|
||||
const propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
datasources: React.PropTypes.array,
|
||||
datasourceId: React.PropTypes.number,
|
||||
datasourceType: React.PropTypes.string,
|
||||
vizType: React.PropTypes.string,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
datasources: [],
|
||||
datasourceId: null,
|
||||
datasourceType: null,
|
||||
vizType: null,
|
||||
};
|
||||
|
||||
class ChartControl extends React.Component {
|
||||
componentWillMount() {
|
||||
if (this.props.datasourceId) {
|
||||
this.props.actions.setFormOpts(this.props.datasourceId, this.props.datasourceType);
|
||||
}
|
||||
}
|
||||
changeDatasource(datasourceOpt) {
|
||||
const val = (datasourceOpt) ? datasourceOpt.value : null;
|
||||
this.props.actions.setDatasource(val);
|
||||
this.props.actions.resetFormData();
|
||||
this.props.actions.setFormOpts(val, this.props.datasourceType);
|
||||
}
|
||||
changeViz(opt) {
|
||||
const val = opt ? opt.value : null;
|
||||
this.props.actions.setFormData('vizType', val);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className="panel">
|
||||
<div className="panel-header">Chart Options</div>
|
||||
<div className="panel-body">
|
||||
<h5 className="section-heading">Datasource</h5>
|
||||
<div className="row">
|
||||
<Select
|
||||
name="select-datasource"
|
||||
placeholder="Select a datasource"
|
||||
options={this.props.datasources.map((d) => ({ value: d[0], label: d[1] }))}
|
||||
value={this.props.datasourceId}
|
||||
autosize={false}
|
||||
onChange={this.changeDatasource.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<h5 className="section-heading">Viz Type</h5>
|
||||
<div className="row">
|
||||
<Select
|
||||
name="select-viztype"
|
||||
placeholder="Select a viz type"
|
||||
options={VIZ_TYPES}
|
||||
value={this.props.vizType}
|
||||
autosize={false}
|
||||
onChange={this.changeViz.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChartControl.propTypes = propTypes;
|
||||
ChartControl.defaultProps = defaultProps;
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
datasources: state.datasources,
|
||||
datasourceId: state.datasourceId,
|
||||
datasourceType: state.datasourceType,
|
||||
vizType: state.viz.formData.vizType,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch),
|
||||
};
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChartControl);
|
||||
@@ -1,40 +0,0 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Panel } from 'react-bootstrap';
|
||||
import { DefaultControls, VIZ_CONTROL_MAPPING } from '../constants';
|
||||
|
||||
const propTypes = {
|
||||
vizType: React.PropTypes.string,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
vizType: null,
|
||||
};
|
||||
|
||||
function ControlPanelsContainer(props) {
|
||||
return (
|
||||
<Panel>
|
||||
<div className="scrollbar-container">
|
||||
<div className="scrollbar-content">
|
||||
{DefaultControls}
|
||||
{VIZ_CONTROL_MAPPING[props.vizType]}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
ControlPanelsContainer.propTypes = propTypes;
|
||||
ControlPanelsContainer.defaultProps = defaultProps;
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
vizType: state.viz.formData.vizType,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ControlPanelsContainer);
|
||||
@@ -1,46 +0,0 @@
|
||||
import React from 'react';
|
||||
import ChartContainer from './ChartContainer';
|
||||
import ControlPanelsContainer from './ControlPanelsContainer';
|
||||
import QueryAndSaveBtns from '../../explore/components/QueryAndSaveBtns';
|
||||
|
||||
export default class ExploreViewContainer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
height: this.getHeight(),
|
||||
};
|
||||
}
|
||||
|
||||
getHeight() {
|
||||
const navHeight = 90;
|
||||
return `${window.innerHeight - navHeight}px`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className="container-fluid"
|
||||
style={{
|
||||
height: this.state.height,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div className="row">
|
||||
<div className="col-sm-4">
|
||||
<QueryAndSaveBtns
|
||||
canAdd="True"
|
||||
onQuery={() => {}}
|
||||
/>
|
||||
<br /><br />
|
||||
<ControlPanelsContainer />
|
||||
</div>
|
||||
<div className="col-sm-8">
|
||||
<ChartContainer
|
||||
height={this.state.height}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import React from 'react';
|
||||
// import { Tab, Row, Col, Nav, NavItem } from 'react-bootstrap';
|
||||
import Select from 'react-select';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as actions from '../actions/exploreActions';
|
||||
import shortid from 'shortid';
|
||||
|
||||
const propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
filterColumnOpts: React.PropTypes.array,
|
||||
filters: React.PropTypes.array,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
filterColumnOpts: [],
|
||||
filters: [],
|
||||
};
|
||||
|
||||
class Filters extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
opOpts: ['in', 'not in'],
|
||||
};
|
||||
}
|
||||
changeField(filter, fieldOpt) {
|
||||
const val = (fieldOpt) ? fieldOpt.value : null;
|
||||
this.props.actions.changeFilterField(filter, val);
|
||||
}
|
||||
changeOp(filter, opOpt) {
|
||||
const val = (opOpt) ? opOpt.value : null;
|
||||
this.props.actions.changeFilterOp(filter, val);
|
||||
}
|
||||
changeValue(filter, value) {
|
||||
this.props.actions.changeFilterValue(filter, value);
|
||||
}
|
||||
removeFilter(filter) {
|
||||
this.props.actions.removeFilter(filter);
|
||||
}
|
||||
addFilter() {
|
||||
this.props.actions.addFilter({
|
||||
id: shortid.generate(),
|
||||
field: null,
|
||||
op: null,
|
||||
value: null,
|
||||
});
|
||||
}
|
||||
render() {
|
||||
const filters = this.props.filters.map((filter) => (
|
||||
<div>
|
||||
<Select
|
||||
className="row"
|
||||
multi={false}
|
||||
name="select-column"
|
||||
placeholder="Select column"
|
||||
options={this.props.filterColumnOpts}
|
||||
value={filter.field}
|
||||
autosize={false}
|
||||
onChange={this.changeField.bind(this, filter)}
|
||||
/>
|
||||
<div className="row">
|
||||
<Select
|
||||
className="col-sm-3"
|
||||
multi={false}
|
||||
name="select-op"
|
||||
placeholder="Select operator"
|
||||
options={this.state.opOpts.map((o) => ({ value: o, label: o }))}
|
||||
value={filter.op}
|
||||
autosize={false}
|
||||
onChange={this.changeOp.bind(this, filter)}
|
||||
/>
|
||||
<div className="col-sm-6">
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.changeValue.bind(this, filter)}
|
||||
className="form-control input-sm"
|
||||
placeholder="Filter value"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-3">
|
||||
<Button
|
||||
bsStyle="primary"
|
||||
onClick={this.removeFilter.bind(this, filter)}
|
||||
>
|
||||
<i className="fa fa-minus" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
return (
|
||||
<div className="panel space-1">
|
||||
<div className="panel-header">Filters</div>
|
||||
<div className="panel-body">
|
||||
{filters}
|
||||
<Button
|
||||
bsStyle="primary"
|
||||
onClick={this.addFilter.bind(this)}
|
||||
>
|
||||
<i className="fa fa-plus" />Add Filter
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Filters.propTypes = propTypes;
|
||||
Filters.defaultProps = defaultProps;
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
filterColumnOpts: state.filterColumnOpts,
|
||||
filters: state.filters,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Filters);
|
||||
@@ -1,68 +0,0 @@
|
||||
import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as actions from '../actions/exploreActions';
|
||||
import { connect } from 'react-redux';
|
||||
import SelectArray from './SelectArray';
|
||||
|
||||
const propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
metricsOpts: React.PropTypes.array,
|
||||
metrics: React.PropTypes.array,
|
||||
groupByColumnOpts: React.PropTypes.array,
|
||||
groupByColumns: React.PropTypes.array,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
metricsOpts: [],
|
||||
metrics: [],
|
||||
groupByColumnOpts: [],
|
||||
groupByColumns: [],
|
||||
};
|
||||
|
||||
const GroupBy = (props) => {
|
||||
const selects = [
|
||||
{
|
||||
key: 'groupByColumns',
|
||||
title: 'Group By',
|
||||
options: props.groupByColumnOpts,
|
||||
value: props.groupByColumns,
|
||||
multi: true,
|
||||
width: '12',
|
||||
},
|
||||
{
|
||||
key: 'metrics',
|
||||
title: 'Metrics',
|
||||
options: props.metricsOpts,
|
||||
value: props.metrics,
|
||||
multi: true,
|
||||
width: '12',
|
||||
}];
|
||||
return (
|
||||
<div className="panel">
|
||||
<div className="panel-header">GroupBy</div>
|
||||
<div className="panel-body">
|
||||
<SelectArray selectArray={selects} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
GroupBy.propTypes = propTypes;
|
||||
GroupBy.defaultProps = defaultProps;
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
metricsOpts: state.metricsOpts,
|
||||
metrics: state.viz.formData.metrics,
|
||||
groupByColumnOpts: state.groupByColumnOpts,
|
||||
groupByColumns: state.viz.formData.groupByColumns,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(GroupBy);
|
||||
@@ -1,68 +0,0 @@
|
||||
import React from 'react';
|
||||
import SelectArray from './SelectArray';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as actions from '../actions/exploreActions';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
columnOpts: React.PropTypes.array,
|
||||
columns: React.PropTypes.array,
|
||||
orderingOpts: React.PropTypes.array,
|
||||
orderings: React.PropTypes.array,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
columnOpts: [],
|
||||
columns: [],
|
||||
orderingOpts: [],
|
||||
orderings: [],
|
||||
};
|
||||
|
||||
const NotGroupBy = (props) => {
|
||||
const selects = [
|
||||
{
|
||||
key: 'columns',
|
||||
title: 'Columns',
|
||||
options: props.columnOpts,
|
||||
value: props.columns,
|
||||
multi: true,
|
||||
width: '12',
|
||||
},
|
||||
{
|
||||
key: 'orderings',
|
||||
title: 'Orderings',
|
||||
options: props.orderingOpts,
|
||||
value: props.orderings,
|
||||
multi: true,
|
||||
width: '12',
|
||||
}];
|
||||
return (
|
||||
<div className="panel">
|
||||
<div className="panel-header">Not GroupBy</div>
|
||||
<div className="panel-body">
|
||||
<SelectArray selectArray={selects} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NotGroupBy.propTypes = propTypes;
|
||||
NotGroupBy.defaultProps = defaultProps;
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
columnOpts: state.columnOpts,
|
||||
columns: state.viz.formData.columns,
|
||||
orderingOpts: state.orderingOpts,
|
||||
orderings: state.viz.formData.orderings,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(NotGroupBy);
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from 'react';
|
||||
import SelectArray from './SelectArray';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as actions from '../actions/exploreActions';
|
||||
import { connect } from 'react-redux';
|
||||
import { timestampOptions, rowLimitOptions } from '../constants';
|
||||
|
||||
const propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
timeStampFormat: React.PropTypes.string,
|
||||
rowLimit: React.PropTypes.number,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
timeStampFormat: null,
|
||||
rowLimit: null,
|
||||
};
|
||||
|
||||
const Options = (props) => {
|
||||
const selects = [
|
||||
{
|
||||
key: 'timeStampFormat',
|
||||
title: 'Timestamp Format',
|
||||
options: timestampOptions.map((t) => ({ value: t[0], label: t[1] })),
|
||||
value: props.timeStampFormat,
|
||||
width: '12',
|
||||
},
|
||||
{
|
||||
key: 'rowLimit',
|
||||
title: 'Row Limit',
|
||||
options: rowLimitOptions.map((r) => ({ value: r, label: r })),
|
||||
value: props.rowLimit,
|
||||
width: '12',
|
||||
}];
|
||||
return (
|
||||
<div className="panel">
|
||||
<div className="panel-header">Options</div>
|
||||
<div className="panel-body">
|
||||
<SelectArray selectArray={selects} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Options.propTypes = propTypes;
|
||||
Options.defaultProps = defaultProps;
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
timeStampFormat: state.viz.formData.timeStampFormat,
|
||||
rowLimit: state.viz.formData.rowLimit,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Options);
|
||||
@@ -1,74 +0,0 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as actions from '../actions/exploreActions';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
selectArray: React.PropTypes.arrayOf(
|
||||
React.PropTypes.shape({
|
||||
key: React.PropTypes.string.isRequired,
|
||||
title: React.PropTypes.string.isRequired,
|
||||
options: React.PropTypes.array.isRequired,
|
||||
value: React.PropTypes.oneOfType([
|
||||
React.PropTypes.string,
|
||||
React.PropTypes.array,
|
||||
]),
|
||||
width: React.PropTypes.string,
|
||||
multi: React.PropTypes.bool,
|
||||
})
|
||||
).isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
selectArray: [],
|
||||
};
|
||||
|
||||
class SelectArray extends React.Component {
|
||||
changeSelectData(key, multi, opt) {
|
||||
if (multi) this.props.actions.setFormData(key, opt);
|
||||
else {
|
||||
const val = opt ? opt.value : null;
|
||||
this.props.actions.setFormData(key, val);
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const selects = this.props.selectArray.map((obj) => (
|
||||
<div
|
||||
className={(obj.width) ? `col-sm-${obj.width}` : 'col-sm-6'}
|
||||
key={obj.key}
|
||||
>
|
||||
<h5 className="section-heading">{obj.title}</h5>
|
||||
<Select
|
||||
multi={obj.multi}
|
||||
name={`select-${obj.key}`}
|
||||
options={obj.options}
|
||||
value={obj.value}
|
||||
autosize={false}
|
||||
onChange={this.changeSelectData.bind(this, obj.key, obj.multi)}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div>
|
||||
{selects}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectArray.propTypes = propTypes;
|
||||
SelectArray.defaultProps = defaultProps;
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SelectArray);
|
||||
@@ -1,55 +0,0 @@
|
||||
import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as actions from '../actions/exploreActions';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
class SqlClause extends React.Component {
|
||||
onChange(key, event) {
|
||||
this.props.actions.setFormData(key, event.target.value);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className="panel">
|
||||
<div className="panel-header">SQL</div>
|
||||
<div className="panel-body">
|
||||
<div className="row">
|
||||
<h5 className="section-heading">Where</h5>
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.onChange.bind(this, 'where')}
|
||||
className="form-control input-sm"
|
||||
placeholder="Where Clause"
|
||||
/>
|
||||
</div>
|
||||
<div className="row">
|
||||
<h5 className="section-heading">Having</h5>
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.onChange.bind(this, 'having')}
|
||||
className="form-control input-sm"
|
||||
placeholder="Having Clause"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SqlClause.propTypes = propTypes;
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SqlClause);
|
||||
@@ -1,88 +0,0 @@
|
||||
import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as actions from '../actions/exploreActions';
|
||||
import { connect } from 'react-redux';
|
||||
import { sinceOptions, untilOptions } from '../constants';
|
||||
import SelectArray from './SelectArray';
|
||||
|
||||
const propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
datasourceType: React.PropTypes.string,
|
||||
timeColumnOpts: React.PropTypes.array,
|
||||
timeColumn: React.PropTypes.string,
|
||||
timeGrainOpts: React.PropTypes.array,
|
||||
timeGrain: React.PropTypes.string,
|
||||
since: React.PropTypes.string,
|
||||
until: React.PropTypes.string,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
timeColumnOpts: [],
|
||||
timeColumn: null,
|
||||
timeGrainOpts: [],
|
||||
timeGrain: null,
|
||||
since: null,
|
||||
until: null,
|
||||
};
|
||||
|
||||
const TimeFilter = (props) => {
|
||||
const isDatasourceTypeTable = props.datasourceType === 'table';
|
||||
const timeColumnTitle = isDatasourceTypeTable ? 'Time Column' : 'Time Granularity';
|
||||
const timeGrainTitle = isDatasourceTypeTable ? 'Time Grain' : 'Origin';
|
||||
const selects = [
|
||||
{
|
||||
key: 'timeColumn',
|
||||
title: timeColumnTitle,
|
||||
options: props.timeColumnOpts,
|
||||
value: props.timeColumn,
|
||||
},
|
||||
{
|
||||
key: 'timeGrain',
|
||||
title: timeGrainTitle,
|
||||
options: props.timeGrainOpts,
|
||||
value: props.timeGrain,
|
||||
},
|
||||
{
|
||||
key: 'since',
|
||||
title: 'Since',
|
||||
options: sinceOptions.map((s) => ({ value: s, label: s })),
|
||||
value: props.since,
|
||||
},
|
||||
{
|
||||
key: 'until',
|
||||
title: 'Until',
|
||||
options: untilOptions.map((u) => ({ value: u, label: u })),
|
||||
value: props.until,
|
||||
}];
|
||||
return (
|
||||
<div className="panel">
|
||||
<div className="panel-header">Time Filter</div>
|
||||
<div className="panel-body">
|
||||
<SelectArray selectArray={selects} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TimeFilter.propTypes = propTypes;
|
||||
TimeFilter.defaultProps = defaultProps;
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
datasourceType: state.datasourceType,
|
||||
timeColumnOpts: state.timeColumnOpts,
|
||||
timeColumn: state.viz.formData.timeColumn,
|
||||
timeGrainOpts: state.timeGrainOpts,
|
||||
timeGrain: state.viz.formData.timeGrain,
|
||||
since: state.viz.formData.since,
|
||||
until: state.viz.formData.until,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TimeFilter);
|
||||
@@ -1,76 +0,0 @@
|
||||
import React from 'react';
|
||||
import TimeFilter from './components/TimeFilter';
|
||||
import ChartControl from './components/ChartControl';
|
||||
import GroupBy from './components/GroupBy';
|
||||
import SqlClause from './components/SqlClause';
|
||||
import Filters from './components/Filters';
|
||||
import NotGroupBy from './components/NotGroupBy';
|
||||
import Options from './components/Options';
|
||||
|
||||
export const VIZ_TYPES = [
|
||||
{ value: 'dist_bar', label: 'Distribution - Bar Chart', requiresTime: false },
|
||||
{ value: 'pie', label: 'Pie Chart', requiresTime: false },
|
||||
{ value: 'line', label: 'Time Series - Line Chart', requiresTime: true },
|
||||
{ value: 'bar', label: 'Time Series - Bar Chart', requiresTime: true },
|
||||
{ value: 'compare', label: 'Time Series - Percent Change', requiresTime: true },
|
||||
{ value: 'area', label: 'Time Series - Stacked', requiresTime: true },
|
||||
{ value: 'table', label: 'Table View', requiresTime: false },
|
||||
{ value: 'markup', label: 'Markup', requiresTime: false },
|
||||
{ value: 'pivot_table', label: 'Pivot Table', requiresTime: false },
|
||||
{ value: 'separator', label: 'Separator', requiresTime: false },
|
||||
{ value: 'word_cloud', label: 'Word Cloud', requiresTime: false },
|
||||
{ value: 'treemap', label: 'Treemap', requiresTime: false },
|
||||
{ value: 'cal_heatmap', label: 'Calendar Heatmap', requiresTime: true },
|
||||
{ value: 'box_plot', label: 'Box Plot', requiresTime: false },
|
||||
{ value: 'bubble', label: 'Bubble Chart', requiresTime: false },
|
||||
{ value: 'big_number', label: 'Big Number with Trendline', requiresTime: false },
|
||||
{ value: 'bubble', label: 'Bubble Chart', requiresTime: false },
|
||||
{ value: 'histogram', label: 'Histogram', requiresTime: false },
|
||||
{ value: 'sunburst', label: 'Sunburst', requiresTime: false },
|
||||
{ value: 'sankey', label: 'Sankey', requiresTime: false },
|
||||
{ value: 'directed_force', label: 'Directed Force Layout', requiresTime: false },
|
||||
{ value: 'world_map', label: 'World Map', requiresTime: false },
|
||||
{ value: 'filter_box', label: 'Filter Box', requiresTime: false },
|
||||
{ value: 'iframe', label: 'iFrame', requiresTime: false },
|
||||
{ value: 'para', label: 'Parallel Coordinates', requiresTime: false },
|
||||
{ value: 'heatmap', label: 'Heatmap', requiresTime: false },
|
||||
{ value: 'horizon', label: 'Horizon', requiresTime: false },
|
||||
{ value: 'mapbox', label: 'Mapbox', requiresTime: false },
|
||||
];
|
||||
|
||||
export const sinceOptions = ['1 hour ago', '12 hours ago', '1 day ago',
|
||||
'7 days ago', '28 days ago', '90 days ago', '1 year ago'];
|
||||
export const untilOptions = ['now', '1 day ago', '7 days ago',
|
||||
'28 days ago', '90 days ago', '1 year ago'];
|
||||
|
||||
export const timestampOptions = [
|
||||
['smart_date', 'Adaptative formating'],
|
||||
['%m/%d/%Y', '"%m/%d/%Y" | 01/14/2019'],
|
||||
['%Y-%m-%d', '"%Y-%m-%d" | 2019-01-14'],
|
||||
['%Y-%m-%d %H:%M:%S',
|
||||
'"%Y-%m-%d %H:%M:%S" | 2019-01-14 01:32:10'],
|
||||
['%H:%M:%S', '"%H:%M:%S" | 01:32:10'],
|
||||
];
|
||||
|
||||
export const rowLimitOptions = [10, 50, 100, 250, 500, 1000, 5000, 10000, 50000];
|
||||
|
||||
export const DefaultControls = (
|
||||
<div>
|
||||
<ChartControl />
|
||||
<TimeFilter />
|
||||
<GroupBy />
|
||||
<SqlClause />
|
||||
<Filters />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const TableVizControls = (
|
||||
<div>
|
||||
<NotGroupBy />
|
||||
<Options />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const VIZ_CONTROL_MAPPING = {
|
||||
table: TableVizControls,
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import ExploreViewContainer from './components/ExploreViewContainer';
|
||||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import { initialState } from './stores/store';
|
||||
|
||||
const exploreViewContainer = document.getElementById('js-explore-view-container');
|
||||
const bootstrapData = JSON.parse(exploreViewContainer.getAttribute('data-bootstrap'));
|
||||
|
||||
import { exploreReducer } from './reducers/exploreReducer';
|
||||
|
||||
const bootstrappedState = Object.assign(initialState, {
|
||||
datasources: bootstrapData.datasources,
|
||||
datasourceId: parseInt(bootstrapData.datasource_id, 10),
|
||||
datasourceType: bootstrapData.datasource_type,
|
||||
sliceName: bootstrapData.viz.form_data.slice_name,
|
||||
viz: {
|
||||
jsonEndPoint: bootstrapData.viz.json_endpoint,
|
||||
data: bootstrapData.viz.data,
|
||||
formData: {
|
||||
sliceId: bootstrapData.viz.form_data.slice_id,
|
||||
vizType: bootstrapData.viz.form_data.viz_type,
|
||||
timeColumn: bootstrapData.viz.form_data.granularity_sqla,
|
||||
timeGrain: bootstrapData.viz.form_data.time_grain_sqla,
|
||||
metrics: [bootstrapData.viz.form_data.metrics].map((m) => ({ value: m, label: m })),
|
||||
since: bootstrapData.viz.form_data.since,
|
||||
until: bootstrapData.viz.form_data.until,
|
||||
having: bootstrapData.viz.form_data.having,
|
||||
where: bootstrapData.viz.form_data.where,
|
||||
rowLimit: bootstrapData.viz.form_data.row_limit,
|
||||
timeStampFormat: bootstrapData.viz.form_data.table_timestamp_format,
|
||||
},
|
||||
},
|
||||
});
|
||||
const store = createStore(exploreReducer, bootstrappedState,
|
||||
compose(applyMiddleware(thunk))
|
||||
);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<ExploreViewContainer />
|
||||
</Provider>,
|
||||
exploreViewContainer
|
||||
);
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import { defaultFormData, defaultOpts } from '../stores/store';
|
||||
import * as actions from '../actions/exploreActions';
|
||||
import { addToArr, removeFromArr, alterInArr } from '../../../utils/reducerUtils';
|
||||
|
||||
const setFormInViz = function (state, action) {
|
||||
const newFormData = Object.assign({}, state);
|
||||
newFormData[action.key] = action.value;
|
||||
return newFormData;
|
||||
};
|
||||
|
||||
const setVizInState = function (state, action) {
|
||||
switch (action.type) {
|
||||
case actions.SET_FORM_DATA:
|
||||
return Object.assign(
|
||||
{},
|
||||
state,
|
||||
{ formData: setFormInViz(state.formData, action) }
|
||||
);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const exploreReducer = function (state, action) {
|
||||
const actionHandlers = {
|
||||
[actions.SET_DATASOURCE]() {
|
||||
return Object.assign({}, state, { datasourceId: action.datasourceId });
|
||||
},
|
||||
[actions.SET_TIME_COLUMN_OPTS]() {
|
||||
return Object.assign({}, state, { timeColumnOpts: action.timeColumnOpts });
|
||||
},
|
||||
[actions.SET_TIME_GRAIN_OPTS]() {
|
||||
return Object.assign({}, state, { timeGrainOpts: action.timeGrainOpts });
|
||||
},
|
||||
[actions.SET_GROUPBY_COLUMN_OPTS]() {
|
||||
return Object.assign({}, state, { groupByColumnOpts: action.groupByColumnOpts });
|
||||
},
|
||||
[actions.SET_METRICS_OPTS]() {
|
||||
return Object.assign({}, state, { metricsOpts: action.metricsOpts });
|
||||
},
|
||||
[actions.SET_COLUMN_OPTS]() {
|
||||
return Object.assign({}, state, { columnOpts: action.columnOpts });
|
||||
},
|
||||
[actions.SET_ORDERING_OPTS]() {
|
||||
return Object.assign({}, state, { orderingOpts: action.orderingOpts });
|
||||
},
|
||||
[actions.TOGGLE_SEARCHBOX]() {
|
||||
return Object.assign({}, state, { searchBox: action.searchBox });
|
||||
},
|
||||
[actions.SET_FILTER_COLUMN_OPTS]() {
|
||||
return Object.assign({}, state, { filterColumnOpts: action.filterColumnOpts });
|
||||
},
|
||||
[actions.ADD_FILTER]() {
|
||||
return addToArr(state, 'filters', action.filter);
|
||||
},
|
||||
[actions.REMOVE_FILTER]() {
|
||||
return removeFromArr(state, 'filters', action.filter);
|
||||
},
|
||||
[actions.CHANGE_FILTER_FIELD]() {
|
||||
return alterInArr(state, 'filters', action.filter, { field: action.field });
|
||||
},
|
||||
[actions.CHANGE_FILTER_OP]() {
|
||||
return alterInArr(state, 'filters', action.filter, { op: action.op });
|
||||
},
|
||||
[actions.CHANGE_FILTER_VALUE]() {
|
||||
return alterInArr(state, 'filters', action.filter, { value: action.value });
|
||||
},
|
||||
[actions.RESET_FORM_DATA]() {
|
||||
return Object.assign({}, state, defaultFormData);
|
||||
},
|
||||
[actions.CLEAR_ALL_OPTS]() {
|
||||
return Object.assign({}, state, defaultOpts);
|
||||
},
|
||||
[actions.SET_DATASOURCE_TYPE]() {
|
||||
return Object.assign({}, state, { datasourceType: action.datasourceType });
|
||||
},
|
||||
[actions.SET_FORM_DATA]() {
|
||||
return Object.assign(
|
||||
{},
|
||||
state,
|
||||
{ viz: setVizInState(state.viz, action) }
|
||||
);
|
||||
},
|
||||
};
|
||||
if (action.type in actionHandlers) {
|
||||
return actionHandlers[action.type]();
|
||||
}
|
||||
return state;
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
// TODO: add datasource_type here after druid support is added
|
||||
export const defaultFormData = {
|
||||
sliceId: null,
|
||||
vizType: null,
|
||||
timeColumn: null,
|
||||
timeGrain: null,
|
||||
groupByColumns: [],
|
||||
metrics: [],
|
||||
since: null,
|
||||
until: null,
|
||||
having: null,
|
||||
where: null,
|
||||
columns: [],
|
||||
orderings: [],
|
||||
timeStampFormat: 'smart_date',
|
||||
rowLimit: 50000,
|
||||
searchBox: false,
|
||||
whereClause: '',
|
||||
havingClause: '',
|
||||
filters: [],
|
||||
};
|
||||
|
||||
export const initialState = {
|
||||
datasources: null,
|
||||
datasourceId: null,
|
||||
datasourceType: null,
|
||||
timeColumnOpts: [],
|
||||
timeGrainOpts: [],
|
||||
timeGrain: null,
|
||||
groupByColumnOpts: [],
|
||||
metricsOpts: [],
|
||||
columnOpts: [],
|
||||
orderingOpts: [],
|
||||
searchBox: false,
|
||||
whereClause: '',
|
||||
havingClause: '',
|
||||
filters: [],
|
||||
filterColumnOpts: [],
|
||||
viz: {
|
||||
formData: defaultFormData,
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultOpts = {
|
||||
timeColumnOpts: [],
|
||||
timeGrainOpts: [],
|
||||
groupByColumnOpts: [],
|
||||
metricsOpts: [],
|
||||
filterColumnOpts: [],
|
||||
columnOpts: [],
|
||||
orderingOpts: [],
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
const $ = window.$ = require('jquery');
|
||||
/* eslint no-unused-vars: 0 */
|
||||
const jQuery = window.jQuery = $;
|
||||
const px = require('./modules/caravel.js');
|
||||
const utils = require('./modules/utils.js');
|
||||
|
||||
require('bootstrap');
|
||||
|
||||
const standaloneController = Object.assign(
|
||||
{}, utils.controllerInterface, { type: 'standalone' });
|
||||
|
||||
$(document).ready(function () {
|
||||
const data = $('.slice').data('slice');
|
||||
const slice = px.Slice(data, standaloneController);
|
||||
slice.render();
|
||||
slice.bindResizeToWindowResize();
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
import { it, describe } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import shortid from 'shortid';
|
||||
import * as actions from '../../../../javascripts/explorev2/actions/exploreActions';
|
||||
import { initialState } from '../../../../javascripts/explorev2/stores/store';
|
||||
import { exploreReducer } from '../../../../javascripts/explorev2/reducers/exploreReducer';
|
||||
|
||||
describe('reducers', () => {
|
||||
it('should return new state with datasource id', () => {
|
||||
const newState = exploreReducer(initialState, actions.setDatasource(1));
|
||||
expect(newState.datasourceId).to.equal(1);
|
||||
});
|
||||
|
||||
it('should return new state with search box toggled', () => {
|
||||
const newState = exploreReducer(initialState, actions.toggleSearchBox(true));
|
||||
expect(newState.searchBox).to.equal(true);
|
||||
});
|
||||
|
||||
it('should return new state with added filter', () => {
|
||||
const newFilter = {
|
||||
id: shortid.generate(),
|
||||
eq: 'value',
|
||||
op: 'in',
|
||||
col: 'vals',
|
||||
};
|
||||
const newState = exploreReducer(initialState, actions.addFilter(newFilter));
|
||||
expect(newState.filters).to.deep.equal([newFilter]);
|
||||
});
|
||||
|
||||
it('should return new state with removed filter', () => {
|
||||
const filter1 = {
|
||||
id: shortid.generate(),
|
||||
eq: 'value',
|
||||
op: 'in',
|
||||
col: 'vals1',
|
||||
};
|
||||
const filter2 = {
|
||||
id: shortid.generate(),
|
||||
eq: 'value',
|
||||
op: 'not in',
|
||||
col: 'vals2',
|
||||
};
|
||||
const testState = {
|
||||
initialState,
|
||||
filters: [filter1, filter2],
|
||||
};
|
||||
const newState = exploreReducer(testState, actions.removeFilter(filter1));
|
||||
expect(newState.filters).to.deep.equal([filter2]);
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import QuerySearch from '../../../javascripts/SqlLab/components/QuerySearch';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
describe('QuerySearch', () => {
|
||||
it('should render', () => {
|
||||
expect(
|
||||
React.isValidElement(<QuerySearch />)
|
||||
).to.equal(true);
|
||||
});
|
||||
|
||||
it('should have two Select', () => {
|
||||
const wrapper = shallow(<QuerySearch />);
|
||||
expect(wrapper.find(Select)).to.have.length(2);
|
||||
});
|
||||
|
||||
it('should have one input for searchText', () => {
|
||||
const wrapper = shallow(<QuerySearch />);
|
||||
expect(wrapper.find('input')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should have one Button', () => {
|
||||
const wrapper = shallow(<QuerySearch />);
|
||||
expect(wrapper.find(Button)).to.have.length(1);
|
||||
});
|
||||
});
|
||||
@@ -1,158 +0,0 @@
|
||||
import React from 'react';
|
||||
import Link from '../../../javascripts/SqlLab/components/Link';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { TableElement } from '../../../javascripts/SqlLab/components/TableElement';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
|
||||
describe('TableElement', () => {
|
||||
|
||||
const mockedProps = {
|
||||
'table': {
|
||||
"dbId": 1,
|
||||
"queryEditorId": "rJ-KP47a",
|
||||
"schema": "caravel",
|
||||
"name": "ab_user",
|
||||
"id": "r11Vgt60",
|
||||
"indexes": [
|
||||
{
|
||||
"unique": true,
|
||||
"column_names": [
|
||||
"username"
|
||||
],
|
||||
"type": "UNIQUE",
|
||||
"name": "username"
|
||||
},
|
||||
{
|
||||
"unique": true,
|
||||
"column_names": [
|
||||
"email"
|
||||
],
|
||||
"type": "UNIQUE",
|
||||
"name": "email"
|
||||
},
|
||||
{
|
||||
"unique": false,
|
||||
"column_names": [
|
||||
"created_by_fk"
|
||||
],
|
||||
"name": "created_by_fk"
|
||||
},
|
||||
{
|
||||
"unique": false,
|
||||
"column_names": [
|
||||
"changed_by_fk"
|
||||
],
|
||||
"name": "changed_by_fk"
|
||||
}
|
||||
],
|
||||
"columns": [
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "INTEGER(11)",
|
||||
"type": "INTEGER",
|
||||
"name": "id"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "VARCHAR(64)",
|
||||
"type": "VARCHAR",
|
||||
"name": "first_name"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "VARCHAR(64)",
|
||||
"type": "VARCHAR",
|
||||
"name": "last_name"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"longType": "VARCHAR(64)",
|
||||
"type": "VARCHAR",
|
||||
"name": "username"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "VARCHAR(256)",
|
||||
"type": "VARCHAR",
|
||||
"name": "password"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "TINYINT(1)",
|
||||
"type": "TINYINT",
|
||||
"name": "active"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"longType": "VARCHAR(64)",
|
||||
"type": "VARCHAR",
|
||||
"name": "email"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "DATETIME",
|
||||
"type": "DATETIME",
|
||||
"name": "last_login"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "INTEGER(11)",
|
||||
"type": "INTEGER",
|
||||
"name": "login_count"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "INTEGER(11)",
|
||||
"type": "INTEGER",
|
||||
"name": "fail_login_count"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "DATETIME",
|
||||
"type": "DATETIME",
|
||||
"name": "created_on"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"longType": "DATETIME",
|
||||
"type": "DATETIME",
|
||||
"name": "changed_on"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"longType": "INTEGER(11)",
|
||||
"type": "INTEGER",
|
||||
"name": "created_by_fk"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"longType": "INTEGER(11)",
|
||||
"type": "INTEGER",
|
||||
"name": "changed_by_fk"
|
||||
}
|
||||
],
|
||||
"expanded": true
|
||||
}
|
||||
}
|
||||
it('should just render', () => {
|
||||
expect(
|
||||
React.isValidElement(<TableElement />)
|
||||
).to.equal(true);
|
||||
});
|
||||
it('should render with props', () => {
|
||||
expect(
|
||||
React.isValidElement(<TableElement {...mockedProps} />)
|
||||
).to.equal(true);
|
||||
});
|
||||
it('has 3 Link elements', () => {
|
||||
const wrapper = shallow(<TableElement {...mockedProps} />);
|
||||
expect(wrapper.find(Link)).to.have.length(3);
|
||||
});
|
||||
it('has 14 columns', () => {
|
||||
const wrapper = shallow(<TableElement {...mockedProps} />);
|
||||
expect(wrapper.find('div.table-column')).to.have.length(14);
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import * as r from '../../../javascripts/SqlLab/reducers';
|
||||
import * as actions from '../../../javascripts/SqlLab/actions';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
describe('sqlLabReducer', () => {
|
||||
describe('CLONE_QUERY_TO_NEW_TAB', () => {
|
||||
const testQuery = { sql: 'SELECT * FROM...', dbId: 1, id: 1 };
|
||||
const state = Object.assign(r.initialState, { queries: [testQuery] });
|
||||
const newState = r.sqlLabReducer(state, actions.cloneQueryToNewTab(testQuery));
|
||||
|
||||
it('should have at most one more tab', () => {
|
||||
expect(newState.queryEditors).have.length(2);
|
||||
});
|
||||
|
||||
it('should have the same SQL as the cloned query', () => {
|
||||
expect(newState.queryEditors[1].sql).to.equal(testQuery.sql);
|
||||
});
|
||||
|
||||
it('should prefix the new tab title with "Copy of"', () => {
|
||||
expect(newState.queryEditors[1].title).to.include('Copy of');
|
||||
});
|
||||
|
||||
it('should push the cloned tab onto tab history stack', () => {
|
||||
expect(newState.tabHistory[1]).to.eq(newState.queryEditors[1].id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
.scrollbar-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.scrollbar-content {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
overflow: scroll;
|
||||
margin-right: 0px;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
146
caravel/assets/vendor/select2.sortable.js
vendored
@@ -1,146 +0,0 @@
|
||||
/**
|
||||
* jQuery Select2 Sortable
|
||||
* - enable select2 to be sortable via normal select element
|
||||
*
|
||||
* author : Vafour
|
||||
* modified : Kevin Provance (kprovance)
|
||||
* inspired by : jQuery Chosen Sortable (https://github.com/mrhenry/jquery-chosen-sortable)
|
||||
* License : GPL
|
||||
*/
|
||||
|
||||
(function ($) {
|
||||
$.fn.extend({
|
||||
select2SortableOrder: function () {
|
||||
var $this = this.filter('[multiple]');
|
||||
|
||||
$this.each(function () {
|
||||
var $select = $(this);
|
||||
|
||||
// skip elements not select2-ed
|
||||
if (typeof ($select.data('select2')) !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
var $select2 = $select.siblings('.select2-container');
|
||||
var sorted;
|
||||
|
||||
// Opt group names
|
||||
var optArr = [];
|
||||
|
||||
$select.find('optgroup').each(function(idx, val) {
|
||||
optArr.push (val);
|
||||
});
|
||||
|
||||
$select.find('option').each(function(idx, val) {
|
||||
var groupName = $(this).parent('optgroup').prop('label');
|
||||
var optVal = this;
|
||||
|
||||
if (groupName === undefined) {
|
||||
if (this.value !== '' && !this.selected) {
|
||||
optArr.push (optVal);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sorted = $($select2.find('.select2-choices li[class!="select2-search-field"]').map(function () {
|
||||
if (!this) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var id = $(this).data('select2Data').id;
|
||||
|
||||
return $select.find('option[value="' + id + '"]')[0];
|
||||
}));
|
||||
|
||||
sorted.push.apply(sorted, optArr);
|
||||
|
||||
$select.children().remove();
|
||||
$select.append(sorted);
|
||||
});
|
||||
|
||||
return $this;
|
||||
},
|
||||
|
||||
select2Sortable: function () {
|
||||
var args = Array.prototype.slice.call(arguments, 0);
|
||||
var $this = this.filter('[multiple]'),
|
||||
validMethods = ['destroy'];
|
||||
|
||||
if (args.length === 0 || typeof (args[0]) === 'object') {
|
||||
var defaultOptions = {
|
||||
bindOrder: 'formSubmit', // or sortableStop
|
||||
sortableOptions: {
|
||||
placeholder: 'ui-state-highlight',
|
||||
items: 'li:not(.select2-search-field)',
|
||||
tolerance: 'pointer'
|
||||
}
|
||||
};
|
||||
|
||||
var options = $.extend(defaultOptions, args[0]);
|
||||
|
||||
// Init select2 only if not already initialized to prevent select2 configuration loss
|
||||
if (typeof ($this.data('select2')) !== 'object') {
|
||||
$this.select2();
|
||||
}
|
||||
|
||||
$this.each(function () {
|
||||
var $select = $(this)
|
||||
var $select2choices = $select.siblings('.select2-container').find('.select2-choices');
|
||||
|
||||
// Init jQuery UI Sortable
|
||||
$select2choices.sortable(options.sortableOptions);
|
||||
|
||||
switch (options.bindOrder) {
|
||||
case 'sortableStop':
|
||||
// apply options ordering in sortstop event
|
||||
$select2choices.on("sortstop.select2sortable", function (event, ui) {
|
||||
$select.select2SortableOrder();
|
||||
});
|
||||
|
||||
$select.on('change', function (e) {
|
||||
$(this).select2SortableOrder();
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
// apply options ordering in form submit
|
||||
$select.closest('form').unbind('submit.select2sortable').on('submit.select2sortable', function () {
|
||||
$select.select2SortableOrder();
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (typeof (args[0] === 'string')) {
|
||||
if ($.inArray(args[0], validMethods) == -1) {
|
||||
throw "Unknown method: " + args[0];
|
||||
}
|
||||
|
||||
if (args[0] === 'destroy') {
|
||||
$this.select2SortableDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
},
|
||||
|
||||
select2SortableDestroy: function () {
|
||||
var $this = this.filter('[multiple]');
|
||||
$this.each(function () {
|
||||
var $select = $(this)
|
||||
var $select2choices = $select.parent().find('.select2-choices');
|
||||
|
||||
// unbind form submit event
|
||||
$select.closest('form').unbind('submit.select2sortable');
|
||||
|
||||
// unbind sortstop event
|
||||
$select2choices.unbind("sortstop.select2sortable");
|
||||
|
||||
// destroy select2Sortable
|
||||
$select2choices.sortable('destroy');
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
});
|
||||
}(jQuery));
|
||||
@@ -1,195 +0,0 @@
|
||||
import d3 from 'd3';
|
||||
import { formatDate } from '../javascripts/modules/dates';
|
||||
|
||||
require('./big_number.css');
|
||||
|
||||
function bigNumberVis(slice) {
|
||||
const div = d3.select(slice.selector);
|
||||
|
||||
function render() {
|
||||
d3.json(slice.jsonEndpoint(), function (error, payload) {
|
||||
// Define the percentage bounds that define color from red to green
|
||||
if (error !== null) {
|
||||
slice.error(error.responseText, error);
|
||||
return;
|
||||
}
|
||||
div.html(''); // reset
|
||||
|
||||
const fd = payload.form_data;
|
||||
const json = payload.data;
|
||||
|
||||
const f = d3.format(fd.y_axis_format);
|
||||
const fp = d3.format('+.1%');
|
||||
const width = slice.width();
|
||||
const height = slice.height();
|
||||
const svg = div.append('svg');
|
||||
svg.attr('width', width);
|
||||
svg.attr('height', height);
|
||||
const data = json.data;
|
||||
let vCompare;
|
||||
let v;
|
||||
if (fd.viz_type === 'big_number') {
|
||||
v = data[data.length - 1][1];
|
||||
} else {
|
||||
v = data[0][0];
|
||||
}
|
||||
if (json.compare_lag > 0) {
|
||||
const pos = data.length - (json.compare_lag + 1);
|
||||
if (pos >= 0) {
|
||||
vCompare = (v / data[pos][1]) - 1;
|
||||
}
|
||||
}
|
||||
const dateExt = d3.extent(data, (d) => d[0]);
|
||||
const valueExt = d3.extent(data, (d) => d[1]);
|
||||
|
||||
const margin = 20;
|
||||
const scaleX = d3.time.scale.utc().domain(dateExt).range([margin, width - margin]);
|
||||
const scaleY = d3.scale.linear().domain(valueExt).range([height - (margin), margin]);
|
||||
const colorRange = [d3.hsl(0, 1, 0.3), d3.hsl(120, 1, 0.3)];
|
||||
const scaleColor = d3.scale
|
||||
.linear().domain([-1, 1])
|
||||
.interpolate(d3.interpolateHsl)
|
||||
.range(colorRange)
|
||||
.clamp(true);
|
||||
const line = d3.svg.line()
|
||||
.x(function (d) {
|
||||
return scaleX(d[0]);
|
||||
})
|
||||
.y(function (d) {
|
||||
return scaleY(d[1]);
|
||||
})
|
||||
.interpolate('basis');
|
||||
|
||||
let y = height / 2;
|
||||
let g = svg.append('g');
|
||||
// Printing big number
|
||||
g.append('g').attr('class', 'digits')
|
||||
.attr('opacity', 1)
|
||||
.append('text')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', y)
|
||||
.attr('class', 'big')
|
||||
.attr('alignment-baseline', 'middle')
|
||||
.attr('id', 'bigNumber')
|
||||
.style('font-weight', 'bold')
|
||||
.style('cursor', 'pointer')
|
||||
.text(f(v))
|
||||
.style('font-size', d3.min([height, width]) / 3.5)
|
||||
.style('text-anchor', 'middle')
|
||||
.attr('fill', 'black');
|
||||
|
||||
// Printing big number subheader text
|
||||
if (json.subheader !== null) {
|
||||
g.append('text')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', (height / 16) * 12)
|
||||
.text(json.subheader)
|
||||
.attr('id', 'subheader_text')
|
||||
.style('font-size', d3.min([height, width]) / 8)
|
||||
.style('text-anchor', 'middle');
|
||||
}
|
||||
|
||||
if (fd.viz_type === 'big_number') {
|
||||
// Drawing trend line
|
||||
|
||||
g.append('path')
|
||||
.attr('d', function () {
|
||||
return line(data);
|
||||
})
|
||||
.attr('stroke-width', 5)
|
||||
.attr('opacity', 0.5)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke-linecap', 'round')
|
||||
.attr('stroke', 'grey');
|
||||
|
||||
g = svg.append('g')
|
||||
.attr('class', 'digits')
|
||||
.attr('opacity', 1);
|
||||
|
||||
if (vCompare !== null) {
|
||||
y = (height / 8) * 3;
|
||||
}
|
||||
|
||||
const c = scaleColor(vCompare);
|
||||
|
||||
// Printing compare %
|
||||
if (vCompare) {
|
||||
g.append('text')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', (height / 16) * 12)
|
||||
.text(fp(vCompare) + json.compare_suffix)
|
||||
.style('font-size', d3.min([height, width]) / 8)
|
||||
.style('text-anchor', 'middle')
|
||||
.attr('fill', c)
|
||||
.attr('stroke', c);
|
||||
}
|
||||
|
||||
const gAxis = svg.append('g').attr('class', 'axis').attr('opacity', 0);
|
||||
g = gAxis.append('g');
|
||||
const xAxis = d3.svg.axis()
|
||||
.scale(scaleX)
|
||||
.orient('bottom')
|
||||
.ticks(4)
|
||||
.tickFormat(formatDate);
|
||||
g.call(xAxis);
|
||||
g.attr('transform', 'translate(0,' + (height - margin) + ')');
|
||||
|
||||
g = gAxis.append('g').attr('transform', 'translate(' + (width - margin) + ',0)');
|
||||
const yAxis = d3.svg.axis()
|
||||
.scale(scaleY)
|
||||
.orient('left')
|
||||
.tickFormat(d3.format(fd.y_axis_format))
|
||||
.tickValues(valueExt);
|
||||
g.call(yAxis);
|
||||
g.selectAll('text')
|
||||
.style('text-anchor', 'end')
|
||||
.attr('y', '-7')
|
||||
.attr('x', '-4');
|
||||
|
||||
g.selectAll('text')
|
||||
.style('font-size', '10px');
|
||||
|
||||
div.on('mouseover', function () {
|
||||
const el = d3.select(this);
|
||||
el.selectAll('path')
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr('opacity', 1)
|
||||
.style('stroke-width', '2px');
|
||||
el.selectAll('g.digits')
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr('opacity', 0.1);
|
||||
el.selectAll('g.axis')
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr('opacity', 1);
|
||||
})
|
||||
.on('mouseout', function () {
|
||||
const el = d3.select(this);
|
||||
el.select('path')
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr('opacity', 0.5)
|
||||
.style('stroke-width', '5px');
|
||||
el.selectAll('g.digits')
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr('opacity', 1);
|
||||
el.selectAll('g.axis')
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr('opacity', 0);
|
||||
});
|
||||
}
|
||||
slice.done(payload);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
render,
|
||||
resize: render,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = bigNumberVis;
|
||||
@@ -1,54 +0,0 @@
|
||||
// JS
|
||||
import d3 from 'd3';
|
||||
|
||||
// CSS
|
||||
require('./cal_heatmap.css');
|
||||
require('../node_modules/cal-heatmap/cal-heatmap.css');
|
||||
|
||||
const CalHeatMap = require('cal-heatmap');
|
||||
|
||||
function calHeatmap(slice) {
|
||||
const div = d3.select(slice.selector);
|
||||
|
||||
const render = function () {
|
||||
d3.json(slice.jsonEndpoint(), function (error, json) {
|
||||
const data = json.data;
|
||||
if (error !== null) {
|
||||
slice.error(error.responseText, error);
|
||||
return;
|
||||
}
|
||||
|
||||
div.selectAll('*').remove();
|
||||
const cal = new CalHeatMap();
|
||||
|
||||
const timestamps = data.timestamps;
|
||||
const extents = d3.extent(Object.keys(timestamps), (key) => timestamps[key]);
|
||||
const step = (extents[1] - extents[0]) / 5;
|
||||
|
||||
try {
|
||||
cal.init({
|
||||
start: data.start,
|
||||
data: timestamps,
|
||||
itemSelector: slice.selector,
|
||||
tooltip: true,
|
||||
domain: data.domain,
|
||||
subDomain: data.subdomain,
|
||||
range: data.range,
|
||||
browsing: true,
|
||||
legend: [extents[0], extents[0] + step, extents[0] + step * 2, extents[0] + step * 3],
|
||||
});
|
||||
} catch (e) {
|
||||
slice.error(e);
|
||||
}
|
||||
|
||||
slice.done(json);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
render,
|
||||
resize: render,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = calHeatmap;
|
||||
@@ -1,182 +0,0 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import d3 from 'd3';
|
||||
|
||||
require('./directed_force.css');
|
||||
|
||||
/* Modified from http://bl.ocks.org/d3noob/5141278 */
|
||||
function directedForceVis(slice) {
|
||||
const div = d3.select(slice.selector);
|
||||
|
||||
const render = function () {
|
||||
const width = slice.width();
|
||||
const height = slice.height() - 25;
|
||||
d3.json(slice.jsonEndpoint(), function (error, json) {
|
||||
if (error !== null) {
|
||||
slice.error(error.responseText, error);
|
||||
return;
|
||||
}
|
||||
const linkLength = json.form_data.link_length || 200;
|
||||
const charge = json.form_data.charge || -500;
|
||||
|
||||
const links = json.data;
|
||||
const nodes = {};
|
||||
// Compute the distinct nodes from the links.
|
||||
links.forEach(function (link) {
|
||||
link.source = nodes[link.source] || (nodes[link.source] = {
|
||||
name: link.source,
|
||||
});
|
||||
link.target = nodes[link.target] || (nodes[link.target] = {
|
||||
name: link.target,
|
||||
});
|
||||
link.value = Number(link.value);
|
||||
|
||||
const targetName = link.target.name;
|
||||
const sourceName = link.source.name;
|
||||
|
||||
if (nodes[targetName].total === undefined) {
|
||||
nodes[targetName].total = link.value;
|
||||
}
|
||||
if (nodes[sourceName].total === undefined) {
|
||||
nodes[sourceName].total = 0;
|
||||
}
|
||||
if (nodes[targetName].max === undefined) {
|
||||
nodes[targetName].max = 0;
|
||||
}
|
||||
if (link.value > nodes[targetName].max) {
|
||||
nodes[targetName].max = link.value;
|
||||
}
|
||||
if (nodes[targetName].min === undefined) {
|
||||
nodes[targetName].min = 0;
|
||||
}
|
||||
if (link.value > nodes[targetName].min) {
|
||||
nodes[targetName].min = link.value;
|
||||
}
|
||||
|
||||
nodes[targetName].total += link.value;
|
||||
});
|
||||
|
||||
/* eslint-disable no-use-before-define */
|
||||
// add the curvy lines
|
||||
function tick() {
|
||||
path.attr('d', function (d) {
|
||||
const dx = d.target.x - d.source.x;
|
||||
const dy = d.target.y - d.source.y;
|
||||
const dr = Math.sqrt(dx * dx + dy * dy);
|
||||
return (
|
||||
'M' +
|
||||
d.source.x + ',' +
|
||||
d.source.y + 'A' +
|
||||
dr + ',' + dr + ' 0 0,1 ' +
|
||||
d.target.x + ',' +
|
||||
d.target.y
|
||||
);
|
||||
});
|
||||
|
||||
node.attr('transform', function (d) {
|
||||
return 'translate(' + d.x + ',' + d.y + ')';
|
||||
});
|
||||
}
|
||||
/* eslint-enable no-use-before-define */
|
||||
|
||||
const force = d3.layout.force()
|
||||
.nodes(d3.values(nodes))
|
||||
.links(links)
|
||||
.size([width, height])
|
||||
.linkDistance(linkLength)
|
||||
.charge(charge)
|
||||
.on('tick', tick)
|
||||
.start();
|
||||
|
||||
div.selectAll('*').remove();
|
||||
const svg = div.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height);
|
||||
|
||||
// build the arrow.
|
||||
svg.append('svg:defs').selectAll('marker')
|
||||
.data(['end']) // Different link/path types can be defined here
|
||||
.enter()
|
||||
.append('svg:marker') // This section adds in the arrows
|
||||
.attr('id', String)
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refX', 15)
|
||||
.attr('refY', -1.5)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('svg:path')
|
||||
.attr('d', 'M0,-5L10,0L0,5');
|
||||
|
||||
const edgeScale = d3.scale.linear()
|
||||
.range([0.1, 0.5]);
|
||||
// add the links and the arrows
|
||||
const path = svg.append('svg:g').selectAll('path')
|
||||
.data(force.links())
|
||||
.enter()
|
||||
.append('svg:path')
|
||||
.attr('class', 'link')
|
||||
.style('opacity', function (d) {
|
||||
return edgeScale(d.value / d.target.max);
|
||||
})
|
||||
.attr('marker-end', 'url(#end)');
|
||||
|
||||
// define the nodes
|
||||
const node = svg.selectAll('.node')
|
||||
.data(force.nodes())
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'node')
|
||||
.on('mouseenter', function () {
|
||||
d3.select(this)
|
||||
.select('circle')
|
||||
.transition()
|
||||
.style('stroke-width', 5);
|
||||
|
||||
d3.select(this)
|
||||
.select('text')
|
||||
.transition()
|
||||
.style('font-size', 25);
|
||||
})
|
||||
.on('mouseleave', function () {
|
||||
d3.select(this)
|
||||
.select('circle')
|
||||
.transition()
|
||||
.style('stroke-width', 1.5);
|
||||
d3.select(this)
|
||||
.select('text')
|
||||
.transition()
|
||||
.style('font-size', 12);
|
||||
})
|
||||
.call(force.drag);
|
||||
|
||||
// add the nodes
|
||||
const ext = d3.extent(d3.values(nodes), function (d) {
|
||||
return Math.sqrt(d.total);
|
||||
});
|
||||
const circleScale = d3.scale.linear()
|
||||
.domain(ext)
|
||||
.range([3, 30]);
|
||||
|
||||
node.append('circle')
|
||||
.attr('r', function (d) {
|
||||
return circleScale(Math.sqrt(d.total));
|
||||
});
|
||||
|
||||
// add the text
|
||||
node.append('text')
|
||||
.attr('x', 6)
|
||||
.attr('dy', '.35em')
|
||||
.text(function (d) {
|
||||
return d.name;
|
||||
});
|
||||
|
||||
slice.done(json);
|
||||
});
|
||||
};
|
||||
return {
|
||||
render,
|
||||
resize: render,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = directedForceVis;
|
||||
@@ -1,229 +0,0 @@
|
||||
import d3 from 'd3';
|
||||
import { colorScalerFactory } from '../javascripts/modules/colors';
|
||||
|
||||
const $ = require('jquery');
|
||||
d3.tip = require('d3-tip');
|
||||
|
||||
require('./heatmap.css');
|
||||
|
||||
// Inspired from http://bl.ocks.org/mbostock/3074470
|
||||
// https://jsfiddle.net/cyril123/h0reyumq/
|
||||
function heatmapVis(slice) {
|
||||
function refresh() {
|
||||
const margin = {
|
||||
top: 10,
|
||||
right: 10,
|
||||
bottom: 35,
|
||||
left: 35,
|
||||
};
|
||||
|
||||
d3.json(slice.jsonEndpoint(), function (error, payload) {
|
||||
if (error) {
|
||||
slice.error(error.responseText, error);
|
||||
return;
|
||||
}
|
||||
const data = payload.data;
|
||||
// Dynamically adjusts based on max x / y category lengths
|
||||
function adjustMargins() {
|
||||
const pixelsPerCharX = 4.5; // approx, depends on font size
|
||||
const pixelsPerCharY = 6.8; // approx, depends on font size
|
||||
let longestX = 1;
|
||||
let longestY = 1;
|
||||
let datum;
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
datum = data[i];
|
||||
longestX = Math.max(longestX, datum.x.length || 1);
|
||||
longestY = Math.max(longestY, datum.y.length || 1);
|
||||
}
|
||||
|
||||
margin.left = Math.ceil(Math.max(margin.left, pixelsPerCharY * longestY));
|
||||
margin.bottom = Math.ceil(Math.max(margin.bottom, pixelsPerCharX * longestX));
|
||||
}
|
||||
|
||||
function ordScale(k, rangeBands, reverse = false) {
|
||||
let domain = {};
|
||||
$.each(data, function (i, d) {
|
||||
domain[d[k]] = true;
|
||||
});
|
||||
domain = Object.keys(domain).sort(function (a, b) {
|
||||
return b - a;
|
||||
});
|
||||
if (reverse) {
|
||||
domain.reverse();
|
||||
}
|
||||
if (rangeBands === undefined) {
|
||||
return d3.scale.ordinal().domain(domain).range(d3.range(domain.length));
|
||||
}
|
||||
return d3.scale.ordinal().domain(domain).rangeBands(rangeBands);
|
||||
}
|
||||
|
||||
slice.container.html('');
|
||||
const matrix = {};
|
||||
const fd = payload.form_data;
|
||||
|
||||
adjustMargins();
|
||||
|
||||
const width = slice.width();
|
||||
const height = slice.height();
|
||||
const hmWidth = width - (margin.left + margin.right);
|
||||
const hmHeight = height - (margin.bottom + margin.top);
|
||||
const fp = d3.format('.3p');
|
||||
|
||||
const xScale = ordScale('x');
|
||||
const yScale = ordScale('y', undefined, true);
|
||||
const xRbScale = ordScale('x', [0, hmWidth]);
|
||||
const yRbScale = ordScale('y', [hmHeight, 0]);
|
||||
const X = 0;
|
||||
const Y = 1;
|
||||
const heatmapDim = [xRbScale.domain().length, yRbScale.domain().length];
|
||||
|
||||
const color = colorScalerFactory(fd.linear_color_scheme);
|
||||
|
||||
const scale = [
|
||||
d3.scale.linear()
|
||||
.domain([0, heatmapDim[X]])
|
||||
.range([0, hmWidth]),
|
||||
d3.scale.linear()
|
||||
.domain([0, heatmapDim[Y]])
|
||||
.range([0, hmHeight]),
|
||||
];
|
||||
|
||||
const container = d3.select(slice.selector);
|
||||
|
||||
const canvas = container.append('canvas')
|
||||
.attr('width', heatmapDim[X])
|
||||
.attr('height', heatmapDim[Y])
|
||||
.style('width', hmWidth + 'px')
|
||||
.style('height', hmHeight + 'px')
|
||||
.style('image-rendering', fd.canvas_image_rendering)
|
||||
.style('left', margin.left + 'px')
|
||||
.style('top', margin.top + 'px')
|
||||
.style('position', 'absolute');
|
||||
|
||||
const svg = container.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.style('left', '0px')
|
||||
.style('top', '0px')
|
||||
.style('position', 'absolute');
|
||||
|
||||
const rect = svg.append('g')
|
||||
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
|
||||
.append('rect')
|
||||
.style('fill-opacity', 0)
|
||||
.attr('stroke', 'black')
|
||||
.attr('width', hmWidth)
|
||||
.attr('height', hmHeight);
|
||||
|
||||
const tip = d3.tip()
|
||||
.attr('class', 'd3-tip')
|
||||
.offset(function () {
|
||||
const k = d3.mouse(this);
|
||||
const x = k[0] - (hmWidth / 2);
|
||||
return [k[1] - 20, x];
|
||||
})
|
||||
.html(function () {
|
||||
let s = '';
|
||||
const k = d3.mouse(this);
|
||||
const m = Math.floor(scale[0].invert(k[0]));
|
||||
const n = Math.floor(scale[1].invert(k[1]));
|
||||
if (m in matrix && n in matrix[m]) {
|
||||
const obj = matrix[m][n];
|
||||
s += '<div><b>' + fd.all_columns_x + ': </b>' + obj.x + '<div>';
|
||||
s += '<div><b>' + fd.all_columns_y + ': </b>' + obj.y + '<div>';
|
||||
s += '<div><b>' + fd.metric + ': </b>' + obj.v + '<div>';
|
||||
s += '<div><b>%: </b>' + fp(obj.perc) + '<div>';
|
||||
tip.style('display', null);
|
||||
} else {
|
||||
// this is a hack to hide the tooltip because we have map it to a single <rect>
|
||||
// d3-tip toggles opacity and calling hide here is undone by the lib after this call
|
||||
tip.style('display', 'none');
|
||||
}
|
||||
return s;
|
||||
});
|
||||
|
||||
rect.call(tip);
|
||||
|
||||
const xAxis = d3.svg.axis()
|
||||
.scale(xRbScale)
|
||||
.tickValues(xRbScale.domain().filter(
|
||||
function (d, i) {
|
||||
return !(i % (parseInt(fd.xscale_interval, 10)));
|
||||
}))
|
||||
.orient('bottom');
|
||||
|
||||
const yAxis = d3.svg.axis()
|
||||
.scale(yRbScale)
|
||||
.tickValues(yRbScale.domain().filter(
|
||||
function (d, i) {
|
||||
return !(i % (parseInt(fd.yscale_interval, 10)));
|
||||
}))
|
||||
.orient('left');
|
||||
|
||||
svg.append('g')
|
||||
.attr('class', 'x axis')
|
||||
.attr('transform', 'translate(' + margin.left + ',' + (margin.top + hmHeight) + ')')
|
||||
.call(xAxis)
|
||||
.selectAll('text')
|
||||
.style('text-anchor', 'end')
|
||||
.attr('transform', 'rotate(-45)');
|
||||
|
||||
svg.append('g')
|
||||
.attr('class', 'y axis')
|
||||
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
|
||||
.call(yAxis);
|
||||
|
||||
rect.on('mousemove', tip.show);
|
||||
rect.on('mouseout', tip.hide);
|
||||
|
||||
const context = canvas.node().getContext('2d');
|
||||
context.imageSmoothingEnabled = false;
|
||||
|
||||
// Compute the pixel colors; scaled by CSS.
|
||||
function createImageObj() {
|
||||
const imageObj = new Image();
|
||||
const image = context.createImageData(heatmapDim[0], heatmapDim[1]);
|
||||
const pixs = {};
|
||||
$.each(data, function (i, d) {
|
||||
const c = d3.rgb(color(d.perc));
|
||||
const x = xScale(d.x);
|
||||
const y = yScale(d.y);
|
||||
pixs[x + (y * xScale.domain().length)] = c;
|
||||
if (matrix[x] === undefined) {
|
||||
matrix[x] = {};
|
||||
}
|
||||
if (matrix[x][y] === undefined) {
|
||||
matrix[x][y] = d;
|
||||
}
|
||||
});
|
||||
|
||||
let p = -1;
|
||||
for (let i = 0; i < heatmapDim[0] * heatmapDim[1]; i++) {
|
||||
let c = pixs[i];
|
||||
let alpha = 255;
|
||||
if (c === undefined) {
|
||||
c = d3.rgb('#F00');
|
||||
alpha = 0;
|
||||
}
|
||||
image.data[++p] = c.r;
|
||||
image.data[++p] = c.g;
|
||||
image.data[++p] = c.b;
|
||||
image.data[++p] = alpha;
|
||||
}
|
||||
context.putImageData(image, 0, 0);
|
||||
imageObj.src = canvas.node().toDataURL();
|
||||
}
|
||||
|
||||
createImageObj();
|
||||
|
||||
slice.done(payload);
|
||||
});
|
||||
}
|
||||
return {
|
||||
render: refresh,
|
||||
resize: refresh,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = heatmapVis;
|
||||
@@ -1,25 +0,0 @@
|
||||
const $ = require('jquery');
|
||||
|
||||
function iframeWidget(slice) {
|
||||
function refresh() {
|
||||
$('#code').attr('rows', '15');
|
||||
$.getJSON(slice.jsonEndpoint(), function (payload) {
|
||||
const url = slice.render_template(payload.form_data.url);
|
||||
slice.container.html('<iframe style="width:100%;"></iframe>');
|
||||
const iframe = slice.container.find('iframe');
|
||||
iframe.css('height', slice.height());
|
||||
iframe.attr('src', url);
|
||||
slice.done(payload);
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
slice.error(xhr.responseText, xhr);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
render: refresh,
|
||||
resize: refresh,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = iframeWidget;
|
||||
@@ -1,22 +0,0 @@
|
||||
const $ = require('jquery');
|
||||
|
||||
require('./markup.css');
|
||||
|
||||
function markupWidget(slice) {
|
||||
function refresh() {
|
||||
$('#code').attr('rows', '15');
|
||||
$.getJSON(slice.jsonEndpoint(), function (payload) {
|
||||
slice.container.html(payload.data.html);
|
||||
slice.done(payload);
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
slice.error(xhr.responseText, xhr);
|
||||
});
|
||||
}
|
||||
return {
|
||||
render: refresh,
|
||||
resize: refresh,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = markupWidget;
|
||||
@@ -1,363 +0,0 @@
|
||||
// JS
|
||||
import { category21 } from '../javascripts/modules/colors';
|
||||
import { timeFormatFactory, formatDate } from '../javascripts/modules/dates';
|
||||
const d3 = require('d3');
|
||||
const nv = require('nvd3');
|
||||
|
||||
// CSS
|
||||
require('../node_modules/nvd3/build/nv.d3.min.css');
|
||||
require('./nvd3_vis.css');
|
||||
|
||||
const minBarWidth = 15;
|
||||
const animationTime = 1000;
|
||||
|
||||
const addTotalBarValues = function (chart, data, stacked) {
|
||||
const svg = d3.select('svg');
|
||||
const format = d3.format('.3s');
|
||||
const countSeriesDisplayed = data.length;
|
||||
|
||||
const totalStackedValues = stacked && data.length !== 0 ?
|
||||
data[0].values.map(function (bar, iBar) {
|
||||
const bars = data.map(function (series) {
|
||||
return series.values[iBar];
|
||||
});
|
||||
return d3.sum(bars, function (d) {
|
||||
return d.y;
|
||||
});
|
||||
}) : [];
|
||||
|
||||
const rectsToBeLabeled = svg.selectAll('g.nv-group').filter(
|
||||
function (d, i) {
|
||||
if (!stacked) {
|
||||
return true;
|
||||
}
|
||||
return i === countSeriesDisplayed - 1;
|
||||
}).selectAll('rect.positive');
|
||||
|
||||
const groupLabels = svg.select('g.nv-barsWrap').append('g');
|
||||
rectsToBeLabeled.each(
|
||||
function (d, index) {
|
||||
const rectObj = d3.select(this);
|
||||
const transformAttr = rectObj.attr('transform');
|
||||
const yPos = parseFloat(rectObj.attr('y'));
|
||||
const xPos = parseFloat(rectObj.attr('x'));
|
||||
const rectWidth = parseFloat(rectObj.attr('width'));
|
||||
const t = groupLabels.append('text')
|
||||
.attr('x', xPos) // rough position first, fine tune later
|
||||
.attr('y', yPos - 5)
|
||||
.text(format(stacked ? totalStackedValues[index] : d.y))
|
||||
.attr('transform', transformAttr)
|
||||
.attr('class', 'bar-chart-label');
|
||||
const labelWidth = t.node().getBBox().width;
|
||||
t.attr('x', xPos + rectWidth / 2 - labelWidth / 2); // fine tune
|
||||
});
|
||||
};
|
||||
|
||||
function nvd3Vis(slice) {
|
||||
let chart;
|
||||
let colorKey = 'key';
|
||||
|
||||
|
||||
const render = function () {
|
||||
d3.json(slice.jsonEndpoint(), function (error, payload) {
|
||||
slice.container.html('');
|
||||
// Check error first, otherwise payload can be null
|
||||
if (error) {
|
||||
slice.error(error.responseText, error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculates the longest label size for stretching bottom margin
|
||||
function calculateStretchMargins(payloadData) {
|
||||
let stretchMargin = 0;
|
||||
const pixelsPerCharX = 4.5; // approx, depends on font size
|
||||
let maxLabelSize = 10; // accomodate for shorter labels
|
||||
payloadData.data.forEach((d) => {
|
||||
const axisLabels = d.values;
|
||||
for (let i = 0; i < axisLabels.length; i++) {
|
||||
maxLabelSize = Math.max(axisLabels[i].x.length, maxLabelSize);
|
||||
}
|
||||
});
|
||||
stretchMargin = Math.ceil(pixelsPerCharX * maxLabelSize);
|
||||
return stretchMargin;
|
||||
}
|
||||
|
||||
let width = slice.width();
|
||||
const fd = payload.form_data;
|
||||
|
||||
const barchartWidth = function () {
|
||||
let bars;
|
||||
if (fd.bar_stacked) {
|
||||
bars = d3.max(payload.data, function (d) { return d.values.length; });
|
||||
} else {
|
||||
bars = d3.sum(payload.data, function (d) { return d.values.length; });
|
||||
}
|
||||
if (bars * minBarWidth > width) {
|
||||
return bars * minBarWidth;
|
||||
}
|
||||
return width;
|
||||
};
|
||||
|
||||
const vizType = fd.viz_type;
|
||||
const f = d3.format('.3s');
|
||||
const reduceXTicks = fd.reduce_x_ticks || false;
|
||||
let stacked = false;
|
||||
let row;
|
||||
nv.addGraph(function () {
|
||||
switch (vizType) {
|
||||
case 'line':
|
||||
if (fd.show_brush) {
|
||||
chart = nv.models.lineWithFocusChart();
|
||||
chart.focus.xScale(d3.time.scale.utc());
|
||||
chart.x2Axis
|
||||
.showMaxMin(fd.x_axis_showminmax)
|
||||
.staggerLabels(false);
|
||||
} else {
|
||||
chart = nv.models.lineChart();
|
||||
}
|
||||
// To alter the tooltip header
|
||||
// chart.interactiveLayer.tooltip.headerFormatter(function(){return '';});
|
||||
chart.xScale(d3.time.scale.utc());
|
||||
chart.interpolate(fd.line_interpolation);
|
||||
chart.xAxis
|
||||
.showMaxMin(fd.x_axis_showminmax)
|
||||
.staggerLabels(false);
|
||||
break;
|
||||
|
||||
case 'bar':
|
||||
chart = nv.models.multiBarChart()
|
||||
.showControls(fd.show_controls)
|
||||
.groupSpacing(0.1);
|
||||
|
||||
if (!reduceXTicks) {
|
||||
width = barchartWidth();
|
||||
}
|
||||
chart.width(width);
|
||||
chart.xAxis
|
||||
.showMaxMin(false)
|
||||
.staggerLabels(true);
|
||||
|
||||
stacked = fd.bar_stacked;
|
||||
chart.stacked(stacked);
|
||||
|
||||
if (fd.show_bar_value) {
|
||||
setTimeout(function () {
|
||||
addTotalBarValues(chart, payload.data, stacked);
|
||||
}, animationTime);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'dist_bar':
|
||||
chart = nv.models.multiBarChart()
|
||||
.showControls(fd.show_controls)
|
||||
.reduceXTicks(reduceXTicks)
|
||||
.rotateLabels(45)
|
||||
.groupSpacing(0.1); // Distance between each group of bars.
|
||||
|
||||
chart.xAxis
|
||||
.showMaxMin(false);
|
||||
|
||||
stacked = fd.bar_stacked;
|
||||
chart.stacked(stacked);
|
||||
|
||||
if (fd.show_bar_value) {
|
||||
setTimeout(function () {
|
||||
addTotalBarValues(chart, payload.data, stacked);
|
||||
}, animationTime);
|
||||
}
|
||||
if (!reduceXTicks) {
|
||||
width = barchartWidth();
|
||||
}
|
||||
chart.width(width);
|
||||
break;
|
||||
|
||||
case 'pie':
|
||||
chart = nv.models.pieChart();
|
||||
colorKey = 'x';
|
||||
chart.valueFormat(f);
|
||||
if (fd.donut) {
|
||||
chart.donut(true);
|
||||
}
|
||||
chart.labelsOutside(fd.labels_outside);
|
||||
chart.labelThreshold(0.05) // Configure the minimum slice size for labels to show up
|
||||
.labelType(fd.pie_label_type);
|
||||
chart.cornerRadius(true);
|
||||
break;
|
||||
|
||||
case 'column':
|
||||
chart = nv.models.multiBarChart()
|
||||
.reduceXTicks(false)
|
||||
.rotateLabels(45);
|
||||
break;
|
||||
|
||||
case 'compare':
|
||||
chart = nv.models.cumulativeLineChart();
|
||||
chart.xScale(d3.time.scale.utc());
|
||||
chart.xAxis
|
||||
.showMaxMin(false)
|
||||
.staggerLabels(true);
|
||||
break;
|
||||
|
||||
case 'bubble':
|
||||
row = (col1, col2) => `<tr><td>${col1}</td><td>${col2}</td></tr>`;
|
||||
chart = nv.models.scatterChart();
|
||||
chart.showDistX(true);
|
||||
chart.showDistY(true);
|
||||
chart.tooltip.contentGenerator(function (obj) {
|
||||
const p = obj.point;
|
||||
let s = '<table>';
|
||||
s += (
|
||||
`<tr><td style="color: ${p.color};">` +
|
||||
`<strong>${p[fd.entity]}</strong> (${p.group})` +
|
||||
'</td></tr>');
|
||||
s += row(fd.x, f(p.x));
|
||||
s += row(fd.y, f(p.y));
|
||||
s += row(fd.size, f(p.size));
|
||||
s += '</table>';
|
||||
return s;
|
||||
});
|
||||
chart.pointRange([5, fd.max_bubble_size * fd.max_bubble_size]);
|
||||
break;
|
||||
|
||||
case 'area':
|
||||
chart = nv.models.stackedAreaChart();
|
||||
chart.showControls(fd.show_controls);
|
||||
chart.style(fd.stacked_style);
|
||||
chart.xScale(d3.time.scale.utc());
|
||||
chart.xAxis
|
||||
.showMaxMin(false)
|
||||
.staggerLabels(true);
|
||||
break;
|
||||
|
||||
case 'box_plot':
|
||||
colorKey = 'label';
|
||||
chart = nv.models.boxPlotChart();
|
||||
chart.x(function (d) {
|
||||
return d.label;
|
||||
});
|
||||
chart.staggerLabels(true);
|
||||
chart.maxBoxWidth(75); // prevent boxes from being incredibly wide
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('Unrecognized visualization for nvd3' + vizType);
|
||||
}
|
||||
|
||||
if ('showLegend' in chart && typeof fd.show_legend !== 'undefined') {
|
||||
chart.showLegend(fd.show_legend);
|
||||
}
|
||||
|
||||
let height = slice.height() - 15;
|
||||
|
||||
chart.height(height);
|
||||
slice.container.css('height', height + 'px');
|
||||
|
||||
if ((vizType === 'line' || vizType === 'area') && fd.rich_tooltip) {
|
||||
chart.useInteractiveGuideline(true);
|
||||
}
|
||||
if (fd.y_axis_zero) {
|
||||
chart.forceY([0]);
|
||||
} else if (fd.y_log_scale) {
|
||||
chart.yScale(d3.scale.log());
|
||||
}
|
||||
if (fd.x_log_scale) {
|
||||
chart.xScale(d3.scale.log());
|
||||
}
|
||||
let xAxisFormatter;
|
||||
if (vizType === 'bubble') {
|
||||
xAxisFormatter = d3.format('.3s');
|
||||
} else if (fd.x_axis_format === 'smart_date') {
|
||||
xAxisFormatter = formatDate;
|
||||
chart.xAxis.tickFormat(xAxisFormatter);
|
||||
} else if (fd.x_axis_format !== undefined) {
|
||||
xAxisFormatter = timeFormatFactory(fd.x_axis_format);
|
||||
chart.xAxis.tickFormat(xAxisFormatter);
|
||||
}
|
||||
|
||||
if (chart.hasOwnProperty('x2Axis')) {
|
||||
chart.x2Axis.tickFormat(xAxisFormatter);
|
||||
height += 30;
|
||||
}
|
||||
|
||||
if (vizType === 'bubble') {
|
||||
chart.xAxis.tickFormat(d3.format('.3s'));
|
||||
} else if (fd.x_axis_format === 'smart_date') {
|
||||
chart.xAxis.tickFormat(formatDate);
|
||||
} else if (fd.x_axis_format !== undefined) {
|
||||
chart.xAxis.tickFormat(timeFormatFactory(fd.x_axis_format));
|
||||
}
|
||||
if (chart.yAxis !== undefined) {
|
||||
chart.yAxis.tickFormat(d3.format('.3s'));
|
||||
}
|
||||
|
||||
if (fd.y_axis_format) {
|
||||
chart.yAxis.tickFormat(d3.format(fd.y_axis_format));
|
||||
if (chart.y2Axis !== undefined) {
|
||||
chart.y2Axis.tickFormat(d3.format(fd.y_axis_format));
|
||||
}
|
||||
}
|
||||
chart.color((d) => category21(d[colorKey]));
|
||||
|
||||
if (fd.x_axis_label && fd.x_axis_label !== '' && chart.xAxis) {
|
||||
let distance = 0;
|
||||
if (fd.bottom_margin) {
|
||||
distance = fd.bottom_margin - 50;
|
||||
}
|
||||
chart.xAxis.axisLabel(fd.x_axis_label).axisLabelDistance(distance);
|
||||
}
|
||||
|
||||
if (fd.y_axis_label && fd.y_axis_label !== '' && chart.yAxis) {
|
||||
chart.yAxis.axisLabel(fd.y_axis_label);
|
||||
chart.margin({ left: 90 });
|
||||
}
|
||||
|
||||
if (fd.bottom_margin === 'auto') {
|
||||
if (vizType === 'dist_bar') {
|
||||
const stretchMargin = calculateStretchMargins(payload);
|
||||
chart.margin({ bottom: stretchMargin });
|
||||
} else {
|
||||
chart.margin({ bottom: 50 });
|
||||
}
|
||||
} else {
|
||||
chart.margin({ bottom: fd.bottom_margin });
|
||||
}
|
||||
|
||||
let svg = d3.select(slice.selector).select('svg');
|
||||
if (svg.empty()) {
|
||||
svg = d3.select(slice.selector).append('svg');
|
||||
}
|
||||
|
||||
svg
|
||||
.datum(payload.data)
|
||||
.transition().duration(500)
|
||||
.attr('height', height)
|
||||
.attr('width', width)
|
||||
.call(chart);
|
||||
|
||||
if (fd.show_markers) {
|
||||
svg.selectAll('.nv-point')
|
||||
.style('stroke-opacity', 1)
|
||||
.style('fill-opacity', 1);
|
||||
}
|
||||
|
||||
return chart;
|
||||
});
|
||||
|
||||
slice.done(payload);
|
||||
});
|
||||
};
|
||||
|
||||
const update = function () {
|
||||
if (chart && chart.update) {
|
||||
chart.update();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
render,
|
||||
resize: update,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = nvd3Vis;
|
||||
@@ -1,105 +0,0 @@
|
||||
const $ = require('jquery');
|
||||
import d3 from 'd3';
|
||||
d3.parcoords = require('../vendor/parallel_coordinates/d3.parcoords.js');
|
||||
d3.divgrid = require('../vendor/parallel_coordinates/divgrid.js');
|
||||
|
||||
require('../vendor/parallel_coordinates/d3.parcoords.css');
|
||||
require('./parallel_coordinates.css');
|
||||
|
||||
function parallelCoordVis(slice) {
|
||||
function refresh() {
|
||||
$('#code').attr('rows', '15');
|
||||
$.getJSON(slice.jsonEndpoint(), function (payload) {
|
||||
const fd = payload.form_data;
|
||||
const data = payload.data;
|
||||
|
||||
let cols = fd.metrics;
|
||||
if (fd.include_series) {
|
||||
cols = [fd.series].concat(fd.metrics);
|
||||
}
|
||||
|
||||
const ttypes = {};
|
||||
ttypes[fd.series] = 'string';
|
||||
fd.metrics.forEach(function (v) {
|
||||
ttypes[v] = 'number';
|
||||
});
|
||||
|
||||
let ext = d3.extent(data, function (d) {
|
||||
return d[fd.secondary_metric];
|
||||
});
|
||||
ext = [ext[0], (ext[1] - ext[0]) / 2, ext[1]];
|
||||
const cScale = d3.scale.linear()
|
||||
.domain(ext)
|
||||
.range(['red', 'grey', 'blue'])
|
||||
.interpolate(d3.interpolateLab);
|
||||
|
||||
const color = function (d) {
|
||||
return cScale(d[fd.secondary_metric]);
|
||||
};
|
||||
const container = d3.select(slice.selector);
|
||||
container.selectAll('*').remove();
|
||||
const effHeight = fd.show_datatable ? (slice.height() / 2) : slice.height();
|
||||
|
||||
container.append('div')
|
||||
.attr('id', 'parcoords_' + slice.container_id)
|
||||
.style('height', effHeight + 'px')
|
||||
.classed('parcoords', true);
|
||||
|
||||
const parcoords = d3.parcoords()('#parcoords_' + slice.container_id)
|
||||
.width(slice.width())
|
||||
.color(color)
|
||||
.alpha(0.5)
|
||||
.composite('darken')
|
||||
.height(effHeight)
|
||||
.data(data)
|
||||
.dimensions(cols)
|
||||
.types(ttypes)
|
||||
.render()
|
||||
.createAxes()
|
||||
.shadows()
|
||||
.reorderable()
|
||||
.brushMode('1D-axes');
|
||||
|
||||
if (fd.show_datatable) {
|
||||
// create data table, row hover highlighting
|
||||
const grid = d3.divgrid();
|
||||
container.append('div')
|
||||
.style('height', effHeight + 'px')
|
||||
.datum(data)
|
||||
.call(grid)
|
||||
.classed('parcoords grid', true)
|
||||
.selectAll('.row')
|
||||
.on({
|
||||
mouseover(d) {
|
||||
parcoords.highlight([d]);
|
||||
},
|
||||
mouseout: parcoords.unhighlight,
|
||||
});
|
||||
// update data table on brush event
|
||||
parcoords.on('brush', function (d) {
|
||||
d3.select('.grid')
|
||||
.datum(d)
|
||||
.call(grid)
|
||||
.selectAll('.row')
|
||||
.on({
|
||||
mouseover(dd) {
|
||||
parcoords.highlight([dd]);
|
||||
},
|
||||
mouseout: parcoords.unhighlight,
|
||||
});
|
||||
});
|
||||
}
|
||||
slice.done(payload);
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
slice.error(xhr.responseText, xhr);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
render: refresh,
|
||||
resize: refresh,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = parallelCoordVis;
|
||||
@@ -1,38 +0,0 @@
|
||||
import { fixDataTableBodyHeight } from '../javascripts/modules/utils';
|
||||
const $ = require('jquery');
|
||||
|
||||
require('datatables.net-bs');
|
||||
require('./pivot_table.css');
|
||||
require('datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css');
|
||||
|
||||
module.exports = function (slice) {
|
||||
const container = slice.container;
|
||||
|
||||
function refresh() {
|
||||
$.getJSON(slice.jsonEndpoint(), function (json) {
|
||||
const fd = json.form_data;
|
||||
container.html(json.data);
|
||||
if (fd.groupby.length === 1) {
|
||||
const height = container.height();
|
||||
const table = container.find('table').DataTable({
|
||||
paging: false,
|
||||
searching: false,
|
||||
bInfo: false,
|
||||
scrollY: height + 'px',
|
||||
scrollCollapse: true,
|
||||
scrollX: true,
|
||||
});
|
||||
table.column('-1').order('desc').draw();
|
||||
fixDataTableBodyHeight(
|
||||
container.find('.dataTables_wrapper'), height);
|
||||
}
|
||||
slice.done(json);
|
||||
}).fail(function (xhr) {
|
||||
slice.error(xhr.responseText, xhr);
|
||||
});
|
||||
}
|
||||
return {
|
||||
render: refresh,
|
||||
resize: refresh,
|
||||
};
|
||||
};
|
||||
@@ -1,184 +0,0 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { category21 } from '../javascripts/modules/colors';
|
||||
import d3 from 'd3';
|
||||
|
||||
d3.sankey = require('d3-sankey').sankey;
|
||||
|
||||
require('./sankey.css');
|
||||
|
||||
function sankeyVis(slice) {
|
||||
const div = d3.select(slice.selector);
|
||||
const render = function () {
|
||||
const margin = {
|
||||
top: 5,
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
left: 5,
|
||||
};
|
||||
const width = slice.width() - margin.left - margin.right;
|
||||
const height = slice.height() - margin.top - margin.bottom;
|
||||
|
||||
const formatNumber = d3.format(',.2f');
|
||||
|
||||
div.selectAll('*').remove();
|
||||
const svg = div.append('svg')
|
||||
.attr('width', width + margin.left + margin.right)
|
||||
.attr('height', height + margin.top + margin.bottom)
|
||||
.append('g')
|
||||
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
|
||||
|
||||
const tooltip = div.append('div')
|
||||
.attr('class', 'sankey-tooltip')
|
||||
.style('opacity', 0);
|
||||
|
||||
const sankey = d3.sankey()
|
||||
.nodeWidth(15)
|
||||
.nodePadding(10)
|
||||
.size([width, height]);
|
||||
|
||||
const path = sankey.link();
|
||||
|
||||
d3.json(slice.jsonEndpoint(), function (error, json) {
|
||||
if (error !== null) {
|
||||
slice.error(error.responseText, error);
|
||||
return;
|
||||
}
|
||||
let nodes = {};
|
||||
// Compute the distinct nodes from the links.
|
||||
const links = json.data.map(function (row) {
|
||||
const link = Object.assign({}, row);
|
||||
link.source = nodes[link.source] || (nodes[link.source] = { name: link.source });
|
||||
link.target = nodes[link.target] || (nodes[link.target] = { name: link.target });
|
||||
link.value = Number(link.value);
|
||||
return link;
|
||||
});
|
||||
nodes = d3.values(nodes);
|
||||
|
||||
sankey
|
||||
.nodes(nodes)
|
||||
.links(links)
|
||||
.layout(32);
|
||||
|
||||
function getTooltipHtml(d) {
|
||||
let html;
|
||||
|
||||
if (d.sourceLinks) { // is node
|
||||
html = d.name + " Value: <span class='emph'>" + formatNumber(d.value) + '</span>';
|
||||
} else {
|
||||
const val = formatNumber(d.value);
|
||||
const sourcePercent = d3.round((d.value / d.source.value) * 100, 1);
|
||||
const targetPercent = d3.round((d.value / d.target.value) * 100, 1);
|
||||
|
||||
html = [
|
||||
"<div class=''>Path Value: <span class='emph'>", val, '</span></div>',
|
||||
"<div class='percents'>",
|
||||
"<span class='emph'>",
|
||||
(isFinite(sourcePercent) ? sourcePercent : '100'),
|
||||
'%</span> of ', d.source.name, '<br/>',
|
||||
"<span class='emph'>" +
|
||||
(isFinite(targetPercent) ? targetPercent : '--') +
|
||||
'%</span> of ', d.target.name, 'target',
|
||||
'</div>',
|
||||
].join('');
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function onmouseover(d) {
|
||||
tooltip
|
||||
.html(function () { return getTooltipHtml(d); })
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style('left', (d3.event.layerX + 10) + 'px')
|
||||
.style('top', (d3.event.layerY + 10) + 'px')
|
||||
.style('opacity', 0.95);
|
||||
}
|
||||
|
||||
function onmouseout() {
|
||||
tooltip.transition()
|
||||
.duration(100)
|
||||
.style('opacity', 0);
|
||||
}
|
||||
|
||||
const link = svg.append('g').selectAll('.link')
|
||||
.data(links)
|
||||
.enter()
|
||||
.append('path')
|
||||
.attr('class', 'link')
|
||||
.attr('d', path)
|
||||
.style('stroke-width', (d) => Math.max(1, d.dy))
|
||||
.sort((a, b) => b.dy - a.dy)
|
||||
.on('mouseover', onmouseover)
|
||||
.on('mouseout', onmouseout);
|
||||
|
||||
function dragmove(d) {
|
||||
d3.select(this)
|
||||
.attr(
|
||||
'transform',
|
||||
`translate(${d.x},${(d.y = Math.max(0, Math.min(height - d.dy, d3.event.y)))})`
|
||||
);
|
||||
sankey.relayout();
|
||||
link.attr('d', path);
|
||||
}
|
||||
|
||||
const node = svg.append('g').selectAll('.node')
|
||||
.data(nodes)
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'node')
|
||||
.attr('transform', function (d) {
|
||||
return 'translate(' + d.x + ',' + d.y + ')';
|
||||
})
|
||||
.call(d3.behavior.drag()
|
||||
.origin(function (d) {
|
||||
return d;
|
||||
})
|
||||
.on('dragstart', function () {
|
||||
this.parentNode.appendChild(this);
|
||||
})
|
||||
.on('drag', dragmove)
|
||||
);
|
||||
|
||||
node.append('rect')
|
||||
.attr('height', function (d) {
|
||||
return d.dy;
|
||||
})
|
||||
.attr('width', sankey.nodeWidth())
|
||||
.style('fill', function (d) {
|
||||
d.color = category21(d.name.replace(/ .*/, ''));
|
||||
return d.color;
|
||||
})
|
||||
.style('stroke', function (d) {
|
||||
return d3.rgb(d.color).darker(2);
|
||||
})
|
||||
.on('mouseover', onmouseover)
|
||||
.on('mouseout', onmouseout);
|
||||
|
||||
node.append('text')
|
||||
.attr('x', -6)
|
||||
.attr('y', function (d) {
|
||||
return d.dy / 2;
|
||||
})
|
||||
.attr('dy', '.35em')
|
||||
.attr('text-anchor', 'end')
|
||||
.attr('transform', null)
|
||||
.text(function (d) {
|
||||
return d.name;
|
||||
})
|
||||
.filter(function (d) {
|
||||
return d.x < width / 2;
|
||||
})
|
||||
.attr('x', 6 + sankey.nodeWidth())
|
||||
.attr('text-anchor', 'start');
|
||||
|
||||
|
||||
slice.done(json);
|
||||
});
|
||||
};
|
||||
return {
|
||||
render,
|
||||
resize: render,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = sankeyVis;
|
||||
@@ -1,389 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle, no-param-reassign */
|
||||
import d3 from 'd3';
|
||||
import { category21 } from '../javascripts/modules/colors';
|
||||
import { wrapSvgText } from '../javascripts/modules/utils';
|
||||
|
||||
require('./sunburst.css');
|
||||
|
||||
// Modified from http://bl.ocks.org/kerryrodden/7090426
|
||||
function sunburstVis(slice) {
|
||||
const container = d3.select(slice.selector);
|
||||
|
||||
const render = function () {
|
||||
// vars with shared scope within this function
|
||||
const margin = { top: 10, right: 5, bottom: 10, left: 5 };
|
||||
const containerWidth = slice.width();
|
||||
const containerHeight = slice.height();
|
||||
const breadcrumbHeight = containerHeight * 0.085;
|
||||
const visWidth = containerWidth - margin.left - margin.right;
|
||||
const visHeight = containerHeight - margin.top - margin.bottom - breadcrumbHeight;
|
||||
const radius = Math.min(visWidth, visHeight) / 2;
|
||||
|
||||
let colorByCategory = true; // color by category if primary/secondary metrics match
|
||||
let maxBreadcrumbs;
|
||||
let breadcrumbDims; // set based on data
|
||||
let totalSize; // total size of all segments; set after loading the data.
|
||||
let colorScale;
|
||||
let breadcrumbs;
|
||||
let vis;
|
||||
let arcs;
|
||||
let gMiddleText; // dom handles
|
||||
|
||||
// Helper + path gen functions
|
||||
const partition = d3.layout.partition()
|
||||
.size([2 * Math.PI, radius * radius])
|
||||
.value(function (d) { return d.m1; });
|
||||
|
||||
const arc = d3.svg.arc()
|
||||
.startAngle((d) => d.x)
|
||||
.endAngle((d) => d.x + d.dx)
|
||||
.innerRadius(function (d) {
|
||||
return Math.sqrt(d.y);
|
||||
})
|
||||
.outerRadius(function (d) {
|
||||
return Math.sqrt(d.y + d.dy);
|
||||
});
|
||||
|
||||
const formatNum = d3.format('.3s');
|
||||
const formatPerc = d3.format('.3p');
|
||||
|
||||
container.select('svg').remove();
|
||||
|
||||
const svg = container.append('svg:svg')
|
||||
.attr('width', containerWidth)
|
||||
.attr('height', containerHeight);
|
||||
|
||||
function createBreadcrumbs(rawData) {
|
||||
const firstRowData = rawData.data[0];
|
||||
// -2 bc row contains 2x metrics, +extra for %label and buffer
|
||||
maxBreadcrumbs = (firstRowData.length - 2) + 1;
|
||||
breadcrumbDims = {
|
||||
width: visWidth / maxBreadcrumbs,
|
||||
height: breadcrumbHeight * 0.8, // more margin
|
||||
spacing: 3,
|
||||
tipTailWidth: 10,
|
||||
};
|
||||
|
||||
breadcrumbs = svg.append('svg:g')
|
||||
.attr('class', 'breadcrumbs')
|
||||
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
|
||||
|
||||
breadcrumbs.append('svg:text')
|
||||
.attr('class', 'end-label');
|
||||
}
|
||||
|
||||
// Given a node in a partition layout, return an array of all of its ancestor
|
||||
// nodes, highest first, but excluding the root.
|
||||
function getAncestors(node) {
|
||||
const path = [];
|
||||
let current = node;
|
||||
while (current.parent) {
|
||||
path.unshift(current);
|
||||
current = current.parent;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
// Generate a string that describes the points of a breadcrumb polygon.
|
||||
function breadcrumbPoints(d, i) {
|
||||
const points = [];
|
||||
points.push('0,0');
|
||||
points.push(breadcrumbDims.width + ',0');
|
||||
points.push(
|
||||
breadcrumbDims.width + breadcrumbDims.tipTailWidth + ',' + (breadcrumbDims.height / 2));
|
||||
points.push(breadcrumbDims.width + ',' + breadcrumbDims.height);
|
||||
points.push('0,' + breadcrumbDims.height);
|
||||
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
|
||||
points.push(breadcrumbDims.tipTailWidth + ',' + (breadcrumbDims.height / 2));
|
||||
}
|
||||
return points.join(' ');
|
||||
}
|
||||
|
||||
function updateBreadcrumbs(sequenceArray, percentageString) {
|
||||
const g = breadcrumbs.selectAll('g')
|
||||
.data(sequenceArray, function (d) {
|
||||
return d.name + d.depth;
|
||||
});
|
||||
|
||||
// Add breadcrumb and label for entering nodes.
|
||||
const entering = g.enter().append('svg:g');
|
||||
|
||||
entering.append('svg:polygon')
|
||||
.attr('points', breadcrumbPoints)
|
||||
.style('fill', function (d) {
|
||||
return colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1);
|
||||
});
|
||||
|
||||
entering.append('svg:text')
|
||||
.attr('x', (breadcrumbDims.width + breadcrumbDims.tipTailWidth) / 2)
|
||||
.attr('y', breadcrumbDims.height / 4)
|
||||
.attr('dy', '0.35em')
|
||||
.style('fill', function (d) {
|
||||
// Make text white or black based on the lightness of the background
|
||||
const col = d3.hsl(colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1));
|
||||
return col.l < 0.5 ? 'white' : 'black';
|
||||
})
|
||||
.attr('class', 'step-label')
|
||||
.text(function (d) { return d.name.replace(/_/g, ' '); })
|
||||
.call(wrapSvgText, breadcrumbDims.width, breadcrumbDims.height / 2);
|
||||
|
||||
// Set position for entering and updating nodes.
|
||||
g.attr('transform', function (d, i) {
|
||||
return 'translate(' + i * (breadcrumbDims.width + breadcrumbDims.spacing) + ', 0)';
|
||||
});
|
||||
|
||||
// Remove exiting nodes.
|
||||
g.exit().remove();
|
||||
|
||||
// Now move and update the percentage at the end.
|
||||
breadcrumbs.select('.end-label')
|
||||
.attr('x', (sequenceArray.length + 0.5) * (breadcrumbDims.width + breadcrumbDims.spacing))
|
||||
.attr('y', breadcrumbDims.height / 2)
|
||||
.attr('dy', '0.35em')
|
||||
.text(percentageString);
|
||||
|
||||
// Make the breadcrumb trail visible, if it's hidden.
|
||||
breadcrumbs.style('visibility', null);
|
||||
}
|
||||
|
||||
// Fade all but the current sequence, and show it in the breadcrumb trail.
|
||||
function mouseenter(d) {
|
||||
const sequenceArray = getAncestors(d);
|
||||
const parentOfD = sequenceArray[sequenceArray.length - 2] || null;
|
||||
|
||||
const absolutePercentage = (d.m1 / totalSize).toPrecision(3);
|
||||
const conditionalPercentage = parentOfD ? (d.m1 / parentOfD.m1).toPrecision(3) : null;
|
||||
|
||||
const absolutePercString = formatPerc(absolutePercentage);
|
||||
const conditionalPercString = parentOfD ? formatPerc(conditionalPercentage) : '';
|
||||
|
||||
// 3 levels of text if inner-most level, 4 otherwise
|
||||
const yOffsets = ['-25', '7', '35', '60'];
|
||||
let offsetIndex = 0;
|
||||
|
||||
// If metrics match, assume we are coloring by category
|
||||
const metricsMatch = Math.abs(d.m1 - d.m2) < 0.00001;
|
||||
|
||||
gMiddleText.selectAll('*').remove();
|
||||
|
||||
gMiddleText.append('text')
|
||||
.attr('class', 'path-abs-percent')
|
||||
.attr('y', yOffsets[offsetIndex++])
|
||||
.text(absolutePercString + ' of total');
|
||||
|
||||
if (conditionalPercString) {
|
||||
gMiddleText.append('text')
|
||||
.attr('class', 'path-cond-percent')
|
||||
.attr('y', yOffsets[offsetIndex++])
|
||||
.text(conditionalPercString + ' of parent');
|
||||
}
|
||||
|
||||
gMiddleText.append('text')
|
||||
.attr('class', 'path-metrics')
|
||||
.attr('y', yOffsets[offsetIndex++])
|
||||
.text('m1: ' + formatNum(d.m1) + (metricsMatch ? '' : ', m2: ' + formatNum(d.m2)));
|
||||
|
||||
gMiddleText.append('text')
|
||||
.attr('class', 'path-ratio')
|
||||
.attr('y', yOffsets[offsetIndex++])
|
||||
.text((metricsMatch ? '' : ('m2/m1: ' + formatPerc(d.m2 / d.m1))));
|
||||
|
||||
// Reset and fade all the segments.
|
||||
arcs.selectAll('path')
|
||||
.style('stroke-width', null)
|
||||
.style('stroke', null)
|
||||
.style('opacity', 0.7);
|
||||
|
||||
// Then highlight only those that are an ancestor of the current segment.
|
||||
arcs.selectAll('path')
|
||||
.filter(function (node) {
|
||||
return (sequenceArray.indexOf(node) >= 0);
|
||||
})
|
||||
.style('opacity', 1)
|
||||
.style('stroke-width', '2px')
|
||||
.style('stroke', '#000');
|
||||
|
||||
updateBreadcrumbs(sequenceArray, absolutePercString);
|
||||
}
|
||||
|
||||
// Restore everything to full opacity when moving off the visualization.
|
||||
function mouseleave() {
|
||||
// Hide the breadcrumb trail
|
||||
breadcrumbs.style('visibility', 'hidden');
|
||||
|
||||
gMiddleText.selectAll('*').remove();
|
||||
|
||||
// Deactivate all segments during transition.
|
||||
arcs.selectAll('path').on('mouseenter', null);
|
||||
|
||||
// Transition each segment to full opacity and then reactivate it.
|
||||
arcs.selectAll('path')
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style('opacity', 1)
|
||||
.style('stroke', null)
|
||||
.style('stroke-width', null)
|
||||
.each('end', function () {
|
||||
d3.select(this).on('mouseenter', mouseenter);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function buildHierarchy(rows) {
|
||||
const root = {
|
||||
name: 'root',
|
||||
children: [],
|
||||
};
|
||||
|
||||
// each record [groupby1val, groupby2val, (<string> or 0)n, m1, m2]
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
const m1 = Number(row[row.length - 2]);
|
||||
const m2 = Number(row[row.length - 1]);
|
||||
const levels = row.slice(0, row.length - 2);
|
||||
if (isNaN(m1)) { // e.g. if this is a header row
|
||||
continue;
|
||||
}
|
||||
let currentNode = root;
|
||||
for (let level = 0; level < levels.length; level++) {
|
||||
const children = currentNode.children || [];
|
||||
const nodeName = levels[level];
|
||||
// If the next node has the name '0', it will
|
||||
const isLeafNode = (level >= levels.length - 1) || levels[level + 1] === 0;
|
||||
let childNode;
|
||||
let currChild;
|
||||
|
||||
if (!isLeafNode) {
|
||||
// Not yet at the end of the sequence; move down the tree.
|
||||
let foundChild = false;
|
||||
for (let k = 0; k < children.length; k++) {
|
||||
currChild = children[k];
|
||||
if (currChild.name === nodeName &&
|
||||
currChild.level === level) {
|
||||
// must match name AND level
|
||||
|
||||
childNode = currChild;
|
||||
foundChild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If we don't already have a child node for this branch, create it.
|
||||
if (!foundChild) {
|
||||
childNode = {
|
||||
name: nodeName,
|
||||
children: [],
|
||||
level,
|
||||
};
|
||||
children.push(childNode);
|
||||
}
|
||||
currentNode = childNode;
|
||||
} else if (nodeName !== 0) {
|
||||
// Reached the end of the sequence; create a leaf node.
|
||||
childNode = {
|
||||
name: nodeName,
|
||||
m1,
|
||||
m2,
|
||||
};
|
||||
children.push(childNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function recurse(node) {
|
||||
if (node.children) {
|
||||
let sums;
|
||||
let m1 = 0;
|
||||
let m2 = 0;
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
sums = recurse(node.children[i]);
|
||||
m1 += sums[0];
|
||||
m2 += sums[1];
|
||||
}
|
||||
node.m1 = m1;
|
||||
node.m2 = m2;
|
||||
}
|
||||
return [node.m1, node.m2];
|
||||
}
|
||||
|
||||
recurse(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
// Main function to draw and set up the visualization, once we have the data.
|
||||
function createVisualization(rawData) {
|
||||
const tree = buildHierarchy(rawData.data);
|
||||
|
||||
vis = svg.append('svg:g')
|
||||
.attr('class', 'sunburst-vis')
|
||||
.attr('transform', (
|
||||
'translate(' +
|
||||
`${(margin.left + (visWidth / 2))},` +
|
||||
`${(margin.top + breadcrumbHeight + (visHeight / 2))}` +
|
||||
')'
|
||||
))
|
||||
.on('mouseleave', mouseleave);
|
||||
|
||||
arcs = vis.append('svg:g')
|
||||
.attr('id', 'arcs');
|
||||
|
||||
gMiddleText = vis.append('svg:g')
|
||||
.attr('class', 'center-label');
|
||||
|
||||
// Bounding circle underneath the sunburst, to make it easier to detect
|
||||
// when the mouse leaves the parent g.
|
||||
arcs.append('svg:circle')
|
||||
.attr('r', radius)
|
||||
.style('opacity', 0);
|
||||
|
||||
// For efficiency, filter nodes to keep only those large enough to see.
|
||||
const nodes = partition.nodes(tree)
|
||||
.filter(function (d) {
|
||||
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
|
||||
});
|
||||
|
||||
let ext;
|
||||
|
||||
if (rawData.form_data.metric !== rawData.form_data.secondary_metric) {
|
||||
colorByCategory = false;
|
||||
ext = d3.extent(nodes, (d) => d.m2 / d.m1);
|
||||
colorScale = d3.scale.linear()
|
||||
.domain([ext[0], ext[0] + ((ext[1] - ext[0]) / 2), ext[1]])
|
||||
.range(['#00D1C1', 'white', '#FFB400']);
|
||||
}
|
||||
|
||||
const path = arcs.data([tree]).selectAll('path')
|
||||
.data(nodes)
|
||||
.enter()
|
||||
.append('svg:path')
|
||||
.attr('display', function (d) {
|
||||
return d.depth ? null : 'none';
|
||||
})
|
||||
.attr('d', arc)
|
||||
.attr('fill-rule', 'evenodd')
|
||||
.style('fill', (d) => colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1))
|
||||
.style('opacity', 1)
|
||||
.on('mouseenter', mouseenter);
|
||||
|
||||
// Get total size of the tree = value of root node from partition.
|
||||
totalSize = path.node().__data__.value;
|
||||
}
|
||||
|
||||
|
||||
d3.json(slice.jsonEndpoint(), function (error, rawData) {
|
||||
if (error !== null) {
|
||||
slice.error(error.responseText, error);
|
||||
return;
|
||||
}
|
||||
createBreadcrumbs(rawData);
|
||||
createVisualization(rawData);
|
||||
slice.done(rawData);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
render,
|
||||
resize: render,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = sunburstVis;
|
||||
@@ -1,154 +0,0 @@
|
||||
const $ = require('jquery');
|
||||
import d3 from 'd3';
|
||||
import { fixDataTableBodyHeight } from '../javascripts/modules/utils';
|
||||
import { timeFormatFactory, formatDate } from '../javascripts/modules/dates';
|
||||
|
||||
require('./table.css');
|
||||
require('datatables.net-bs');
|
||||
require('datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css');
|
||||
|
||||
function tableVis(slice) {
|
||||
const fC = d3.format('0,000');
|
||||
let timestampFormatter;
|
||||
|
||||
function refresh() {
|
||||
function onError(xhr) {
|
||||
slice.error(xhr.responseText, xhr);
|
||||
return;
|
||||
}
|
||||
function onSuccess(json) {
|
||||
const data = json.data;
|
||||
const fd = json.form_data;
|
||||
// Removing metrics (aggregates) that are strings
|
||||
const realMetrics = [];
|
||||
for (const k in data.records[0]) {
|
||||
if (fd.metrics.indexOf(k) > -1 && !isNaN(data.records[0][k])) {
|
||||
realMetrics.push(k);
|
||||
}
|
||||
}
|
||||
const metrics = realMetrics;
|
||||
|
||||
function col(c) {
|
||||
const arr = [];
|
||||
for (let i = 0; i < data.records.length; i++) {
|
||||
arr.push(data.records[i][c]);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
const maxes = {};
|
||||
for (let i = 0; i < metrics.length; i++) {
|
||||
maxes[metrics[i]] = d3.max(col(metrics[i]));
|
||||
}
|
||||
|
||||
if (fd.table_timestamp_format === 'smart_date') {
|
||||
timestampFormatter = formatDate;
|
||||
} else if (fd.table_timestamp_format !== undefined) {
|
||||
timestampFormatter = timeFormatFactory(fd.table_timestamp_format);
|
||||
}
|
||||
|
||||
const div = d3.select(slice.selector);
|
||||
div.html('');
|
||||
const table = div.append('table')
|
||||
.classed(
|
||||
'dataframe dataframe table table-striped table-bordered ' +
|
||||
'table-condensed table-hover dataTable no-footer', true)
|
||||
.attr('width', '100%');
|
||||
|
||||
table.append('thead').append('tr')
|
||||
.selectAll('th')
|
||||
.data(data.columns)
|
||||
.enter()
|
||||
.append('th')
|
||||
.text(function (d) {
|
||||
return d;
|
||||
});
|
||||
|
||||
table.append('tbody')
|
||||
.selectAll('tr')
|
||||
.data(data.records)
|
||||
.enter()
|
||||
.append('tr')
|
||||
.selectAll('td')
|
||||
.data((row) => data.columns.map((c) => {
|
||||
let val = row[c];
|
||||
if (c === 'timestamp') {
|
||||
val = timestampFormatter(val);
|
||||
}
|
||||
return {
|
||||
col: c,
|
||||
val,
|
||||
isMetric: metrics.indexOf(c) >= 0,
|
||||
};
|
||||
}))
|
||||
.enter()
|
||||
.append('td')
|
||||
.style('background-image', function (d) {
|
||||
if (d.isMetric) {
|
||||
const perc = Math.round((d.val / maxes[d.col]) * 100);
|
||||
return (
|
||||
`linear-gradient(to right, lightgrey, lightgrey ${perc}%, ` +
|
||||
`rgba(0,0,0,0) ${perc}%`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.attr('title', (d) => {
|
||||
if (!isNaN(d.val)) {
|
||||
return fC(d.val);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.attr('data-sort', function (d) {
|
||||
return (d.isMetric) ? d.val : null;
|
||||
})
|
||||
.on('click', function (d) {
|
||||
if (!d.isMetric) {
|
||||
const td = d3.select(this);
|
||||
if (td.classed('filtered')) {
|
||||
slice.removeFilter(d.col, [d.val]);
|
||||
d3.select(this).classed('filtered', false);
|
||||
} else {
|
||||
d3.select(this).classed('filtered', true);
|
||||
slice.addFilter(d.col, [d.val]);
|
||||
}
|
||||
}
|
||||
})
|
||||
.style('cursor', function (d) {
|
||||
return (!d.isMetric) ? 'pointer' : '';
|
||||
})
|
||||
.html((d) => {
|
||||
if (d.isMetric) {
|
||||
return slice.d3format(d.col, d.val);
|
||||
}
|
||||
return d.val;
|
||||
});
|
||||
const height = slice.container.height();
|
||||
const datatable = slice.container.find('.dataTable').DataTable({
|
||||
paging: false,
|
||||
aaSorting: [],
|
||||
searching: fd.include_search,
|
||||
bInfo: false,
|
||||
scrollY: height + 'px',
|
||||
scrollCollapse: true,
|
||||
scrollX: true,
|
||||
});
|
||||
fixDataTableBodyHeight(
|
||||
slice.container.find('.dataTables_wrapper'), height);
|
||||
// Sorting table by main column
|
||||
if (fd.metrics.length > 0) {
|
||||
const mainMetric = fd.metrics[0];
|
||||
datatable.column(data.columns.indexOf(mainMetric)).order('desc').draw();
|
||||
}
|
||||
slice.done(json);
|
||||
slice.container.parents('.widget').find('.tooltip').remove();
|
||||
}
|
||||
$.getJSON(slice.jsonEndpoint(), onSuccess).fail(onError);
|
||||
}
|
||||
|
||||
return {
|
||||
render: refresh,
|
||||
resize() {},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = tableVis;
|
||||
@@ -1,77 +0,0 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
import d3 from 'd3';
|
||||
import cloudLayout from 'd3-cloud';
|
||||
import { category21 } from '../javascripts/modules/colors';
|
||||
|
||||
function wordCloudChart(slice) {
|
||||
const chart = d3.select(slice.selector);
|
||||
|
||||
function refresh() {
|
||||
d3.json(slice.jsonEndpoint(), function (error, json) {
|
||||
if (error !== null) {
|
||||
slice.error(error.responseText, error);
|
||||
return;
|
||||
}
|
||||
const data = json.data;
|
||||
const range = [
|
||||
json.form_data.size_from,
|
||||
json.form_data.size_to,
|
||||
];
|
||||
const rotation = json.form_data.rotation;
|
||||
let fRotation;
|
||||
if (rotation === 'square') {
|
||||
fRotation = () => ~~(Math.random() * 2) * 90;
|
||||
} else if (rotation === 'flat') {
|
||||
fRotation = () => 0;
|
||||
} else {
|
||||
fRotation = () => (~~(Math.random() * 6) - 3) * 30;
|
||||
}
|
||||
const size = [slice.width(), slice.height()];
|
||||
|
||||
const scale = d3.scale.linear()
|
||||
.range(range)
|
||||
.domain(d3.extent(data, function (d) {
|
||||
return d.size;
|
||||
}));
|
||||
|
||||
function draw(words) {
|
||||
chart.selectAll('*').remove();
|
||||
|
||||
chart.append('svg')
|
||||
.attr('width', layout.size()[0])
|
||||
.attr('height', layout.size()[1])
|
||||
.append('g')
|
||||
.attr('transform', `translate(${layout.size()[0] / 2},${layout.size()[1] / 2})`)
|
||||
.selectAll('text')
|
||||
.data(words)
|
||||
.enter()
|
||||
.append('text')
|
||||
.style('font-size', (d) => d.size + 'px')
|
||||
.style('font-family', 'Impact')
|
||||
.style('fill', (d) => category21(d.text))
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('transform', (d) => `translate(${d.x}, ${d.y}) rotate(${d.rotate})`)
|
||||
.text((d) => d.text);
|
||||
}
|
||||
|
||||
const layout = cloudLayout()
|
||||
.size(size)
|
||||
.words(data)
|
||||
.padding(5)
|
||||
.rotate(fRotation)
|
||||
.font('serif')
|
||||
.fontSize((d) => scale(d.size))
|
||||
.on('end', draw);
|
||||
|
||||
layout.start();
|
||||
slice.done(json);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
render: refresh,
|
||||
resize: refresh,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = wordCloudChart;
|
||||
@@ -1,106 +0,0 @@
|
||||
// JS
|
||||
const d3 = require('d3');
|
||||
const Datamap = require('datamaps');
|
||||
|
||||
// CSS
|
||||
require('./world_map.css');
|
||||
|
||||
function worldMapChart(slice) {
|
||||
const render = function () {
|
||||
const container = slice.container;
|
||||
const div = d3.select(slice.selector);
|
||||
|
||||
container.css('height', slice.height());
|
||||
|
||||
d3.json(slice.jsonEndpoint(), function (error, json) {
|
||||
div.selectAll('*').remove();
|
||||
if (error !== null) {
|
||||
slice.error(error.responseText, error);
|
||||
return;
|
||||
}
|
||||
const fd = json.form_data;
|
||||
// Ignore XXX's to get better normalization
|
||||
let data = json.data.filter((d) => (d.country && d.country !== 'XXX'));
|
||||
|
||||
const ext = d3.extent(data, function (d) {
|
||||
return d.m1;
|
||||
});
|
||||
const extRadius = d3.extent(data, function (d) {
|
||||
return d.m2;
|
||||
});
|
||||
const radiusScale = d3.scale.linear()
|
||||
.domain([extRadius[0], extRadius[1]])
|
||||
.range([1, fd.max_bubble_size]);
|
||||
|
||||
const colorScale = d3.scale.linear()
|
||||
.domain([ext[0], ext[1]])
|
||||
.range(['#FFF', 'black']);
|
||||
|
||||
data = data.map((d) => Object.assign({}, d, {
|
||||
radius: radiusScale(d.m2),
|
||||
fillColor: colorScale(d.m1),
|
||||
}));
|
||||
|
||||
const mapData = {};
|
||||
data.forEach((d) => {
|
||||
mapData[d.country] = d;
|
||||
});
|
||||
|
||||
const f = d3.format('.3s');
|
||||
|
||||
container.show();
|
||||
|
||||
const map = new Datamap({
|
||||
element: slice.container.get(0),
|
||||
data,
|
||||
fills: {
|
||||
defaultFill: '#ddd',
|
||||
},
|
||||
geographyConfig: {
|
||||
popupOnHover: true,
|
||||
highlightOnHover: true,
|
||||
borderWidth: 1,
|
||||
borderColor: '#fff',
|
||||
highlightBorderColor: '#fff',
|
||||
highlightFillColor: '#005a63',
|
||||
highlightBorderWidth: 1,
|
||||
popupTemplate: (geo, d) => (
|
||||
`<div class="hoverinfo"><strong>${d.name}</strong><br>${f(d.m1)}</div>`
|
||||
),
|
||||
},
|
||||
bubblesConfig: {
|
||||
borderWidth: 1,
|
||||
borderOpacity: 1,
|
||||
borderColor: '#005a63',
|
||||
popupOnHover: true,
|
||||
radius: null,
|
||||
popupTemplate: (geo, d) => (
|
||||
`<div class="hoverinfo"><strong>${d.name}</strong><br>${f(d.m2)}</div>`
|
||||
),
|
||||
fillOpacity: 0.5,
|
||||
animate: true,
|
||||
highlightOnHover: true,
|
||||
highlightFillColor: '#005a63',
|
||||
highlightBorderColor: 'black',
|
||||
highlightBorderWidth: 2,
|
||||
highlightBorderOpacity: 1,
|
||||
highlightFillOpacity: 0.85,
|
||||
exitDelay: 100,
|
||||
key: JSON.stringify,
|
||||
},
|
||||
});
|
||||
|
||||
map.updateChoropleth(mapData);
|
||||
|
||||
if (fd.show_bubbles) {
|
||||
map.bubbles(data);
|
||||
div.selectAll('circle.datamaps-bubble').style('fill', '#005a63');
|
||||
}
|
||||
slice.done(json);
|
||||
});
|
||||
};
|
||||
|
||||
return { render, resize: render };
|
||||
}
|
||||
|
||||
module.exports = worldMapChart;
|
||||