Compare commits
988 Commits
v0.11.0.b2
...
v1.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
939aae086f | ||
|
|
742e38b08d | ||
|
|
3c7c93fc9f | ||
|
|
53ffff9759 | ||
|
|
2e7fafc4d8 | ||
|
|
c66b09effe | ||
|
|
a087fe4bcd | ||
|
|
1f4946cc04 | ||
|
|
08505a2208 | ||
|
|
e1c186bbf8 | ||
|
|
c83d354eed | ||
|
|
81063731c9 | ||
|
|
f66fe5ff80 | ||
|
|
8425698583 | ||
|
|
8b08b1a563 | ||
|
|
15b228b754 | ||
|
|
1db4157b29 | ||
|
|
079530cf63 | ||
|
|
d2370a94c7 | ||
|
|
903463972b | ||
|
|
2707e24f30 | ||
|
|
3df826692c | ||
|
|
1142a441fc | ||
|
|
53268989c5 | ||
|
|
83ed9fdc51 | ||
|
|
0dc98e87a6 | ||
|
|
0cf4db1137 | ||
|
|
4e27069d07 | ||
|
|
3fcd07bc1c | ||
|
|
3414ff7331 | ||
|
|
04cd798c48 | ||
|
|
50dcf23b1a | ||
|
|
1bb4d6d534 | ||
|
|
66a5e394de | ||
|
|
c4ab0916cc | ||
|
|
73cb6925d3 | ||
|
|
aaf0da4b70 | ||
|
|
c99bd03d99 | ||
|
|
7fbb1b9229 | ||
|
|
ba54d68513 | ||
|
|
f73cbf3b51 | ||
|
|
3f047348e2 | ||
|
|
10fe3c5373 | ||
|
|
9c8755c9ae | ||
|
|
e8908d04bb | ||
|
|
293f9dcaf6 | ||
|
|
ce31b13ff6 | ||
|
|
a033dc4569 | ||
|
|
6ff338964b | ||
|
|
97a7701879 | ||
|
|
7558b391a9 | ||
|
|
b6bed112ee | ||
|
|
9417dcb2c2 | ||
|
|
5f106a1eee | ||
|
|
cda05c73c7 | ||
|
|
95398697cb | ||
|
|
dc019cc37a | ||
|
|
72cb5babe6 | ||
|
|
ebc2e12621 | ||
|
|
f011d3060a | ||
|
|
8c5f71a0a1 | ||
|
|
da00e74491 | ||
|
|
b56ff1357e | ||
|
|
ecd4d659a8 | ||
|
|
fec5565396 | ||
|
|
6ec5ea5c28 | ||
|
|
3f8e32cc1f | ||
|
|
be6426014d | ||
|
|
8b4643d6ac | ||
|
|
d8a0885953 | ||
|
|
83e6b6f50c | ||
|
|
928bd83967 | ||
|
|
230fe15cde | ||
|
|
72ad16a8b3 | ||
|
|
23cc632d5a | ||
|
|
1cf2bb1bb2 | ||
|
|
181031957f | ||
|
|
cfa9a45fc8 | ||
|
|
9bb87e711a | ||
|
|
255a01f786 | ||
|
|
69c26f2c0d | ||
|
|
3650e21458 | ||
|
|
8eefd0e9c4 | ||
|
|
c72a097808 | ||
|
|
2ffda6f5c5 | ||
|
|
ce8ffae152 | ||
|
|
b54dd27959 | ||
|
|
3e807e5b41 | ||
|
|
20f1a60f90 | ||
|
|
9d2619b856 | ||
|
|
a2c7f6df7a | ||
|
|
15a87db5d5 | ||
|
|
2f86466309 | ||
|
|
bccfef533e | ||
|
|
ef020e88e7 | ||
|
|
222a6069cb | ||
|
|
6b6df84bce | ||
|
|
fcfd204ec6 | ||
|
|
57e6c5f05e | ||
|
|
683e369d86 | ||
|
|
f12596a6fc | ||
|
|
09239439ae | ||
|
|
2bb11dffca | ||
|
|
2f019a0897 | ||
|
|
1350555931 | ||
|
|
6d3aa3b53c | ||
|
|
2407b115e4 | ||
|
|
ca3e125da8 | ||
|
|
2d82c4dc98 | ||
|
|
84ca02be09 | ||
|
|
61244dead3 | ||
|
|
907b33b5a0 | ||
|
|
e6fc73f444 | ||
|
|
672347ba8b | ||
|
|
db465ffe58 | ||
|
|
2a447137d4 | ||
|
|
18a157ac84 | ||
|
|
3864f11694 | ||
|
|
8b59815bf2 | ||
|
|
a98df94399 | ||
|
|
2e751b3dad | ||
|
|
b2e747caef | ||
|
|
f9da6ddcdd | ||
|
|
99cef97c89 | ||
|
|
2071ca1bc8 | ||
|
|
5815d635a0 | ||
|
|
517e5bcddb | ||
|
|
eaa2ec877c | ||
|
|
0e68228e6e | ||
|
|
6e5435261b | ||
|
|
1cc9b87ead | ||
|
|
bd9ad3140d | ||
|
|
3e23143910 | ||
|
|
0f95d12e83 | ||
|
|
fd37fd8545 | ||
|
|
742199f05c | ||
|
|
902fb44782 | ||
|
|
10e78bb4e4 | ||
|
|
e4659d0485 | ||
|
|
a25302773d | ||
|
|
2188baca16 | ||
|
|
8940533176 | ||
|
|
1367b63ae1 | ||
|
|
c2a9e2e960 | ||
|
|
8a41328033 | ||
|
|
ce16124d7b | ||
|
|
c547752dc6 | ||
|
|
3981c1c8a7 | ||
|
|
f732f30bf0 | ||
|
|
9c0f0cb044 | ||
|
|
f01399c33d | ||
|
|
253f2e613c | ||
|
|
f2879a1e3d | ||
|
|
af978e966d | ||
|
|
0151360fdf | ||
|
|
d1b0a9580d | ||
|
|
9d796b20df | ||
|
|
574a7573ce | ||
|
|
db9c925cbb | ||
|
|
9ef6836175 | ||
|
|
351cd62189 | ||
|
|
7bbc782e5d | ||
|
|
50d20ff277 | ||
|
|
964926aaab | ||
|
|
b67ecde107 | ||
|
|
6338596710 | ||
|
|
5361a99b22 | ||
|
|
01a8075a67 | ||
|
|
209e714084 | ||
|
|
e3d0b4075e | ||
|
|
ad18128794 | ||
|
|
71fa013970 | ||
|
|
dd6028384d | ||
|
|
5716da205e | ||
|
|
d650995da3 | ||
|
|
18fee82b73 | ||
|
|
23a87410ac | ||
|
|
9b9a752f78 | ||
|
|
4b4758af22 | ||
|
|
3cdd732078 | ||
|
|
77139e2867 | ||
|
|
ff28b751ed | ||
|
|
84aba70a1a | ||
|
|
39c4e1cd59 | ||
|
|
6cf1c1cb70 | ||
|
|
ae2927d7d3 | ||
|
|
cd51076150 | ||
|
|
a5d73514ce | ||
|
|
7ffa8f89aa | ||
|
|
11faabed1f | ||
|
|
25ca061b3d | ||
|
|
8140100496 | ||
|
|
9aab74d8e7 | ||
|
|
4e06a38676 | ||
|
|
7e43e54b9d | ||
|
|
84e1ac8072 | ||
|
|
ba6109e7ac | ||
|
|
febe908e65 | ||
|
|
8445b04844 | ||
|
|
42a5194893 | ||
|
|
3b4e3067bb | ||
|
|
98757db3e3 | ||
|
|
4e7adcf89b | ||
|
|
df0d1f3cc8 | ||
|
|
38f6f82c0b | ||
|
|
4f9ffdc094 | ||
|
|
3431f2ebe8 | ||
|
|
788830ddad | ||
|
|
2e358e89f4 | ||
|
|
d58040ad11 | ||
|
|
4904c67cbe | ||
|
|
b479bb39a0 | ||
|
|
5ee5b5a8f5 | ||
|
|
7cc97fafdb | ||
|
|
c82a01b5c8 | ||
|
|
65237967ec | ||
|
|
53536a15f1 | ||
|
|
4e4d52a558 | ||
|
|
7b11dc0ee6 | ||
|
|
bc230e988d | ||
|
|
c39f440450 | ||
|
|
00e1e90d6e | ||
|
|
b1bd8f1c25 | ||
|
|
837e3a372a | ||
|
|
81fca9329e | ||
|
|
06186d9e8d | ||
|
|
ddf6fc50a5 | ||
|
|
af70f34f05 | ||
|
|
d93a325902 | ||
|
|
8b74de71ba | ||
|
|
509c17632a | ||
|
|
bca7d08082 | ||
|
|
785b6fc412 | ||
|
|
94891502c3 | ||
|
|
8d1b5b4153 | ||
|
|
e586f99cb1 | ||
|
|
7e986db979 | ||
|
|
681fd213da | ||
|
|
34f7b2f50a | ||
|
|
1a06a5c789 | ||
|
|
9d36d39f47 | ||
|
|
c67519e45d | ||
|
|
b7ad138c6c | ||
|
|
6124e08fa3 | ||
|
|
9dd7efe894 | ||
|
|
b1e7a767ae | ||
|
|
99d867f1b4 | ||
|
|
6de66447bb | ||
|
|
de4ff7fc56 | ||
|
|
29de13ba87 | ||
|
|
09542138cc | ||
|
|
8f53274f17 | ||
|
|
8f1750b9f0 | ||
|
|
3d0b90ff2a | ||
|
|
361abac7ad | ||
|
|
57e25786cd | ||
|
|
47b1b03be7 | ||
|
|
9d7171ffaf | ||
|
|
34ffdf52c7 | ||
|
|
8226445e4d | ||
|
|
4d4822fcfb | ||
|
|
cb2e6fddf4 | ||
|
|
a2be7bf060 | ||
|
|
894a85ae63 | ||
|
|
57a0ddb1a9 | ||
|
|
dd24d27558 | ||
|
|
a62db75624 | ||
|
|
ab0a448c87 | ||
|
|
5da8c6c0cb | ||
|
|
b11685d8d4 | ||
|
|
187b557eee | ||
|
|
9126874c87 | ||
|
|
4ccf0decbd | ||
|
|
869fa263f6 | ||
|
|
689a1aac4d | ||
|
|
2aae497169 | ||
|
|
c635d9abed | ||
|
|
eb637b69b8 | ||
|
|
9213a18057 | ||
|
|
07b6b16fda | ||
|
|
bc18b05d0b | ||
|
|
6a489bb743 | ||
|
|
b1fd2101df | ||
|
|
96bc3a5d0e | ||
|
|
2df0979be5 | ||
|
|
512ac9bb27 | ||
|
|
e33439c9f7 | ||
|
|
fd19d2b79d | ||
|
|
b3d2d5aba2 | ||
|
|
3dfaea1dcf | ||
|
|
7d8b8a16fd | ||
|
|
1fae6017bd | ||
|
|
d86962b4e3 | ||
|
|
ca4db619f2 | ||
|
|
c39af040fe | ||
|
|
c900e83bd9 | ||
|
|
589634b621 | ||
|
|
627f3f4fd5 | ||
|
|
69339b7c80 | ||
|
|
703f996630 | ||
|
|
e58f703331 | ||
|
|
fe363e27a3 | ||
|
|
4e218b76ae | ||
|
|
f54e7b3343 | ||
|
|
bb24b878c6 | ||
|
|
7182cbb9d3 | ||
|
|
48faf50db6 | ||
|
|
1db12fa0ec | ||
|
|
c6ef932183 | ||
|
|
5d3cf5367a | ||
|
|
78408e50c5 | ||
|
|
ea12dc4038 | ||
|
|
1388e76b48 | ||
|
|
8d92702fc7 | ||
|
|
f5e8fc816c | ||
|
|
b3d0f705be | ||
|
|
894da612f4 | ||
|
|
35eb7b0980 | ||
|
|
06f96d9258 | ||
|
|
01076c0759 | ||
|
|
490733eff9 | ||
|
|
a781d312c0 | ||
|
|
9e5944d563 | ||
|
|
ff6bb2c45b | ||
|
|
e4f13253e5 | ||
|
|
a16b333b93 | ||
|
|
bff4ebd43d | ||
|
|
7a94bed250 | ||
|
|
e0a010f7be | ||
|
|
40b82004fa | ||
|
|
3bc98fba5b | ||
|
|
045c921372 | ||
|
|
420973c015 | ||
|
|
513d74ac7d | ||
|
|
e0d6d3feeb | ||
|
|
ae5b6fbc57 | ||
|
|
9d579dc089 | ||
|
|
94519d1d76 | ||
|
|
9b4cf4d3ee | ||
|
|
ee57a5bf08 | ||
|
|
e5aa567c73 | ||
|
|
0029b4cd7f | ||
|
|
c423832931 | ||
|
|
bf2366bf1c | ||
|
|
358d6807e5 | ||
|
|
024837137b | ||
|
|
6510b01b6c | ||
|
|
ac18aedde9 | ||
|
|
70f1a923a3 | ||
|
|
350b253833 | ||
|
|
6f3ca1e01a | ||
|
|
8d39ee4171 | ||
|
|
a860c83963 | ||
|
|
f3f39a5b00 | ||
|
|
e6e83423e7 | ||
|
|
d0696853af | ||
|
|
b07b347faa | ||
|
|
47c54b3c85 | ||
|
|
d4534d6045 | ||
|
|
3cf6e31727 | ||
|
|
d631171e81 | ||
|
|
4df58bb724 | ||
|
|
19960eedda | ||
|
|
b3bfc3bc74 | ||
|
|
1978e07748 | ||
|
|
e524db0215 | ||
|
|
81fb139b88 | ||
|
|
1d18109964 | ||
|
|
c380596930 | ||
|
|
106c743647 | ||
|
|
12cbfe1af4 | ||
|
|
3cce4d0ce4 | ||
|
|
4945d0bec7 | ||
|
|
4ba399af67 | ||
|
|
da31d983b7 | ||
|
|
923c463e4a | ||
|
|
70d545410d | ||
|
|
74e6ef5c1d | ||
|
|
abf57e4e70 | ||
|
|
2d206ef470 | ||
|
|
2b33963bee | ||
|
|
a84c3e25f5 | ||
|
|
b9024b18c1 | ||
|
|
bcce9cf251 | ||
|
|
80a7d377fe | ||
|
|
73121890b3 | ||
|
|
51117e8e5b | ||
|
|
fb75626458 | ||
|
|
045e880f25 | ||
|
|
0c974bd48b | ||
|
|
463da02be1 | ||
|
|
ecbed0087e | ||
|
|
8c2b310419 | ||
|
|
6b0e45441c | ||
|
|
74e1b3119f | ||
|
|
dc6bc071f1 | ||
|
|
4edc3e3f21 | ||
|
|
8280859ad3 | ||
|
|
f3d813445b | ||
|
|
520835dad0 | ||
|
|
a5805d0700 | ||
|
|
03b2a416c8 | ||
|
|
7d45812ef7 | ||
|
|
3f54799020 | ||
|
|
c0f48909a7 | ||
|
|
9b5aaa787d | ||
|
|
271b468bcb | ||
|
|
6c3d5d184e | ||
|
|
261b374924 | ||
|
|
9f789d3018 | ||
|
|
29cdfcd7a1 | ||
|
|
d103e3f7bf | ||
|
|
c5548e9375 | ||
|
|
802b812932 | ||
|
|
9c1450f4c9 | ||
|
|
4459c464ca | ||
|
|
f4c76527ee | ||
|
|
419234a23e | ||
|
|
b61dbfa16b | ||
|
|
6b2d6a22f5 | ||
|
|
c355eeffb6 | ||
|
|
c6ef6041cf | ||
|
|
dff39a6849 | ||
|
|
bb755b5c25 | ||
|
|
d1fcb43562 | ||
|
|
811a4ef248 | ||
|
|
c386ff91d6 | ||
|
|
8680ebe96f | ||
|
|
d59299b85a | ||
|
|
9210f5fb0c | ||
|
|
c2378d837a | ||
|
|
90879c964f | ||
|
|
2a525210e4 | ||
|
|
04447e0df6 | ||
|
|
55cb3747ed | ||
|
|
2bff12b376 | ||
|
|
b390cd2e3d | ||
|
|
f55b836896 | ||
|
|
982667ffa9 | ||
|
|
f00d77dec4 | ||
|
|
ea166665d3 | ||
|
|
d2aef544c3 | ||
|
|
24217d969e | ||
|
|
dde55e764b | ||
|
|
80491ea4c2 | ||
|
|
c2f9e376be | ||
|
|
20a0d95c1f | ||
|
|
fcd623e203 | ||
|
|
4b6d4c14d0 | ||
|
|
550310ea63 | ||
|
|
e7fdc23b03 | ||
|
|
a754e58fd8 | ||
|
|
17895d09a9 | ||
|
|
8201509216 | ||
|
|
f3df2b1d7a | ||
|
|
e546592c01 | ||
|
|
eaa0b5da09 | ||
|
|
ba60bfa3b0 | ||
|
|
4cd58cdacf | ||
|
|
196177021c | ||
|
|
bce2e3323a | ||
|
|
624a144880 | ||
|
|
870e7c200e | ||
|
|
f473e18bef | ||
|
|
a463bad9b5 | ||
|
|
14d46063af | ||
|
|
dca899883c | ||
|
|
b5e2234234 | ||
|
|
b507c7cdb1 | ||
|
|
51528508ba | ||
|
|
724b7675c7 | ||
|
|
1ea9f0e900 | ||
|
|
8901079084 | ||
|
|
c25a4e3285 | ||
|
|
5a3033aeb6 | ||
|
|
cd670b677a | ||
|
|
2067eab56b | ||
|
|
7c5d395736 | ||
|
|
0a06d516e2 | ||
|
|
68ccf4b13a | ||
|
|
c029e13ba2 | ||
|
|
a874d88667 | ||
|
|
3314599be7 | ||
|
|
bb64b17e45 | ||
|
|
5c99175d2e | ||
|
|
0a06f950d5 | ||
|
|
50fcb14fda | ||
|
|
73e5ed4a07 | ||
|
|
4ef4f98a66 | ||
|
|
8676eb000d | ||
|
|
3fecc023d2 | ||
|
|
afc31aea9a | ||
|
|
3070d85789 | ||
|
|
8952919f41 | ||
|
|
c67828f48d | ||
|
|
8e55be9712 | ||
|
|
2ef8ecd14a | ||
|
|
2b9066bb49 | ||
|
|
7dbd20cb15 | ||
|
|
9579c0a970 | ||
|
|
d237943a85 | ||
|
|
ca3c934ee4 | ||
|
|
55e3d86308 | ||
|
|
df5e1b51d0 | ||
|
|
599fe5f4de | ||
|
|
f7eec278c9 | ||
|
|
0ced011f0f | ||
|
|
0ab815ba9c | ||
|
|
4b191637ea | ||
|
|
02d27c1a0b | ||
|
|
edd2a1b547 | ||
|
|
4c3de76ea1 | ||
|
|
21cfa35211 | ||
|
|
b88f36a9f9 | ||
|
|
3743ef5611 | ||
|
|
f9260ca60f | ||
|
|
a368b14a97 | ||
|
|
e9746ac528 | ||
|
|
b9664141d5 | ||
|
|
f5c43d819a | ||
|
|
ff697109c3 | ||
|
|
0e6b68d3b2 | ||
|
|
21283caae6 | ||
|
|
f44912babe | ||
|
|
71be7aa1ee | ||
|
|
df11077279 | ||
|
|
d929226c69 | ||
|
|
f372e68cb8 | ||
|
|
55d8f9e01c | ||
|
|
0b6d2b4486 | ||
|
|
b249b9a444 | ||
|
|
00bd38771d | ||
|
|
99815b7c71 | ||
|
|
59c3f3859d | ||
|
|
fece3f8269 | ||
|
|
d02c4e7f8d | ||
|
|
416f7da75f | ||
|
|
d561904aa6 | ||
|
|
33834a4436 | ||
|
|
b76fbc2150 | ||
|
|
e0aeae4541 | ||
|
|
54eb2fed50 | ||
|
|
cb01381dfa | ||
|
|
98cc0fe1ce | ||
|
|
3583f57d00 | ||
|
|
de91bb941b | ||
|
|
6eb083193f | ||
|
|
d49e5f9d57 | ||
|
|
0e5325ef1c | ||
|
|
db795c7b59 | ||
|
|
bbbaa7a842 | ||
|
|
ee31edf690 | ||
|
|
22236c7ffe | ||
|
|
9c9feee7b5 | ||
|
|
4ad53eb8dd | ||
|
|
aa529e40d3 | ||
|
|
0318be5ed9 | ||
|
|
da790de60d | ||
|
|
4e0c9af18d | ||
|
|
56bdea2d8d | ||
|
|
a83646070f | ||
|
|
6f52c50adc | ||
|
|
c449bfbcf5 | ||
|
|
47c1ac89f2 | ||
|
|
6bdc863b64 | ||
|
|
9b6b27a002 | ||
|
|
b9aeb2f419 | ||
|
|
92dee61bcd | ||
|
|
f3db250fd7 | ||
|
|
9f68deefea | ||
|
|
8eefad290b | ||
|
|
0947491400 | ||
|
|
f572315e9d | ||
|
|
52c71bc740 | ||
|
|
3a80dfff39 | ||
|
|
e6482cffab | ||
|
|
971f961bcd | ||
|
|
4cb0f910ea | ||
|
|
265c9733ba | ||
|
|
a834b08603 | ||
|
|
a66136e6a4 | ||
|
|
037d196557 | ||
|
|
9118464970 | ||
|
|
c94daceb5f | ||
|
|
0a0ca219d0 | ||
|
|
e65ec8cf4b | ||
|
|
d03cafc09b | ||
|
|
3b7f167861 | ||
|
|
a951034f4b | ||
|
|
8a622fb7ca | ||
|
|
ca39892c64 | ||
|
|
bdee3392ec | ||
|
|
8ad336ad71 | ||
|
|
60a4c3de04 | ||
|
|
de64c4c80c | ||
|
|
3ca78bebde | ||
|
|
2d7a497073 | ||
|
|
788c16ce37 | ||
|
|
2de4aa2a0c | ||
|
|
10cbb7fd52 | ||
|
|
0cbbe7095d | ||
|
|
c88dafa4ad | ||
|
|
48a79fe996 | ||
|
|
b70a24f6b4 | ||
|
|
d1b82694a6 | ||
|
|
db1a941459 | ||
|
|
e22706692a | ||
|
|
c06aeae84f | ||
|
|
e0617d9ad7 | ||
|
|
8c78252ea2 | ||
|
|
29a2fb1931 | ||
|
|
e21799a754 | ||
|
|
1ad0c9c75f | ||
|
|
438ebb940e | ||
|
|
d6febb0cb4 | ||
|
|
7bc71c9cb5 | ||
|
|
519964a179 | ||
|
|
1d486938c1 | ||
|
|
ad3d01280a | ||
|
|
3216e67b41 | ||
|
|
e5665879bd | ||
|
|
61fe16e18e | ||
|
|
2184f53277 | ||
|
|
733f245e36 | ||
|
|
6c4294b64d | ||
|
|
69c1f15ce9 | ||
|
|
72389b00c9 | ||
|
|
65a6385380 | ||
|
|
2f090435a5 | ||
|
|
d9bad96e8e | ||
|
|
b7a5d95bb8 | ||
|
|
ab85e43e58 | ||
|
|
96553ad942 | ||
|
|
c1847fbc12 | ||
|
|
dc345aa363 | ||
|
|
002f794f2a | ||
|
|
d2c64c6da2 | ||
|
|
e2595e7540 | ||
|
|
aa5d14ed02 | ||
|
|
4ba7aa1fc0 | ||
|
|
fd9dc4b4e8 | ||
|
|
c57c765688 | ||
|
|
52b87efb73 | ||
|
|
36d01a2029 | ||
|
|
2592959550 | ||
|
|
6c5dd09a78 | ||
|
|
9cb9bdb515 | ||
|
|
df17759ab4 | ||
|
|
028393b229 | ||
|
|
8245a667ef | ||
|
|
6218421266 | ||
|
|
f34471ec10 | ||
|
|
40cc592591 | ||
|
|
bb96702ae6 | ||
|
|
8b091129ed | ||
|
|
edea6f3a05 | ||
|
|
c51477ac93 | ||
|
|
9f3bbfee13 | ||
|
|
7ba5a2062a | ||
|
|
00a77f8d3a | ||
|
|
b9ab9135d0 | ||
|
|
19e5a0af86 | ||
|
|
b748eb14f4 | ||
|
|
60a79cbe08 | ||
|
|
c0c4f453f2 | ||
|
|
e0672f4c4d | ||
|
|
6b540e03fc | ||
|
|
95dca53b1e | ||
|
|
91a46ea1bb | ||
|
|
903ba0c1e0 | ||
|
|
2a688200be | ||
|
|
37dff5f0a4 | ||
|
|
6397b8ca1f | ||
|
|
360028c01f | ||
|
|
6a42daffe2 | ||
|
|
7ee41d41b5 | ||
|
|
c138d0592a | ||
|
|
3db0eea921 | ||
|
|
9ce211bf09 | ||
|
|
5610ce1721 | ||
|
|
67528eeb73 | ||
|
|
880627c69c | ||
|
|
ae2cd5363f | ||
|
|
b0ecd0e9a0 | ||
|
|
23c605b149 | ||
|
|
464b8368bf | ||
|
|
cb0ea7b63e | ||
|
|
ef07388d2a | ||
|
|
8464d8c64a | ||
|
|
37a02bfe37 | ||
|
|
41f7791c87 | ||
|
|
9041ccabd3 | ||
|
|
d4a1a5b239 | ||
|
|
cf7ed8fae7 | ||
|
|
f63d43c3cf | ||
|
|
aec38614c0 | ||
|
|
459a25bedd | ||
|
|
14e024bca8 | ||
|
|
fc8985f689 | ||
|
|
cecc1a9462 | ||
|
|
fe6497dfe7 | ||
|
|
79df2b8d22 | ||
|
|
ef6a543850 | ||
|
|
c17a6956dc | ||
|
|
6775f01684 | ||
|
|
3a4754303d | ||
|
|
f9824675f1 | ||
|
|
42ae78a017 | ||
|
|
f56cbf051c | ||
|
|
ef80fb1d1a | ||
|
|
b92f22c36e | ||
|
|
fde0ba1503 | ||
|
|
c8b62755d0 | ||
|
|
7de2d6c101 | ||
|
|
8cc4e2bee7 | ||
|
|
7b0f5a195e | ||
|
|
0d8ee9ced7 | ||
|
|
838c211198 | ||
|
|
9c3baed230 | ||
|
|
435b49fa9c | ||
|
|
707df82b40 | ||
|
|
8116c6140f | ||
|
|
34543e67f7 | ||
|
|
afe5cae2a9 | ||
|
|
94a0bddb3d | ||
|
|
9786063dbb | ||
|
|
025e9d2710 | ||
|
|
a9562d361f | ||
|
|
97ad716d5a | ||
|
|
95367abc91 | ||
|
|
f7af1fa82a | ||
|
|
5321948e46 | ||
|
|
df437999ca | ||
|
|
f4b87e76a3 | ||
|
|
f0d0d60dc1 | ||
|
|
26bd08bb2b | ||
|
|
e5146c3755 | ||
|
|
f12d47752c | ||
|
|
7683402741 | ||
|
|
ba354ce65a | ||
|
|
f892a3c70a | ||
|
|
cc1dae8eed | ||
|
|
0436c3b5b7 | ||
|
|
e810b36496 | ||
|
|
50ece739d9 | ||
|
|
2d2df5c9e0 | ||
|
|
d54e9125d9 | ||
|
|
78bc42e65c | ||
|
|
186537d849 | ||
|
|
a729601dff | ||
|
|
07af792943 | ||
|
|
c14d119fe7 | ||
|
|
1c4225beff | ||
|
|
53b710ee7b | ||
|
|
04398ff909 | ||
|
|
ce77f452c7 | ||
|
|
7a855d1e0a | ||
|
|
0235d37005 | ||
|
|
5df4e7eb78 | ||
|
|
015b1dc8fd | ||
|
|
8e9e288a1d | ||
|
|
2135dfd2e5 | ||
|
|
08676a3d0b | ||
|
|
1ac3119648 | ||
|
|
39aaa2fd94 | ||
|
|
85fe74f3db | ||
|
|
7cbf350b73 | ||
|
|
b22191b789 | ||
|
|
23ba98bc94 | ||
|
|
66f8922d5b | ||
|
|
3283116518 | ||
|
|
2565af604e | ||
|
|
0d944794e4 | ||
|
|
7cc22c71a1 | ||
|
|
4d47583a94 | ||
|
|
49e788a1aa | ||
|
|
7145aa2086 | ||
|
|
d1a3ed312a | ||
|
|
2db4b67505 | ||
|
|
39091e006a | ||
|
|
229ca6cb52 | ||
|
|
2ac64a7d08 | ||
|
|
00acaa214b | ||
|
|
462faea52d | ||
|
|
d6dd95db31 | ||
|
|
a8fa68a563 | ||
|
|
931a1f3379 | ||
|
|
0952cf8178 | ||
|
|
0eab12880f | ||
|
|
39b4f9af22 | ||
|
|
1049d46a20 | ||
|
|
73e1837469 | ||
|
|
ca1ca9b451 | ||
|
|
fb30a8217c | ||
|
|
30451bc0d9 | ||
|
|
cd2e9276fb | ||
|
|
fc00e61d49 | ||
|
|
6a973f31b3 | ||
|
|
a562ce748d | ||
|
|
4462afc670 | ||
|
|
ad5e4f46d6 | ||
|
|
d48192cb0f | ||
|
|
1e85caa6c1 | ||
|
|
649e0bc53f | ||
|
|
d72a19894a | ||
|
|
11a2b55c08 | ||
|
|
9cd9958827 | ||
|
|
beb89ec657 | ||
|
|
eb47d88b33 | ||
|
|
0530b5fe1e | ||
|
|
e8eb840d32 | ||
|
|
8cf0252b07 | ||
|
|
0b79fb833e | ||
|
|
5096e4ed79 | ||
|
|
83ffd915c8 | ||
|
|
e8582ec100 | ||
|
|
6829192854 | ||
|
|
41f99f54cf | ||
|
|
3b6017495e | ||
|
|
808fdd4507 | ||
|
|
aefd2fde0a | ||
|
|
af56f59255 | ||
|
|
dfb1a204e2 | ||
|
|
b711e5c4a2 | ||
|
|
8c1056cc4f | ||
|
|
9d6b3f14a5 | ||
|
|
03217dd7ea | ||
|
|
ff9e844204 | ||
|
|
01eb099c3d | ||
|
|
2b25f2e80a | ||
|
|
6d686f03a3 | ||
|
|
de222429a1 | ||
|
|
a3cf92ecf6 | ||
|
|
37b40164ab | ||
|
|
e155191c93 | ||
|
|
b20b263ed1 | ||
|
|
8cfbf8b8bb | ||
|
|
e42f93fcce | ||
|
|
b9d1e43a8e | ||
|
|
6cbc39cbe2 | ||
|
|
09a848f524 | ||
|
|
21a9b4b03e | ||
|
|
d9623faf8c | ||
|
|
ef4699aca7 | ||
|
|
43075f741d | ||
|
|
ddd91e37db | ||
|
|
4caf2e309d | ||
|
|
0eb5a7d203 | ||
|
|
170bd65237 | ||
|
|
2739f04f1e | ||
|
|
4a8a67f6f4 | ||
|
|
4710c4193e | ||
|
|
2e5ec26be9 | ||
|
|
cfbb466f92 | ||
|
|
bc3a5ab04c | ||
|
|
db4aec22f6 | ||
|
|
d22f0d44b6 | ||
|
|
03837c0659 | ||
|
|
2eeb94765d | ||
|
|
9fef335315 | ||
|
|
17726dbcb9 | ||
|
|
10b398e8e6 | ||
|
|
2b5e34099f | ||
|
|
e05a63db9a | ||
|
|
9a980759d3 | ||
|
|
8ce02d3003 | ||
|
|
6202d0963d | ||
|
|
d41b84eb2e | ||
|
|
e7d6ac07c9 | ||
|
|
ba30577601 | ||
|
|
7cce9d5d6e | ||
|
|
b308e0275c | ||
|
|
0319acc7ca | ||
|
|
93aac14c87 | ||
|
|
ca6ee5e04f | ||
|
|
2aaf5dd2f0 | ||
|
|
10f5ecdb00 | ||
|
|
6ba76debf0 | ||
|
|
b8eca28e20 | ||
|
|
490928d474 | ||
|
|
19530f4132 | ||
|
|
2e1dce5961 | ||
|
|
d5b374c540 | ||
|
|
94ce4b7b6e | ||
|
|
dfb7cc1934 | ||
|
|
37271c746c | ||
|
|
986dc686bb | ||
|
|
bd5039ad95 | ||
|
|
37873196ec | ||
|
|
87d77d4d27 | ||
|
|
eee2e7c833 | ||
|
|
dfb92dbb4e | ||
|
|
5baf72a01e | ||
|
|
b78100355c | ||
|
|
b750843865 | ||
|
|
a69ee0cfe9 | ||
|
|
0b928e6a9b | ||
|
|
a411af2512 | ||
|
|
7843d2ee84 | ||
|
|
1d693ad220 | ||
|
|
88d61e8faa | ||
|
|
058b6bc37c | ||
|
|
8d8af7386c | ||
|
|
91ca74b46c | ||
|
|
1e186d10a8 | ||
|
|
14dea68e25 | ||
|
|
12896ed039 | ||
|
|
549fe8a465 | ||
|
|
eafe0dbe34 | ||
|
|
7598048317 | ||
|
|
17fa957a91 | ||
|
|
ae3af64c09 | ||
|
|
a02eddabb5 | ||
|
|
ca7d8699c8 | ||
|
|
3dbb5a6bfc | ||
|
|
50419f3d8c | ||
|
|
0e70188cb4 | ||
|
|
77ce9b1d58 | ||
|
|
20206048af | ||
|
|
a7cc1eee5f | ||
|
|
295ca92e44 | ||
|
|
1995fe4258 | ||
|
|
5b20fe21aa | ||
|
|
738cd1d69d | ||
|
|
57651f177b | ||
|
|
061783313a | ||
|
|
218937b175 | ||
|
|
c43357cc77 | ||
|
|
42e7a41fcc | ||
|
|
52cbb42aaf | ||
|
|
767fc3644a | ||
|
|
9a6d2d7c62 | ||
|
|
a9fac34560 | ||
|
|
6a9467451a | ||
|
|
56ffec1be7 | ||
|
|
5d43cbe67f | ||
|
|
e0e5dd3dd8 | ||
|
|
2dac682e8e | ||
|
|
f524dda88b | ||
|
|
84d0c2294c | ||
|
|
e0485dec56 | ||
|
|
fb523725f6 | ||
|
|
1dd736d9b5 | ||
|
|
600afa5c82 | ||
|
|
78f65b145a | ||
|
|
ea28e71170 | ||
|
|
9193fed393 | ||
|
|
57ee9fd18b | ||
|
|
3f1d48b1f2 | ||
|
|
7844b908de | ||
|
|
28ffff8930 | ||
|
|
21283e2e83 | ||
|
|
d4bfbc2c57 | ||
|
|
49d8a99bc4 | ||
|
|
dae2907ca3 | ||
|
|
dd45fe04ee | ||
|
|
74021c2d5a | ||
|
|
87d7d9cb5d | ||
|
|
697e377bec | ||
|
|
99906c1d0d | ||
|
|
b1937aaab2 | ||
|
|
cd449183bf | ||
|
|
dd0d29467e | ||
|
|
ff49d25963 | ||
|
|
5a1f4d9144 | ||
|
|
679e44c874 | ||
|
|
eaf127da71 | ||
|
|
628122053b | ||
|
|
8a5a71421d | ||
|
|
906365f011 | ||
|
|
bab1029c9d | ||
|
|
d263688da4 | ||
|
|
7d10edd32c | ||
|
|
a34357d222 | ||
|
|
95fa6849b3 | ||
|
|
4496a004e8 | ||
|
|
6905340c2d | ||
|
|
3ec113e8d0 | ||
|
|
bb2574ef0b | ||
|
|
cfbffe0cce | ||
|
|
991fe618b7 | ||
|
|
0538fe401b | ||
|
|
58a9bedb64 | ||
|
|
ec50cf97a9 | ||
|
|
91e99c42cd | ||
|
|
3f208c03fd | ||
|
|
203cf6e28b | ||
|
|
c850acb3b9 |
22
.codeclimate.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
engines:
|
||||||
|
pep8:
|
||||||
|
enabled: true
|
||||||
|
eslint:
|
||||||
|
enabled: true
|
||||||
|
channel: "eslint-3"
|
||||||
|
config:
|
||||||
|
config: client/.eslintrc.js
|
||||||
|
checks:
|
||||||
|
import/no-unresolved:
|
||||||
|
enabled: false
|
||||||
|
ratings:
|
||||||
|
paths:
|
||||||
|
- "redash/**/*.py"
|
||||||
|
- "client/**/*.js"
|
||||||
|
exclude_paths:
|
||||||
|
- tests/**/*.py
|
||||||
|
- migrations/**/*.py
|
||||||
|
- old_migrations/**/*.py
|
||||||
|
- setup/**/*
|
||||||
|
- bin/**/*
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
rd_ui/.tmp/
|
client/.tmp/
|
||||||
rd_ui/node_modules/
|
node_modules/
|
||||||
|
.tmp/
|
||||||
.git/
|
.git/
|
||||||
.vagrant/
|
|
||||||
|
|||||||
14
.editorconfig
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.py]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.{js,css,html}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
REDASH_STATIC_ASSETS_PATH="../rd_ui/app/"
|
|
||||||
REDASH_LOG_LEVEL="INFO"
|
|
||||||
REDASH_REDIS_URL=redis://localhost:6379/1
|
|
||||||
REDASH_DATABASE_URL="postgresql://redash"
|
|
||||||
REDASH_COOKIE_SECRET=veryverysecret
|
|
||||||
7
.gitignore
vendored
@@ -2,7 +2,7 @@
|
|||||||
.idea
|
.idea
|
||||||
*.pyc
|
*.pyc
|
||||||
.coverage
|
.coverage
|
||||||
rd_ui/dist
|
client/dist
|
||||||
.DS_Store
|
.DS_Store
|
||||||
celerybeat-schedule*
|
celerybeat-schedule*
|
||||||
.#*
|
.#*
|
||||||
@@ -20,10 +20,7 @@ venv
|
|||||||
|
|
||||||
dump.rdb
|
dump.rdb
|
||||||
|
|
||||||
# Docker related
|
|
||||||
docker-compose.yml
|
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
.tmp
|
.tmp
|
||||||
.sass-cache
|
.sass-cache
|
||||||
rd_ui/app/bower_components
|
npm-debug.log
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
ignore-paths:
|
|
||||||
- migrations
|
|
||||||
374
CHANGELOG.md
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
# Change Log
|
||||||
|
|
||||||
|
## v1.0.2 - 2017-04-18
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix: favicon wasn't showing up.
|
||||||
|
- Fix: support for unicode in dashboard tags. @deecay
|
||||||
|
- Fix: page freezes when rendering large result set.
|
||||||
|
- Fix: chart embeds were not rendering in PhantomJS.
|
||||||
|
|
||||||
|
## v1.0.1 - 2017-04-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add: bubble charts support.
|
||||||
|
- Add "Refresh Schema" button to the datasource @44px
|
||||||
|
- [Data Sources] Add: ATSD query runner @rmakulov
|
||||||
|
- [Data Sources] Add: SalesForce query runner @msnider
|
||||||
|
- Add: scheduled query backoff in case of errors @washort
|
||||||
|
- Add: use results row count as the value for the counter visualization. @deecay
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Moved CSV/Excel query results generation code to models. @akiray03
|
||||||
|
- Add support for filtered data in Pivot table visualization @deecay
|
||||||
|
- Friendlier labels for archived state of dashboard/query
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix: optimize queries to avoid N+1 queries.
|
||||||
|
- Fix: percent stacking math was wrong. @spasovski
|
||||||
|
- Fix: set query filter to match value from URL query string. @benmargo
|
||||||
|
- [Clickhouse] Fix: detection of various data types. @denisov-vlad
|
||||||
|
- Fix: user can't edit their own alert.
|
||||||
|
- Fix: angular minification issue in textbox editor and schema browser.
|
||||||
|
- Fixes to better support IE11 (add polyfill for Object.assign and show vertical scrollbar). @deecay
|
||||||
|
- Fix: datetime parameters were not using a date picker.
|
||||||
|
- Fix: Impala schema wasn't loading.
|
||||||
|
- Fix: query embed dialog close button wasn't working @r0fls
|
||||||
|
- Fix: make errors from Presto runner JSON-serializable @washort
|
||||||
|
- Fix: race condition in query task status reporting @washort
|
||||||
|
- Fix: remove $$hashKey from Pivot table
|
||||||
|
- Fix: map visualization had severe performance issue.
|
||||||
|
- Fix: pemrission dialog wasn't rendering.
|
||||||
|
- Fix: word cloud visualization didn't show column names.
|
||||||
|
- Fix: wrong timestamps in admin tasks page.
|
||||||
|
- Fix: page header wasn't updating on dashboards page @MichaelJAndy
|
||||||
|
- Fix: keyboard shortcuts didn't work in parameter inputs
|
||||||
|
|
||||||
|
## v1.0.0-rc.2 - 2017-02-22
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- [#1563](https://github.com/getredash/redash/pull/1563) Send events to webhook as JSON with a schema.
|
||||||
|
- [#1601] [Presto] friendlier error messages. (@aslotnick)
|
||||||
|
- Move the query runner unavailable log message to be DEBUG level instead of WARNING, as it was mainly confusing people.
|
||||||
|
- Remove "Send to Cloud" button from Plotly based visualizations.
|
||||||
|
- Change Plotly's default hover mode to "Compare".
|
||||||
|
- [#1612] Change: Improvements to the dashboards list page.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- [#1564] Fix: map visualization column picker wasn't populated. (@janusd)
|
||||||
|
- [#1597] [SQL Server] Fix: schema wasn't loading on case sensitive servers. (@deecay)
|
||||||
|
- Fix: dashbonard owner couldn't edit his dashboard.
|
||||||
|
- Fix: toggle_publish event wasn't logged properly.
|
||||||
|
- Fix: events with API keys were not logged.
|
||||||
|
- Fix: share dashboard dialog was broken after code minification.
|
||||||
|
- Fix: public dashboard endpoint was broken.
|
||||||
|
- Fix: public dashboard page was broken after code minification.
|
||||||
|
- Fix: visualization embed page was broken after code minification.
|
||||||
|
- Fix: schema browser has dark background.
|
||||||
|
- Fix: Google button missing on invite page.
|
||||||
|
- Fix: global parameters don't render on dashboards with text boxes.
|
||||||
|
- Fix: sunburst / Sankey visualizations have bad data.
|
||||||
|
- Fix: extra whitespace created by the filters component.
|
||||||
|
- Fix: query results cleanup task was trying to delete query objects.
|
||||||
|
- Fix: alert subscriptions were not triggered.
|
||||||
|
- [DynamoDB] Fix: count(*) queries were broken. (@kopanitsa)
|
||||||
|
- Fix: Redash is using too many database connections.
|
||||||
|
- Fix: download links were not working in dashboards.
|
||||||
|
- Fix: the first selection in multi filters was broken in dashboards.
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
- [#1555] Change sourcemaps to generate a sourcemap per module. (@44px)
|
||||||
|
- [#1570] Fix Docker Compose configuration for nginx. (@btmc)
|
||||||
|
- [#1582] Update Dockerfile to build frontend assets and update the folder ownership.
|
||||||
|
- Dockerfile: change the uid of the redash user to match host user uid.
|
||||||
|
- Update npm-shrinkwrap.json file to use http proctocol instead of git. (@deecay)
|
||||||
|
|
||||||
|
## v1.0.0-rc.1 - 2017-01-31
|
||||||
|
|
||||||
|
This version has two big changes behind the scenes:
|
||||||
|
|
||||||
|
* Refactor the frontend to use latest (at the time) Angular version (1.5) along with better frontend pipeline based on
|
||||||
|
WebPack.
|
||||||
|
* Refactor the backend code to use SQLAlchemy and Alembic, for easier migrations/upgrades.
|
||||||
|
|
||||||
|
Along with that we have many fixes, additions, new data sources (Google Analytics, ClickHouse, Amazon Athena, Snowflake)
|
||||||
|
and fixes to the existing ones (mainly ElasticSearch and Cassandra).
|
||||||
|
|
||||||
|
When upgrading make sure to upgrade from version 0.12.0 and update your .env file:
|
||||||
|
|
||||||
|
1. If you have local PostreSQL database, you will need to update the URL from `postgresql://redash` to `postgresql:///redash`.
|
||||||
|
2. Remove the `REDASH_STATIC_ASSETS_PATH` definition.
|
||||||
|
|
||||||
|
Make sure to make these changes before running upgrade as otherwise it will fail.
|
||||||
|
|
||||||
|
We're releasing a new upgrade script -- see [here](https://redash.io/help-onpremise/maintenance/how-to-upgrade-redash.html) for details.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- [#1546](https://github.com/getredash/redash/pull/1546) Add: API docstrings (@washort)
|
||||||
|
- [#1504](https://github.com/getredash/redash/pull/1504) Add: global parameters for dashboards (Tyler Rockwood)
|
||||||
|
- [#1508](https://github.com/getredash/redash/pull/1508) [Jira JQL] Add: support custom JIRA fields and enhance value mapping (@sseifert)
|
||||||
|
- [#1530](https://github.com/getredash/redash/pull/1530) Add: Docker based developer workflow (Arik Fraimovich)
|
||||||
|
- [#1515](https://github.com/getredash/redash/pull/1515) [Python] Add: get_source_schema method (Vladislav Denisov)
|
||||||
|
- [#1512](https://github.com/getredash/redash/pull/1512) [Python] Add: define more safe_builtins (Vladislav Denisov)
|
||||||
|
- [#1513](https://github.com/getredash/redash/pull/1513) Add: get_by_id & get_by_name methods for Query and DataSource classes (Vladislav Denisov)
|
||||||
|
- [#1482](https://github.com/getredash/redash/pull/1482) [Cassandra] Add: schema browser support & explicit protocol version (@yershalom)
|
||||||
|
- [#1488](https://github.com/getredash/redash/pull/1488) [Data Sources] Add: Snowflake query runner (@arikfr)
|
||||||
|
- [#1479](https://github.com/getredash/redash/pull/1479) [ElasticSearch] Add: enable schema browser (@adamlwgriffiths)
|
||||||
|
- [#1475](https://github.com/getredash/redash/pull/1475) [Cassnadra] Added set_keyspace for easier query cassandra (@yershalom)
|
||||||
|
- [#1468](https://github.com/getredash/redash/pull/1468) [Datasources] Add: Amazon Athena query runner (@arikfr)
|
||||||
|
- [#1433](https://github.com/getredash/redash/pull/1433) [Charts] Add: errors bands in graphs (@luke14free)
|
||||||
|
- [#1405](https://github.com/getredash/redash/pull/1405) [Datasources] Add: simple Google Analytics query runner (@denisov-vlad)
|
||||||
|
- [#1409](https://github.com/getredash/redash/pull/1409) [Datasources] Add: Add query runner for Yandex ClickHouse (@denisov-vlad)
|
||||||
|
- [#1373](https://github.com/getredash/redash/pull/1373) Add: rate limit the login page (@AntoineAugusti)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- [#1549](https://github.com/getredash/redash/pull/1549) Change: disable version counter for queries: (Arik Fraimovich)
|
||||||
|
- [#1548](https://github.com/getredash/redash/pull/1548) Change: improve UI in small resolution: (Arik Fraimovich)
|
||||||
|
- [#1547](https://github.com/getredash/redash/pull/1547) Change: Improve drafts UX (Arik Fraimovich)
|
||||||
|
- [#1540](https://github.com/getredash/redash/pull/1540) [MySQL] Change: faster retrieval of schema (Yaning Zhu)
|
||||||
|
- [#1517](https://github.com/getredash/redash/pull/1517) [ClickHouse] Change: convert UInt64 columns to integer type (Vladislav Denisov)
|
||||||
|
- [#1528](https://github.com/getredash/redash/pull/1528) [Vertica] Change: set longer read_timeout (lab79)
|
||||||
|
- [#1522](https://github.com/getredash/redash/pull/1522) Change: move package.json/webpack.config to root directory (Arik Fraimovich)
|
||||||
|
- [#1514](https://github.com/getredash/redash/pull/1514) [Athena] Change: enable query annotations (Gaurav Awadhwal)
|
||||||
|
- [#1525](https://github.com/getredash/redash/pull/1525) Change: update amazon linux bootstrap.sh (Karri Niemelä)
|
||||||
|
- [#1509](https://github.com/getredash/redash/pull/1509) [Presto/Athena] Change: remove special rule around public schema (@GAwadhwalAtlassian)
|
||||||
|
- [#1485](https://github.com/getredash/redash/pull/1485) Close #1453: more minimal notification of draft status for query/dashboard (@arikfr)
|
||||||
|
- [#1474](https://github.com/getredash/redash/pull/1474) [Cassandra] Change: test connection query (@yershalom)
|
||||||
|
- [#1464](https://github.com/getredash/redash/pull/1464) [Clickhouse] Change: use UTF-8 encoding for POST data (@jaykelin)
|
||||||
|
- [#1417](https://github.com/getredash/redash/pull/1417) Change: Replace Peewee with SQLAlchemy/Alembic (@arikfr, @washort)
|
||||||
|
- [#1458](https://github.com/getredash/redash/pull/1458) Change: switch from flask_script to click, add CLI unit tests and upgrade Flask version (@washort)
|
||||||
|
- [#1438](https://github.com/getredash/redash/pull/1438) [ElasticSearch] Change: use simplejson for better error descriptions (@adamlwgriffiths)
|
||||||
|
- [#1435](https://github.com/getredash/redash/pull/1435) Whitelisting more builtin primitives (@mattrobenolt)
|
||||||
|
- [#1376](https://github.com/getredash/redash/pull/1376) Change: upgrade the frontend stack (@arikfr, @luke14free)
|
||||||
|
- [#1429](https://github.com/getredash/redash/pull/1429) Add missing error check from #1402 (@adamlwgriffiths)
|
||||||
|
- [#1256](https://github.com/getredash/redash/pull/1256) Change: when forking a query, copy all visualizations (@ninneko)
|
||||||
|
- [#1421](https://github.com/getredash/redash/pull/1421) Change: [BigQuery] only specify useLegacySQL is it's True (@arikfr)
|
||||||
|
- [#1353](https://github.com/getredash/redash/pull/1353) Change: make draft status for queries and dashboards toggleable (@washort)
|
||||||
|
- [#1419](https://github.com/getredash/redash/pull/1419) Change: use redash.utils.json_dumps instead of json.dumps in Python query runner (@ehfeng)
|
||||||
|
- [#1402](https://github.com/getredash/redash/pull/1402) Change: correctly propagate ElasticSearch errors to the UI (@adamlwgriffiths)
|
||||||
|
- [#1371](https://github.com/getredash/redash/pull/1371) Change: display user's password reset link to the admin when mail server disabled (@vitorbaptista)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- [#1551](https://github.com/getredash/redash/pull/1551) Fix: flask-admin - exclude created_at/updated_at so models can be saved (Arik Fraimovich)
|
||||||
|
- [#1545](https://github.com/getredash/redash/pull/1545) [ElasticSearch] Fix: query fails when properties key is missing (hgs847825)
|
||||||
|
- [#1526](https://github.com/getredash/redash/pull/1526) [ElasticSearch] Fix for #1521 (Adam Griffiths)
|
||||||
|
- [#1521](https://github.com/getredash/redash/pull/1521) [ElasticSearch] Fix: wrong variable name. (Arik Fraimovich)
|
||||||
|
- [#1497](https://github.com/getredash/redash/pull/1497) Fix #16: when updating dashboard name refresh dashboards dropdown (@arikfr)
|
||||||
|
- [#1491](https://github.com/getredash/redash/pull/1491) Fix: DynamoDB test connection was broken (@arikfr)
|
||||||
|
- [#1487](https://github.com/getredash/redash/pull/1487) Fix #1432: delete visualization sends full visualization body instead… (@arikfr)
|
||||||
|
- [#1484](https://github.com/getredash/redash/pull/1484) Fix #1457: sort was using the string value (@arikfr)
|
||||||
|
- [#1478](https://github.com/getredash/redash/pull/1478) [ElasticSearch] Fix: connection test was always succesfful (@adamlwgriffiths)
|
||||||
|
- [#1440](https://github.com/getredash/redash/pull/1440) Fix: API errors for dashboards with invalid layout data (@whummer)
|
||||||
|
- [#1427](https://github.com/getredash/redash/pull/1427) [Cassandra] Fix: remove reference to non existing Error class (@arikfr)
|
||||||
|
- [#1423](https://github.com/getredash/redash/pull/1423) [Cassandra] Fix: cassandra.cluster.Error wasn't imported (@arikfr)
|
||||||
|
- Fix #1001: queries with a column named "length" were not rendered.
|
||||||
|
- Fix #578: dashboard list not scrollable.
|
||||||
|
- Fix #137: add direction indicators when sorting query results.
|
||||||
|
|
||||||
|
## v0.12.0 - 2016-11-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
61fe16e #1374: Add: allow '*' in REDASH_CORS_ACCESS_CONTROL_ALLOW_ORIGIN (Allen Short)
|
||||||
|
2f09043 #1113: Add: share modify/access permissions for queries and dashboard (whummer)
|
||||||
|
3db0eea #1341: Add: support for specifying SAML nameid-format (zoetrope)
|
||||||
|
b0ecd0e #1343: Add: support for local SAML metadata file (zoetrope)
|
||||||
|
0235d37 #1335: Add: allow changing alert email subject. (Arik Fraimovich)
|
||||||
|
2135dfd #1333: Add: control over y axis min/max values (Arik Fraimovich)
|
||||||
|
49e788a #1328: Add: support for snapshot generation service (Arik Fraimovich)
|
||||||
|
229ca6c #1323: Add: collect runtime metrics for Celery tasks (Arik Fraimovich)
|
||||||
|
931a1f3 #1315: Add: support for loading BigQuery schema (Arik Fraimovich)
|
||||||
|
39b4f9a #1314: Add: support MongoDB SSL connections (Arik Fraimovich)
|
||||||
|
ca1ca9b #1312: Add: additional configuration for Celery jobs (Arik Fraimovich)
|
||||||
|
fc00e61 #1310: Add: support for date/time with seconds parameters (Arik Fraimovich)
|
||||||
|
d72a198 #1307: Add: API to force refresh data source schema (Arik Fraimovich)
|
||||||
|
beb89ec #1305: Add: UI to edit dashboard text box widget (Kazuhito Hokamura)
|
||||||
|
808fdd4 #1298: Add: JIRA (JQL) query runner (Arik Fraimovich)
|
||||||
|
ff9e844 #1280: Add: configuration flag to disable scheduled queries (Hirotaka Suzuki)
|
||||||
|
ef4699a #1269: Add: Google Drive federated tables support in BigQuery query runner (Kurt Gooden)
|
||||||
|
2eeb947 #1236: Add: query runner for Cassandra and ScyllaDB (syerushalmy)
|
||||||
|
10b398e #1249: Add: override slack webhook parameters (mystelynx)
|
||||||
|
2b5e340 #1252: Add: Schema loading support for Presto query runner (using information_schema) (Rohan Dhupelia)
|
||||||
|
2aaf5dd #1250: Add: query snippets feature (Arik Fraimovich)
|
||||||
|
8d8af73 #1226: Add: Sankey visualization (Arik Fraimovich)
|
||||||
|
a02edda #1222: Add: additional results format for sunburst visualization (Arik Fraimovich)
|
||||||
|
0e70188 #1213: Add: new sunburst sequence visualization (Arik Fraimovich)
|
||||||
|
9a6d2d7 #1204: Add: show views in schema browser for Vertica data sources (Matthew Carter)
|
||||||
|
600afa5 #1138: Add: ability to register user defined function (UDF) resources for BigQuery DataSource/Query (fabito)
|
||||||
|
b410410 #1166: Add: "every 14 days" refresh option (Arik Fraimovich)
|
||||||
|
906365f #967: Add: extend ElasticSearch query_runner to support aggregations (lloydw)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
2de4aa2 #1395: Change: switch to requests in URL query runner (Arik Fraimovich)
|
||||||
|
db1a941 #1392: Change: Update documentation links to point at the new location. (Arik Fraimovich)
|
||||||
|
002f794 #1368: Change: added ability to disable auto update in admin views (Arik Fraimovich)
|
||||||
|
aa5d14e #1366: Change: improve error message for exception in the Python query runner (deecay)
|
||||||
|
880627c #1355: Change: pass the user object to the run_query method (Arik Fraimovich)
|
||||||
|
23c605b #1342: SAML: specify entity id (zoetrope)
|
||||||
|
015b1dc #1334: Change: allow specifying recipient address when sending email test message (Arik Fraimovich)
|
||||||
|
39aaa2f #1292: Change: improvements to map visualization (Arik Fraimovich)
|
||||||
|
b22191b #1332: Change: upgrade Python packages (Arik Fraimovich)
|
||||||
|
23ba98b #1331: Celery: Upgrade Celery to more recent version. (Arik Fraimovich)
|
||||||
|
3283116 #1330: Change: upgrade Requests to latest version. (Arik Fraimovich)
|
||||||
|
39091e0 #1324: Change: add more logging and information for refresh schemas task (Arik Fraimovich)
|
||||||
|
462faea #1316: Change: remove deprecated settings (Arik Fraimovich)
|
||||||
|
73e1837 #1313: Change: more flexible column width calculation (Arik Fraimovich)
|
||||||
|
e8eb840 #1279: Change: update bootstrap.sh to support Ubuntu 16.04 (IllusiveMilkman)
|
||||||
|
8cf0252 #1262: Change: upgrade Plot.ly version and switch to smaller build (Arik Fraimovich)
|
||||||
|
0b79fb8 #1306: Change: paginate queries page & add explicit urls. (Arik Fraimovich)
|
||||||
|
41f99f5 #1299: Change: send Content-Type header (application/json) in query results responses (Tsuyoshi Tatsukawa)
|
||||||
|
dfb1a20 #1297: Change: update Slack configuration titles. (Arik Fraimovich)
|
||||||
|
8c1056c #1294: Change: don't annotate BigQuery queries (Arik Fraimovich)
|
||||||
|
a3cf92e #1289: Change: use key_as_string when available (ElasticSearch query runner) (Arik Fraimovich)
|
||||||
|
e155191 #1285: Change: do not display Oracle tablespace name in schema browser (Matthew Carter)
|
||||||
|
6cbc39c #1282: Change: deduplicate Google Spreadsheet columns (Arik Fraimovich)
|
||||||
|
4caf2e3 #1277: Set specific version of cryptography lib (Arik Fraimovich)
|
||||||
|
d22f0d4 #1216: Change: bootstrap.sh - use non interactive dist-upgrade (Atsushi Sasaki)
|
||||||
|
19530f4 #1245: Change: switch from CodeMirror to Ace editor (Arik Fraimovich)
|
||||||
|
dfb92db #1234: Change: MongoDB query runner set DB name as mandatory (Arik Fraimovich)
|
||||||
|
b750843 #1230: Change: annotate Presto queries with metadata (Noriaki Katayama)
|
||||||
|
5b20fe2 #1217: Change: install libffi-dev for Cryptography (Ubuntu setup script) (Atsushi Sasaki)
|
||||||
|
a9fac34 #1206: Change: update pymssql version to 2.1.3 (kitsuyui)
|
||||||
|
5d43cbe #1198: Change: add support for Standard SQL in BigQuery query runner (mystelynx)
|
||||||
|
84d0c22 #1193: Change: modify the argument order of moment.add function call (Kenya Yamaguchi)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
d6febb0 #1375: Fix: Download Dataset does not work when not logged in (Joshua Dechant)
|
||||||
|
96553ad #1369: Fix: missing format call in Elasticsearch test method (Adam Griffiths)
|
||||||
|
c57c765 #1365: Fix: compare retrieval times in UTC timezone (Allen Short)
|
||||||
|
37dff5f #1360: Fix: connection test was broken for MySQL (ichihara)
|
||||||
|
360028c #1359: Fix: schema loading query for Hive was wrong for non default schema (laughingman7743)
|
||||||
|
7ee41d4 #1358: Fix: make sure all calls to run_query updated with new parameter (Arik Fraimovich)
|
||||||
|
0d94479 #1329: Fix: Redis memory leak. (Arik Fraimovich)
|
||||||
|
7145aa2 #1325: Fix: queries API was doing N+1 queries in most cases (Arik Fraimovich)
|
||||||
|
cd2e927 #1311: Fix: BoxPlot visualization wasn't rendering on a dashboard (Arik Fraimovich)
|
||||||
|
a562ce7 #1309: Fix: properly render checkboxes in dynamic forms (Arik Fraimovich)
|
||||||
|
d48192c #1308: Fix: support for Unicode columns name in Google Spreadsheets (Arik Fraimovich)
|
||||||
|
e42f93f #1283: Fix: schema browser was unstable after opening a table (Arik Fraimovich)
|
||||||
|
170bd65 #1272: Fix: TreasureData get_schema method was returning array instead of string as column name (ariarijp)
|
||||||
|
4710c41 #1265: Fix: refresh modal not working for unsaved query (Arik Fraimovich)
|
||||||
|
bc3a5ab #1264: Fix: dashboard refresh not working (Arik Fraimovich)
|
||||||
|
6202d09 #1240: Fix: when shared dashboard token not found, return 404 (Wesley Batista)
|
||||||
|
93aac14 #1251: Fix: autocomplete went crazy when database has no autocomplete. (Arik Fraimovich)
|
||||||
|
b8eca28 #1246: Fix: support large schemas in schema browser (Arik Fraimovich)
|
||||||
|
b781003 #1223: Fix: Alert: when hipchat Alert.name is multibyte character, occur error. (toyama0919)
|
||||||
|
0b928e6 #1227: Fix: Bower install fails in vagrant (Kazuhito Hokamura)
|
||||||
|
a411af2 #1232: Fix: don't show warning when query string (parameters value) changes (Kazuhito Hokamura)
|
||||||
|
3dbb5a6 #1221: Fix: sunburst didn't handle all cases of path lengths (Arik Fraimovich)
|
||||||
|
a7cc1ee #1218: Fix: updated result not being saved when changing query text. (Arik Fraimovich)
|
||||||
|
0617833 #1215: Fix: email alerts not working (Arik Fraimovich)
|
||||||
|
78f65b1 #1187: Fix: read only users receive the permission error modal in query view (Arik Fraimovich)
|
||||||
|
bba801f #1167: Fix the version of setuptools on bootstrap script for Ubuntu (Takuya Arita)
|
||||||
|
ce81d69 #1160: Fix indentation in docker-compose-example.yml (Hirofumi Wakasugi)
|
||||||
|
dd759fe #1155: Fix: make all configuration values of Oracle required (Arik Fraimovich)
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
a69ee0c #1225: Fix: RST formatting of the Vagrant documentation (Kazuhito Hokamura)
|
||||||
|
03837c0 #1242: Docs: add warning re. quotes on column names and BigQuery (Ereli)
|
||||||
|
9a98075 #1255: Docs: add documentation for InfluxDB (vishesh92)
|
||||||
|
e0485de #1195: Docs: fix typo in maintenance page title (Antoine Augusti)
|
||||||
|
7681d3e #1164: Docs: update permission documentation (Daniel Darabos)
|
||||||
|
bcd3670 #1156: Docs: add SSL parameters to nginx configuration (Josh Cox)
|
||||||
|
|
||||||
|
## v0.11.1.b2095 - 2016-08-02
|
||||||
|
|
||||||
|
This is a hotfix release, which fixes an issue with email alerts in v0.11.0.
|
||||||
|
|
||||||
|
## v0.11.0.b2016 - 2016-07-03
|
||||||
|
|
||||||
|
The main features of this release are:
|
||||||
|
|
||||||
|
- Alert Destinations: ability to define multiple destinations for alert notifications (currently implemented: HipChat, Slack, Webhook and email).
|
||||||
|
- The long-awaited UI for query parameters (see example in #1069).
|
||||||
|
|
||||||
|
Also, this release includes numerous smaller features, improvements, and bug fixes.
|
||||||
|
|
||||||
|
A big thank you goes to all who contributed code and documentation in this release: @AntoineAugusti, @James226, @adamlwgriffiths, @alexdebrie, @anthony-coble, @ariarijp, @dheerajrav, @edwardsharp, @machira, @nabilblk, @ninneko, @ordd, @tomerben, @toru-takahashi, @vishesh92, @vorakumar and @whummer.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
d5e5b24 #1136: Feature: add --org option to all relevant CLI commands. (@adamlwgriffiths)
|
||||||
|
87e25f2 #1129: Feature: support for JSON query formatting (Mongo, ElasticSearch) (@arikfr)
|
||||||
|
6bb2716 #1121: Show error when failing to communicate with server (@arikfr)
|
||||||
|
f21276e #1119: Feature: add UI to delete alerts (@arikfr)
|
||||||
|
8656540 #1069: Feature: UI for query parameters (@arikfr)
|
||||||
|
790128c #1067: Feature: word cloud visualization (@anthony-coble)
|
||||||
|
8b73a2b #1098: Feature: UI for alert destinations & new destination types (@alexdebrie)
|
||||||
|
1fbeb5d #1092: Add Heroku support (@adamlwgriffiths)
|
||||||
|
f64622d #1089: Add support for serialising UUID type within MSSQL #961 (@James226)
|
||||||
|
857caab #1085: Feature: API to pause a data source (@arikfr)
|
||||||
|
214aa3b #1060: Feature: support configuring user's groups with SAML (@vorakumar)
|
||||||
|
e20a005 #1007: Issue#1006: Make bottom margin editable for Chart visualization (@vorakumar)
|
||||||
|
6e0dd2b #1063: Add support for date/time Y axis (@tomerben)
|
||||||
|
b5a4a6b #979: Feature: Add CLI to edit group permissions (@ninneko)
|
||||||
|
6d495d2 #1014: Add server-side parameter handling for embeds (@whummer)
|
||||||
|
5255804 #1091: Add caching for queries used in embeds (@whummer)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
0314313 #1149: Presto QueryRunner supports tinyint and smallint (@toru-takahashi)
|
||||||
|
8fa6fdb #1030: Make sure data sources list ordered by id (@arikfr)
|
||||||
|
8df822e #1141: Make create data source button more prominent (@arikfr)
|
||||||
|
96dd811 #1127: Mark basic_auth_password as secret (@adamlwgriffiths)
|
||||||
|
ad65391 #1130: Improve Slack notification style (@AntoineAugusti)
|
||||||
|
df637e3 #1116: Return meaningful error when there is no cached result. (@arikfr)
|
||||||
|
65635ec #1102: Switch to HipChat V2 API (@arikfr)
|
||||||
|
14fcf01 #1072: Remove counter from the tasks Done tab (as it always shows 50). #1047 (@arikfr)
|
||||||
|
1a1160e #1062: DynamoDB: Better exception handling (@arikfr)
|
||||||
|
ed45dcb #1044: Improve vagrant flow (@staritza)
|
||||||
|
8b5dc8e #1036: Add optional block for more scripts in template (@arikfr)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
dbd48e1 #1143: Fix: use the email input type where needed (@ariarijp)
|
||||||
|
7445972 #1142: Fix: dates in filters might be duplicated (@arikfr)
|
||||||
|
5d0ed02 #1140: Fix: Hive should use the enabled variable (@arikfr)
|
||||||
|
392627d #1139: Fix: Impala data source referencing wrong variable (@arikfr)
|
||||||
|
c5bfbba #1133: Fix: query scrolling issues (@vishesh92)
|
||||||
|
c01d266 #1128: Fix: visualization options not updating after changing type (@arikfr)
|
||||||
|
6bc0e7a #1126: Fix #669: save fails when doing partial save of new query (@arikfr)
|
||||||
|
3ce27b9 #1118: Fix: remove alerts for archived queries (@arikfr)
|
||||||
|
4fabaae #1117: Fix #1052: filter not working for date/time values (@arikfr)
|
||||||
|
c107c94 #1077: Fix: install needed dependencies to use Hive in Docker image (@nabilblk)
|
||||||
|
abc790c #1115: Fix: allow non integers in alert reference value (@arikfr)
|
||||||
|
4ec473c #1110: Fix #1109: mixed group permissions resulting in wrong permission (@arikfr)
|
||||||
|
1ca5262 #1099: Fix RST syntax for links (@adamlwgriffiths)
|
||||||
|
daa6c1c #1096: Fix typo in env variable VERSION_CHECK (@AntoineAugusti)
|
||||||
|
cd06d27 #1095: Fix: use create_query permission for new query button. (@ordd)
|
||||||
|
2bc0b27 #1061: Fix: area chart stacking doesn't work (@machira)
|
||||||
|
8c21e91 #1108: Remove potnetially concurrency not safe code form enqueue_query (@arikfr)
|
||||||
|
e831218 #1084: Fix #1049: duplicate alerts when data source belongs to multiple groups (@arikfr)
|
||||||
|
6edb0ca #1080: Fix typo (@jeffwidman)
|
||||||
|
64d7538 #1074: Fix: ElasticSearch wasn't using correct type names (@toyama0919)
|
||||||
|
3f90dd9 #1064: Fix: old task trackers were not really removed (@arikfr)
|
||||||
|
e10ecd2 #1058: Bring back filters if dashboard filters are enabled (@AntoineAugusti)
|
||||||
|
701035f #1059: Fix: DynamoDB having issues when setting host (@arikfr)
|
||||||
|
2924d4f #1040: Small fixes to visualizations view (@arikfr)
|
||||||
|
fec0d5f #1037: Fix: multi filter wasn't working with __ syntax (@dheerajrav)
|
||||||
|
b066ce4 #1033: Fix: only ask for notification permissions if wasn't denied (@arikfr)
|
||||||
|
960c416 #1032: Fix: make sure we return dashboards only for current org only (@arikfr)
|
||||||
|
b3844d3 #1029: Hive: close connection only if it exists (@arikfr)
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
6bb09d8 #1146: Docs: add a link to settings documentation. (@adamlwgriffiths)
|
||||||
|
095e759 #1103: Docs: add section about monitoring (@AntoineAugusti)
|
||||||
|
e942486 #1090: Contributing Guide (@arikfr)
|
||||||
|
3037c4f #1066: Docs: command type-o fix. (@edwardsharp)
|
||||||
|
2ee0065 #1038: Add an ISSUE_TEMPLATE.md to direct people at the forum (@arikfr)
|
||||||
|
f7322a4 #1021: Vagrant docs: add purging the cache step (@ariarijp)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For older releases check the GitHub releases page:
|
||||||
|
https://github.com/getredash/redash/releases
|
||||||
@@ -9,7 +9,7 @@ The following is a set of guidelines for contributing to Redash. These are guide
|
|||||||
- [Feature Roadmap](https://trello.com/b/b2LUHU7A/re-dash-roadmap)
|
- [Feature Roadmap](https://trello.com/b/b2LUHU7A/re-dash-roadmap)
|
||||||
- [Feature Requests](https://discuss.redash.io/c/feature-requests)
|
- [Feature Requests](https://discuss.redash.io/c/feature-requests)
|
||||||
- [Gitter Chat](https://gitter.im/getredash/redash) or [Slack](https://slack.redash.io)
|
- [Gitter Chat](https://gitter.im/getredash/redash) or [Slack](https://slack.redash.io)
|
||||||
- [Documentation](http://docs.redash.io)
|
- [Documentation](https://redash.io/help/)
|
||||||
- [Blog](http://blog.redash.io/)
|
- [Blog](http://blog.redash.io/)
|
||||||
- [Twitter](https://twitter.com/getredash)
|
- [Twitter](https://twitter.com/getredash)
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ The following is a set of guidelines for contributing to Redash. These are guide
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
[How can I contribute?](#how-can-i-contribute)
|
[How can I contribute?](#how-can-i-contribute)
|
||||||
@@ -28,12 +28,12 @@ The following is a set of guidelines for contributing to Redash. These are guide
|
|||||||
- [Pull Requests](#pull-requests)
|
- [Pull Requests](#pull-requests)
|
||||||
- [Documentation](#documentation)
|
- [Documentation](#documentation)
|
||||||
- Design?
|
- Design?
|
||||||
|
|
||||||
[Addtional Notes](#additional-notes)
|
[Addtional Notes](#additional-notes)
|
||||||
|
|
||||||
- [Release Method](#release-method)
|
- [Release Method](#release-method)
|
||||||
- [Code of Conduct](#code-of-conduct)
|
- [Code of Conduct](#code-of-conduct)
|
||||||
|
|
||||||
## How can I contribute?
|
## How can I contribute?
|
||||||
|
|
||||||
### Reporting Bugs
|
### Reporting Bugs
|
||||||
@@ -43,26 +43,24 @@ When creating a new bug report, please make sure to:
|
|||||||
- Search for existing issues first. If you find a previous report of your issue, please update the existing issue with additional information instead of creating a new one.
|
- Search for existing issues first. If you find a previous report of your issue, please update the existing issue with additional information instead of creating a new one.
|
||||||
- If you are not sure if your issue is really a bug or just some configuration/setup problem, please start a discussion in [the support forum](https://discuss.redash.io/c/support) first. Unless you can provide clear steps to reproduce, it's probably better to start with a thread in the forum and later to open an issue.
|
- If you are not sure if your issue is really a bug or just some configuration/setup problem, please start a discussion in [the support forum](https://discuss.redash.io/c/support) first. Unless you can provide clear steps to reproduce, it's probably better to start with a thread in the forum and later to open an issue.
|
||||||
- If you still decide to open an issue, please review the template and guidelines and include as much details as possible.
|
- If you still decide to open an issue, please review the template and guidelines and include as much details as possible.
|
||||||
|
|
||||||
### Suggesting Enhancements / Feature Requests
|
### Suggesting Enhancements / Feature Requests
|
||||||
|
|
||||||
If you would like to suggest an enchancement or ask for a new feature:
|
If you would like to suggest an enchancement or ask for a new feature:
|
||||||
|
|
||||||
- Please check [the roadmap](https://trello.com/b/b2LUHU7A/re-dash-roadmap) for existing Trello card for what you want to suggest/ask. If there is, feel free to upvote it to signal interest or add your comments.
|
- Please check [the roadmap](https://trello.com/b/b2LUHU7A/re-dash-roadmap) for existing Trello card for what you want to suggest/ask. If there is, feel free to upvote it to signal interest or add your comments.
|
||||||
- If there is no existing card, open a thread in [the forum](https://discuss.redash.io/c/feature-requests) to start a discussion about what you want to suggest. Try to provide as much details and context as possible and include information about *the problem you want to solve* rather only *your proposed solution*.
|
- If there is no existing card, open a thread in [the forum](https://discuss.redash.io/c/feature-requests) to start a discussion about what you want to suggest. Try to provide as much details and context as possible and include information about *the problem you want to solve* rather only *your proposed solution*.
|
||||||
|
|
||||||
### Pull Requests
|
### Pull Requests
|
||||||
|
|
||||||
- **Code contributions are welcomed**. For big changes or significant features, it's usually better to reach out first and discuss what you want to implement and how (we recommend reading: [Pull Request First](https://medium.com/practical-blend/pull-request-first-f6bb667a9b6#.ozlqxvj36)). This to make sure that what you want to implement is aligned with our goals for the project and that no one else is already working on it.
|
- **Code contributions are welcomed**. For big changes or significant features, it's usually better to reach out first and discuss what you want to implement and how (we recommend reading: [Pull Request First](https://medium.com/practical-blend/pull-request-first-f6bb667a9b6#.ozlqxvj36)). This to make sure that what you want to implement is aligned with our goals for the project and that no one else is already working on it.
|
||||||
- Include screenshots and animated GIFs in your pull request whenever possible.
|
- Include screenshots and animated GIFs in your pull request whenever possible.
|
||||||
- Please add [documentation](#documentation) for new features or changes in functionality along with the code.
|
- Please add [documentation](#documentation) for new features or changes in functionality along with the code.
|
||||||
- Please follow existing code style. We use PEP8 for Python and sensible style for Javascript.
|
- Please follow existing code style. We use PEP8 for Python and sensible style for Javascript.
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
The project's documentation can be found at [docs.redash.io](http://docs.redash.io/). The [documentation sources](https://github.com/getredash/redash/tree/master/docs) are managed along with the code and to contribute edits / new pages, you can use GitHub's interface. Click the "Edit on GitHub" link on the documentation page to quickly open the edit interface.
|
The project's documentation can be found at [https://redash.io/help/](https://redash.io/help/). The [documentation sources](https://github.com/getredash/website/tree/master/user-guide) are hosted on GitHub. To contribute edits / new pages, you can use GitHub's interface. Click the "Edit on GitHub" link on the documentation page to quickly open the edit interface.
|
||||||
|
|
||||||
The pages are written in *reStructuredText* format, which is very similar to Markdown.
|
|
||||||
|
|
||||||
## Additional Notes
|
## Additional Notes
|
||||||
|
|
||||||
|
|||||||
60
Dockerfile
@@ -1,53 +1,13 @@
|
|||||||
FROM ubuntu:trusty
|
FROM redash/base:latest
|
||||||
|
|
||||||
# Ubuntu packages
|
# We first copy only the requirements file, to avoid rebuilding on every file
|
||||||
RUN apt-get update && \
|
# change.
|
||||||
apt-get install -y python-pip python-dev curl build-essential pwgen libffi-dev sudo git-core wget \
|
COPY requirements.txt requirements_dev.txt requirements_all_ds.txt ./
|
||||||
# Postgres client
|
RUN pip install -r requirements.txt -r requirements_dev.txt -r requirements_all_ds.txt
|
||||||
libpq-dev \
|
|
||||||
# Additional packages required for data sources:
|
|
||||||
libssl-dev libmysqlclient-dev freetds-dev libsasl2-dev && \
|
|
||||||
apt-get clean && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Users creation
|
COPY . ./
|
||||||
RUN useradd --system --comment " " --create-home redash
|
RUN npm install && npm run build && rm -rf node_modules
|
||||||
|
RUN chown -R redash /app
|
||||||
|
USER redash
|
||||||
|
|
||||||
# Pip requirements for all data source types
|
ENTRYPOINT ["/app/bin/docker-entrypoint"]
|
||||||
RUN pip install -U setuptools==23.1.0 && \
|
|
||||||
pip install supervisor==3.1.2
|
|
||||||
|
|
||||||
COPY . /opt/redash/current
|
|
||||||
RUN chown -R redash /opt/redash/current
|
|
||||||
|
|
||||||
# Setting working directory
|
|
||||||
WORKDIR /opt/redash/current
|
|
||||||
|
|
||||||
ENV REDASH_STATIC_ASSETS_PATH="../rd_ui/dist/"
|
|
||||||
|
|
||||||
# Install project specific dependencies
|
|
||||||
RUN pip install -r requirements_all_ds.txt && \
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
RUN curl https://deb.nodesource.com/setup_4.x | bash - && \
|
|
||||||
apt-get install -y nodejs && \
|
|
||||||
sudo -u redash -H make deps && \
|
|
||||||
rm -rf node_modules rd_ui/node_modules /home/redash/.npm /home/redash/.cache && \
|
|
||||||
apt-get purge -y nodejs && \
|
|
||||||
apt-get clean && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Setup supervisord
|
|
||||||
RUN mkdir -p /opt/redash/supervisord && \
|
|
||||||
mkdir -p /opt/redash/logs && \
|
|
||||||
cp /opt/redash/current/setup/docker/supervisord/supervisord.conf /opt/redash/supervisord/supervisord.conf
|
|
||||||
|
|
||||||
# Fix permissions
|
|
||||||
RUN chown -R redash /opt/redash
|
|
||||||
|
|
||||||
# Expose ports
|
|
||||||
EXPOSE 5000
|
|
||||||
EXPOSE 9001
|
|
||||||
|
|
||||||
# Startup script
|
|
||||||
CMD ["supervisord", "-c", "/opt/redash/supervisord/supervisord.conf"]
|
|
||||||
|
|||||||
2
LICENSE
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2013-2016, Arik Fraimovich.
|
Copyright (c) 2013-2017, Arik Fraimovich.
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification,
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
|||||||
8
Makefile
@@ -6,17 +6,15 @@ BASE_VERSION=$(shell python ./manage.py version | cut -d + -f 1)
|
|||||||
FILENAME=$(CIRCLE_ARTIFACTS)/$(NAME).$(VERSION).tar.gz
|
FILENAME=$(CIRCLE_ARTIFACTS)/$(NAME).$(VERSION).tar.gz
|
||||||
|
|
||||||
deps:
|
deps:
|
||||||
if [ -d "./rd_ui/app" ]; then npm install; fi
|
if [ -d "./client/app" ]; then npm install; fi
|
||||||
if [ -d "./rd_ui/app" ]; then npm run bower install; fi
|
if [ -d "./client/app" ]; then npm run build; fi
|
||||||
if [ -d "./rd_ui/app" ]; then npm run build; fi
|
|
||||||
|
|
||||||
pack:
|
pack:
|
||||||
sed -ri "s/^__version__ = '([0-9.]*)'/__version__ = '$(FULL_VERSION)'/" redash/__init__.py
|
sed -ri "s/^__version__ = '([0-9.]*)'/__version__ = '$(FULL_VERSION)'/" redash/__init__.py
|
||||||
tar -zcv -f $(FILENAME) --exclude="optipng*" --exclude=".git*" --exclude="*.pyc" --exclude="*.pyo" --exclude="venv" --exclude="node_modules" --exclude="rd_ui/dist/bower_components" --exclude="rd_ui/app" *
|
tar -zcv -f $(FILENAME) --exclude="optipng*" --exclude=".git*" --exclude="*.pyc" --exclude="*.pyo" --exclude="venv" --exclude="node_modules" *
|
||||||
|
|
||||||
upload:
|
upload:
|
||||||
python bin/release_manager.py $(CIRCLE_SHA1) $(BASE_VERSION) $(FILENAME)
|
python bin/release_manager.py $(CIRCLE_SHA1) $(BASE_VERSION) $(FILENAME)
|
||||||
|
|
||||||
test:
|
test:
|
||||||
nosetests --with-coverage --cover-package=redash tests/
|
nosetests --with-coverage --cover-package=redash tests/
|
||||||
#grunt test
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
web: ./manage.py runserver -p $PORT --host 0.0.0.0
|
|
||||||
worker: ./bin/run celery worker --app=redash.worker --beat -Qqueries,celery,scheduled_queries
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
web: ./manage.py runserver -d -r -p $PORT --host 0.0.0.0
|
|
||||||
worker: celery worker --app=redash.worker -c2 --beat -Q queries,celery,scheduled_queries
|
|
||||||
30
README.md
@@ -1,42 +1,36 @@
|
|||||||
More details about the future of re:dash : http://bit.ly/journey-first-step
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img title="re:dash" src='http://redash.io/static/old_img/redash_logo.png' width="200px"/>
|
<img title="Redash" src='https://redash.io/assets/images/logo.png' width="200px"/>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img title="Build Status" src='https://circleci.com/gh/getredash/redash.png?circle-token=8a695aa5ec2cbfa89b48c275aea298318016f040'/>
|
<img title="Build Status" src='https://circleci.com/gh/getredash/redash.png?circle-token=8a695aa5ec2cbfa89b48c275aea298318016f040'/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://gitter.im/getredash/redash?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
[](https://gitter.im/getredash/redash?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
[](http://docs.redash.io)
|
[](https://redash.io/help/)
|
||||||
|
|
||||||
**_re:dash_** is our take on freeing the data within our company in a way that will better fit our culture and usage patterns.
|
**_Redash_** is our take on freeing the data within our company in a way that will better fit our culture and usage patterns.
|
||||||
|
|
||||||
Prior to **_re:dash_**, we tried to use traditional BI suites and discovered a set of bloated, technically challenged and slow tools/flows. What we were looking for was a more hacker'ish way to look at data, so we built one.
|
Prior to **_Redash_**, we tried to use traditional BI suites and discovered a set of bloated, technically challenged and slow tools/flows. What we were looking for was a more hacker'ish way to look at data, so we built one.
|
||||||
|
|
||||||
**_re:dash_** was built to allow fast and easy access to billions of records, that we process and collect using Amazon Redshift ("petabyte scale data warehouse" that "speaks" PostgreSQL).
|
**_Redash_** was built to allow fast and easy access to billions of records, that we process and collect using Amazon Redshift ("petabyte scale data warehouse" that "speaks" PostgreSQL).
|
||||||
Today **_re:dash_** has support for querying multiple databases, including: Redshift, Google BigQuery, PostgreSQL, MySQL, Graphite,
|
Today **_Redash_** has support for querying multiple databases, including: Redshift, Google BigQuery, PostgreSQL, MySQL, Graphite,
|
||||||
Presto, Google Spreadsheets, Cloudera Impala, Hive and custom scripts.
|
Presto, Google Spreadsheets, Cloudera Impala, Hive and custom scripts.
|
||||||
|
|
||||||
**_re:dash_** consists of two parts:
|
**_Redash_** consists of two parts:
|
||||||
|
|
||||||
1. **Query Editor**: think of [JS Fiddle](http://jsfiddle.net) for SQL queries. It's your way to share data in the organization in an open way, by sharing both the dataset and the query that generated it. This way everyone can peer review not only the resulting dataset but also the process that generated it. Also it's possible to fork it and generate new datasets and reach new insights.
|
1. **Query Editor**: think of [JS Fiddle](http://jsfiddle.net) for SQL queries. It's your way to share data in the organization in an open way, by sharing both the dataset and the query that generated it. This way everyone can peer review not only the resulting dataset but also the process that generated it. Also it's possible to fork it and generate new datasets and reach new insights.
|
||||||
2. **Dashboards/Visualizations**: once you have a dataset, you can create different visualizations out of it, and then combine several visualizations into a single dashboard. Currently it supports charts, pivot table and cohorts.
|
2. **Dashboards/Visualizations**: once you have a dataset, you can create different visualizations out of it, and then combine several visualizations into a single dashboard. Currently it supports charts, pivot table and cohorts.
|
||||||
|
|
||||||
**_re:dash_** is a work in progress and has its rough edges and way to go to fulfill its full potential. The Query Editor part is quite solid, but the visualizations need more work to enrich them and to make them more user friendly.
|
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
<img src="https://cloud.githubusercontent.com/assets/71468/12611424/1faf4d6a-c4f5-11e5-89b5-31efc1155d2c.gif" width="60%"/>
|
<img src="https://cloud.githubusercontent.com/assets/71468/17391289/8e83878e-5a1d-11e6-8938-af9054a33b19.gif" width="60%"/>
|
||||||
|
|
||||||
You can try out the demo instance: http://demo.redash.io/ (login with any Google account).
|
You can try out the demo instance: http://demo.redash.io/ (login with any Google account).
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
* [Setting up re:dash instance](http://redash.io/deployment/setup.html) (includes links to ready made AWS/GCE images).
|
* [Setting up Redash instance](https://redash.io/help-onpremise/setup/setting-up-redash-instance.html) (includes links to ready made AWS/GCE images).
|
||||||
* [Documentation](http://docs.redash.io).
|
* [Documentation](https://redash.io/help/).
|
||||||
|
|
||||||
|
|
||||||
## Getting Help
|
## Getting Help
|
||||||
@@ -49,8 +43,8 @@ You can try out the demo instance: http://demo.redash.io/ (login with any Google
|
|||||||
## Reporting Bugs and Contributing Code
|
## Reporting Bugs and Contributing Code
|
||||||
|
|
||||||
* Want to report a bug or request a feature? Please open [an issue](https://github.com/getredash/redash/issues/new).
|
* Want to report a bug or request a feature? Please open [an issue](https://github.com/getredash/redash/issues/new).
|
||||||
* Want to help us build **_re:dash_**? Fork the project, edit in a [dev environment](http://docs.redash.io/en/latest/dev/vagrant.html), and make a pull request. We need all the help we can get!
|
* Want to help us build **_Redash_**? Fork the project, edit in a [dev environment](https://redash.io/help-onpremise/setup/setting-up-development-environment-using-vagrant.html), and make a pull request. We need all the help we can get!
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
See [LICENSE](https://github.com/getredash/redash/blob/master/LICENSE) file.
|
BSD-2-Clause.
|
||||||
|
|||||||
15
Vagrantfile
vendored
@@ -1,15 +0,0 @@
|
|||||||
# -*- mode: ruby -*-
|
|
||||||
# vi: set ft=ruby :
|
|
||||||
|
|
||||||
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
|
|
||||||
VAGRANTFILE_API_VERSION = "2"
|
|
||||||
|
|
||||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
|
||||||
config.vm.box = "redash/dev"
|
|
||||||
config.vm.synced_folder "./", "/opt/redash/current"
|
|
||||||
config.vm.network "forwarded_port", guest: 5000, host: 9001
|
|
||||||
config.vm.provision "shell" do |s|
|
|
||||||
s.inline = "/opt/redash/current/setup/vagrant/provision.sh"
|
|
||||||
s.privileged = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
78
bin/docker-entrypoint
Executable file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
worker() {
|
||||||
|
WORKERS_COUNT=${WORKERS_COUNT:-2}
|
||||||
|
QUEUES=${QUEUES:-queries,scheduled_queries,celery}
|
||||||
|
|
||||||
|
echo "Starting $WORKERS_COUNT workers for queues: $QUEUES..."
|
||||||
|
exec /usr/local/bin/celery worker --app=redash.worker -c$WORKERS_COUNT -Q$QUEUES -linfo --maxtasksperchild=10 -Ofair
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduler() {
|
||||||
|
WORKERS_COUNT=${WORKERS_COUNT:-1}
|
||||||
|
QUEUES=${QUEUES:-celery}
|
||||||
|
|
||||||
|
echo "Starting scheduler and $WORKERS_COUNT workers for queues: $QUEUES..."
|
||||||
|
|
||||||
|
exec /usr/local/bin/celery worker --app=redash.worker --beat -c$WORKERS_COUNT -Q$QUEUES -linfo --maxtasksperchild=10 -Ofair
|
||||||
|
}
|
||||||
|
|
||||||
|
server() {
|
||||||
|
exec /usr/local/bin/gunicorn -b 0.0.0.0:5000 --name redash -w4 redash.wsgi:app
|
||||||
|
}
|
||||||
|
|
||||||
|
help() {
|
||||||
|
echo "Redash Docker."
|
||||||
|
echo ""
|
||||||
|
echo "Usage:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "server -- start Redash server (with gunicorn)"
|
||||||
|
echo "worker -- start Celery worker"
|
||||||
|
echo "scheduler -- start Celery worker with a beat (scheduler) process"
|
||||||
|
echo ""
|
||||||
|
echo "shell -- open shell"
|
||||||
|
echo "dev_server -- start Flask development server with debugger and auto reload"
|
||||||
|
echo "create_db -- create database tables"
|
||||||
|
echo "manage -- CLI to manage redash"
|
||||||
|
}
|
||||||
|
|
||||||
|
tests() {
|
||||||
|
export REDASH_DATABASE_URL="postgresql://postgres@postgres/tests"
|
||||||
|
exec make test
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
worker)
|
||||||
|
shift
|
||||||
|
worker
|
||||||
|
;;
|
||||||
|
server)
|
||||||
|
shift
|
||||||
|
server
|
||||||
|
;;
|
||||||
|
scheduler)
|
||||||
|
shift
|
||||||
|
scheduler
|
||||||
|
;;
|
||||||
|
dev_server)
|
||||||
|
exec /app/manage.py runserver --debugger --reload -h 0.0.0.0
|
||||||
|
;;
|
||||||
|
shell)
|
||||||
|
exec /app/manage.py shell
|
||||||
|
;;
|
||||||
|
create_db)
|
||||||
|
exec /app/manage.py database create_tables
|
||||||
|
;;
|
||||||
|
manage)
|
||||||
|
shift
|
||||||
|
exec /app/manage.py $*
|
||||||
|
;;
|
||||||
|
tests)
|
||||||
|
tests
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
help
|
||||||
|
;;
|
||||||
|
esac
|
||||||
236
bin/upgrade
Executable file
@@ -0,0 +1,236 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from collections import namedtuple
|
||||||
|
from fnmatch import fnmatch
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
try:
|
||||||
|
import semver
|
||||||
|
except ImportError:
|
||||||
|
print("Missing required library: semver.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
REDASH_HOME = os.environ.get('REDASH_HOME', '/opt/redash')
|
||||||
|
CURRENT_VERSION_PATH = '{}/current'.format(REDASH_HOME)
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd, cwd=None):
|
||||||
|
if not cwd:
|
||||||
|
cwd = REDASH_HOME
|
||||||
|
|
||||||
|
return subprocess.check_output(cmd, cwd=cwd, shell=True, stderr=subprocess.STDOUT)
|
||||||
|
|
||||||
|
|
||||||
|
def confirm(question):
|
||||||
|
reply = str(raw_input(question + ' (y/n): ')).lower().strip()
|
||||||
|
|
||||||
|
if reply[0] == 'y':
|
||||||
|
return True
|
||||||
|
if reply[0] == 'n':
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return confirm("Please use 'y' or 'n'")
|
||||||
|
|
||||||
|
|
||||||
|
def version_path(version_name):
|
||||||
|
return "{}/{}".format(REDASH_HOME, version_name)
|
||||||
|
|
||||||
|
END_CODE = '\033[0m'
|
||||||
|
|
||||||
|
|
||||||
|
def colored_string(text, color):
|
||||||
|
if sys.stdout.isatty():
|
||||||
|
return "{}{}{}".format(color, text, END_CODE)
|
||||||
|
else:
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def h1(text):
|
||||||
|
print(colored_string(text, '\033[4m\033[1m'))
|
||||||
|
|
||||||
|
|
||||||
|
def green(text):
|
||||||
|
print(colored_string(text, '\033[92m'))
|
||||||
|
|
||||||
|
|
||||||
|
def red(text):
|
||||||
|
print(colored_string(text, '\033[91m'))
|
||||||
|
|
||||||
|
|
||||||
|
class Release(namedtuple('Release', ('version', 'download_url', 'filename', 'description'))):
|
||||||
|
def v1_or_newer(self):
|
||||||
|
return semver.compare(self.version, '1.0.0-alpha') >= 0
|
||||||
|
|
||||||
|
def is_newer(self, version):
|
||||||
|
return semver.compare(self.version, version) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version_name(self):
|
||||||
|
return self.filename.replace('.tar.gz', '')
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_release_from_ci():
|
||||||
|
response = requests.get('https://circleci.com/api/v1.1/project/github/getredash/redash/latest/artifacts')
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
exit("Failed getting releases (status code: %s)." % response.status_code)
|
||||||
|
|
||||||
|
tarball_asset = filter(lambda asset: asset['url'].endswith('.tar.gz'), response.json())[0]
|
||||||
|
filename = tarball_asset['pretty_path'].replace('$CIRCLE_ARTIFACTS/', '')
|
||||||
|
version = filename.replace('redash.', '').replace('.tar.gz', '')
|
||||||
|
|
||||||
|
release = Release(version, tarball_asset['url'], filename, '')
|
||||||
|
|
||||||
|
return release
|
||||||
|
|
||||||
|
|
||||||
|
def get_release(channel):
|
||||||
|
if channel == 'ci':
|
||||||
|
return get_latest_release_from_ci()
|
||||||
|
|
||||||
|
response = requests.get('https://version.redash.io/api/releases?channel={}'.format(channel))
|
||||||
|
release = response.json()[0]
|
||||||
|
|
||||||
|
filename = release['download_url'].split('/')[-1]
|
||||||
|
release = Release(release['version'], release['download_url'], filename, release['description'])
|
||||||
|
|
||||||
|
return release
|
||||||
|
|
||||||
|
|
||||||
|
def link_to_current(version_name):
|
||||||
|
green("Linking to current version...")
|
||||||
|
run('ln -nfs {} {}'.format(version_path(version_name), CURRENT_VERSION_PATH))
|
||||||
|
|
||||||
|
|
||||||
|
def restart_services():
|
||||||
|
# We're doing this instead of simple 'supervisorctl restart all' because
|
||||||
|
# otherwise it won't notice that /opt/redash/current pointing at a different
|
||||||
|
# directory.
|
||||||
|
green("Restarting...")
|
||||||
|
run('sudo /etc/init.d/redash_supervisord restart')
|
||||||
|
|
||||||
|
|
||||||
|
def update_requirements(version_name):
|
||||||
|
green("Installing new Python packages (if needed)...")
|
||||||
|
new_requirements_file = '{}/requirements.txt'.format(version_path(version_name))
|
||||||
|
|
||||||
|
install_requirements = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
run('diff {}/requirements.txt {}'.format(CURRENT_VERSION_PATH, new_requirements_file)) != 0
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
if e.returncode != 0:
|
||||||
|
install_requirements = True
|
||||||
|
|
||||||
|
if install_requirements:
|
||||||
|
run('sudo pip install -r {}'.format(new_requirements_file))
|
||||||
|
|
||||||
|
|
||||||
|
def apply_migrations(release):
|
||||||
|
green("Running migrations (if needed)...")
|
||||||
|
if not release.v1_or_newer():
|
||||||
|
return apply_migrations_pre_v1(release.version_name)
|
||||||
|
|
||||||
|
run("sudo -u redash bin/run ./manage.py db upgrade", cwd=version_path(release.version_name))
|
||||||
|
|
||||||
|
|
||||||
|
def find_migrations(version_name):
|
||||||
|
current_migrations = set([f for f in os.listdir("{}/migrations".format(CURRENT_VERSION_PATH)) if fnmatch(f, '*_*.py')])
|
||||||
|
new_migrations = sorted([f for f in os.listdir("{}/migrations".format(version_path(version_name))) if fnmatch(f, '*_*.py')])
|
||||||
|
|
||||||
|
return [m for m in new_migrations if m not in current_migrations]
|
||||||
|
|
||||||
|
|
||||||
|
def apply_migrations_pre_v1(version_name):
|
||||||
|
new_migrations = find_migrations(version_name)
|
||||||
|
|
||||||
|
if new_migrations:
|
||||||
|
green("New migrations to run: ")
|
||||||
|
print(', '.join(new_migrations))
|
||||||
|
else:
|
||||||
|
print("No new migrations in this version.")
|
||||||
|
|
||||||
|
if new_migrations and confirm("Apply new migrations? (make sure you have backup)"):
|
||||||
|
for migration in new_migrations:
|
||||||
|
print("Applying {}...".format(migration))
|
||||||
|
run("sudo sudo -u redash PYTHONPATH=. bin/run python migrations/{}".format(migration), cwd=version_path(version_name))
|
||||||
|
|
||||||
|
|
||||||
|
def download_and_unpack(release):
|
||||||
|
directory_name = release.version_name
|
||||||
|
|
||||||
|
green("Downloading release tarball...")
|
||||||
|
run('sudo wget --header="Accept: application/octet-stream" -O {} {}'.format(release.filename, release.download_url))
|
||||||
|
green("Unpacking to: {}...".format(directory_name))
|
||||||
|
run('sudo mkdir -p {}'.format(directory_name))
|
||||||
|
run('sudo tar -C {} -xvf {}'.format(directory_name, release.filename))
|
||||||
|
|
||||||
|
green("Changing ownership to redash...")
|
||||||
|
run('sudo chown redash {}'.format(directory_name))
|
||||||
|
|
||||||
|
green("Linking .env file...")
|
||||||
|
run('sudo ln -nfs {}/.env {}/.env'.format(REDASH_HOME, version_path(directory_name)))
|
||||||
|
|
||||||
|
|
||||||
|
def current_version():
|
||||||
|
real_current_path = os.path.realpath(CURRENT_VERSION_PATH).replace('.b', '+b')
|
||||||
|
return real_current_path.replace(REDASH_HOME + '/', '').replace('redash.', '')
|
||||||
|
|
||||||
|
|
||||||
|
def verify_minimum_version():
|
||||||
|
green("Current version: " + current_version())
|
||||||
|
if semver.compare(current_version(), '0.12.0') < 0:
|
||||||
|
red("You need to have Redash v0.12.0 or newer to upgrade to post v1.0.0 releases.")
|
||||||
|
green("To upgrade to v0.12.0, run the upgrade script set to the legacy channel (--channel legacy).")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def show_description_and_confirm(description):
|
||||||
|
if description:
|
||||||
|
print(description)
|
||||||
|
|
||||||
|
if not confirm("Continue with upgrade?"):
|
||||||
|
red("Cancelling upgrade.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_newer_version(release):
|
||||||
|
if not release.is_newer(current_version()):
|
||||||
|
red("The found release is not newer than your current deployed release ({}). Aborting upgrade.".format(current_version()))
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def deploy_release(channel):
|
||||||
|
h1("Starting Redash upgrade:")
|
||||||
|
|
||||||
|
release = get_release(channel)
|
||||||
|
green("Found version: {}".format(release.version))
|
||||||
|
|
||||||
|
if release.v1_or_newer():
|
||||||
|
verify_minimum_version()
|
||||||
|
|
||||||
|
verify_newer_version(release)
|
||||||
|
show_description_and_confirm(release.description)
|
||||||
|
|
||||||
|
try:
|
||||||
|
download_and_unpack(release)
|
||||||
|
update_requirements(release.version_name)
|
||||||
|
apply_migrations(release)
|
||||||
|
link_to_current(release.version_name)
|
||||||
|
restart_services()
|
||||||
|
green("Done! Enjoy.")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
red("Failed running: {}".format(e.cmd))
|
||||||
|
red("Exit status: {}\nOutput:\n{}".format(e.returncode, e.output))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--channel", help="The channel to get release from (default: stable).", default='stable')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
deploy_release(args.channel)
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
help() {
|
|
||||||
echo "Usage: "
|
|
||||||
echo "`basename "$0"` {start, test}"
|
|
||||||
}
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
start)
|
|
||||||
vagrant up
|
|
||||||
vagrant ssh -c "cd /opt/redash/current; bin/run honcho start -f Procfile.dev;"
|
|
||||||
;;
|
|
||||||
test)
|
|
||||||
vagrant up
|
|
||||||
vagrant ssh -c "cd /opt/redash/current; make test"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
help
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
47
bower.json
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "redash",
|
|
||||||
"version": "0.11.0",
|
|
||||||
"dependencies": {
|
|
||||||
"angular": "1.2.18",
|
|
||||||
"angular-resource": "1.2.18",
|
|
||||||
"angular-route": "1.2.18",
|
|
||||||
"angular-growl": "0.4.0",
|
|
||||||
"json3": "3.2.4",
|
|
||||||
"jquery": "1.9.1",
|
|
||||||
"bootstrap": "3.3.6",
|
|
||||||
"es5-shim": "2.0.8",
|
|
||||||
"angular-moment": "0.10.3",
|
|
||||||
"moment": "~2.8.0",
|
|
||||||
"codemirror": "4.8.0",
|
|
||||||
"underscore": "1.5.1",
|
|
||||||
"pivottable": "2.0.2",
|
|
||||||
"cornelius": "https://github.com/restorando/cornelius.git",
|
|
||||||
"gridster": "0.2.0",
|
|
||||||
"mousetrap": "~1.4.6",
|
|
||||||
"jquery-ui": "~1.10.4",
|
|
||||||
"underscore.string": "~2.3.3",
|
|
||||||
"marked": "~0.3.2",
|
|
||||||
"pace": "~0.5.1",
|
|
||||||
"font-awesome": "~4.2.0",
|
|
||||||
"mustache": "~1.0.0",
|
|
||||||
"canvg": "gabelerner/canvg",
|
|
||||||
"angular-ui-bootstrap-bower": "~0.12.1",
|
|
||||||
"leaflet": "~0.7.3",
|
|
||||||
"angular-base64-upload": "~0.1.11",
|
|
||||||
"angular-ui-select": "~0.13.2",
|
|
||||||
"angular-bootstrap-show-errors": "~2.3.0",
|
|
||||||
"angular-sanitize": "1.2.18",
|
|
||||||
"d3": "3.5.6",
|
|
||||||
"angular-ui-sortable": "~0.13.4",
|
|
||||||
"angular-resizable": "^1.2.0",
|
|
||||||
"material-design-iconic-font": "^2.2.0",
|
|
||||||
"plotly.js": "^1.9.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"angular-mocks": "1.2.18",
|
|
||||||
"angular-scenario": "1.2.18"
|
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"angular": "1.2.18"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
23
circle.yml
@@ -1,22 +1,18 @@
|
|||||||
machine:
|
machine:
|
||||||
services:
|
services:
|
||||||
- docker
|
- docker
|
||||||
|
- redis
|
||||||
node:
|
node:
|
||||||
version:
|
version:
|
||||||
0.12.4
|
6.9.1
|
||||||
python:
|
|
||||||
version:
|
|
||||||
2.7.3
|
|
||||||
dependencies:
|
dependencies:
|
||||||
pre:
|
override:
|
||||||
|
- pip install --upgrade setuptools
|
||||||
- pip install -r requirements_dev.txt
|
- pip install -r requirements_dev.txt
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- pip install pymongo==3.2.1
|
- make deps
|
||||||
- if [ "$CIRCLE_BRANCH" = "master" ]; then make deps; fi
|
|
||||||
cache_directories:
|
cache_directories:
|
||||||
- node_modules/
|
- node_modules/
|
||||||
- rd_ui/node_modules/
|
|
||||||
- rd_ui/app/bower_components/
|
|
||||||
test:
|
test:
|
||||||
override:
|
override:
|
||||||
- nosetests --with-xunit --xunit-file=$CIRCLE_TEST_REPORTS/junit.xml --with-coverage --cover-package=redash tests/
|
- nosetests --with-xunit --xunit-file=$CIRCLE_TEST_REPORTS/junit.xml --with-coverage --cover-package=redash tests/
|
||||||
@@ -25,11 +21,12 @@ deployment:
|
|||||||
branch: master
|
branch: master
|
||||||
commands:
|
commands:
|
||||||
- make pack
|
- make pack
|
||||||
- make upload
|
# Skipping uploads for now, until master is stable.
|
||||||
- echo "rd_ui/app" >> .dockerignore
|
# - make upload
|
||||||
- docker pull redash/redash:latest
|
#- echo "client/app" >> .dockerignore
|
||||||
- docker build -t redash/redash:$(./manage.py version | sed -e "s/\+/./") .
|
#- docker pull redash/redash:latest
|
||||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
||||||
|
- docker build -t redash/redash:$(./manage.py version | sed -e "s/\+/./") .
|
||||||
- docker push redash/redash:$(./manage.py version | sed -e "s/\+/./")
|
- docker push redash/redash:$(./manage.py version | sed -e "s/\+/./")
|
||||||
notify:
|
notify:
|
||||||
webhooks:
|
webhooks:
|
||||||
|
|||||||
4
client/.babelrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"presets": ["es2015", "stage-2"],
|
||||||
|
"plugins": ["transform-object-assign"]
|
||||||
|
}
|
||||||
2
client/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
build/*.js
|
||||||
|
config/*.js
|
||||||
15
client/.eslintrc.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: 'airbnb-base',
|
||||||
|
env: {
|
||||||
|
"browser": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// allow debugger during development
|
||||||
|
'no-param-reassign': 0,
|
||||||
|
'no-mixed-operators': 0,
|
||||||
|
'no-underscore-dangle': 0,
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
1
client/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dist
|
||||||
0
client/app/assets/css/main.scss
Normal file
@@ -4,12 +4,17 @@ body {
|
|||||||
|
|
||||||
body.headless {
|
body.headless {
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.headless nav.app-header {
|
body.headless nav.app-header {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.headless div#footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
a[ng-click] {
|
a[ng-click] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -160,16 +165,13 @@ a.navbar-brand img {
|
|||||||
|
|
||||||
/* Gridster */
|
/* Gridster */
|
||||||
|
|
||||||
.gridster ul {
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
li.widget {
|
li.widget {
|
||||||
/*background-color:grey;*/
|
/*background-color:grey;*/
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: grey;
|
border-color: grey;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.widget:hover {
|
li.widget:hover {
|
||||||
@@ -188,18 +190,20 @@ li.widget:hover {
|
|||||||
background: rgba(0, 0, 0, 0.5) !important;
|
background: rgba(0, 0, 0, 0.5) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CodeMirror */
|
.gridster li .heading {
|
||||||
.CodeMirror {
|
border: #ddd;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Editor */
|
||||||
|
|
||||||
|
.ace_editor {
|
||||||
border: 1px solid #eee;
|
border: 1px solid #eee;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-scroll {
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Support for Font-Awesome in btn-xs */
|
/* Support for Font-Awesome in btn-xs */
|
||||||
|
|
||||||
.btn-xs > .fa {
|
.btn-xs > .fa {
|
||||||
@@ -313,7 +317,7 @@ to add those CSS styles here. */
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rd-form-control {
|
.rd-form-control {
|
||||||
width: 100%;
|
width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div {
|
pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div {
|
||||||
@@ -416,6 +420,16 @@ counter-renderer counter-name {
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.schema-control {
|
||||||
|
display: flex;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-control .form-control {
|
||||||
|
height: 30px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.schema-browser {
|
.schema-browser {
|
||||||
height: calc(100% - 45px);
|
height: calc(100% - 45px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -440,10 +454,6 @@ div.table-name:hover {
|
|||||||
padding: 30px;
|
padding: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-container {
|
|
||||||
margin-bottom: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footer */
|
/* Footer */
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
@@ -481,17 +491,6 @@ div.table-name:hover {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Smart Table */
|
|
||||||
|
|
||||||
.smart-table {
|
|
||||||
margin-bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.smart-table .pagination {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voffset {
|
.voffset {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
@@ -613,9 +612,16 @@ div.table-name:hover {
|
|||||||
|
|
||||||
.collapsing,
|
.collapsing,
|
||||||
.collapse.in {
|
.collapse.in {
|
||||||
background: #f4f4f4;
|
padding: 5px 10px;
|
||||||
padding: 5px 10px;
|
transition: all 0.35s ease;
|
||||||
transition: all 0.35s ease;
|
}
|
||||||
|
|
||||||
|
.schema-browser .collapse.in {
|
||||||
|
background: #f4f4f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .collapse.in {
|
||||||
|
background: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fixes for SuperFlat */
|
/* Fixes for SuperFlat */
|
||||||
@@ -663,3 +669,32 @@ div.table-name:hover {
|
|||||||
.t-header.widget {
|
.t-header.widget {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sankey Visualization */
|
||||||
|
.sankey .node rect {
|
||||||
|
fill-opacity: .9;
|
||||||
|
shape-rendering: crispEdges;
|
||||||
|
stroke-width: 0;
|
||||||
|
}
|
||||||
|
.sankey .node text {
|
||||||
|
text-shadow: 0 1px 0 #fff;
|
||||||
|
}
|
||||||
|
.sankey .link {
|
||||||
|
fill: none;
|
||||||
|
stroke: #000;
|
||||||
|
stroke-opacity: .2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Dashboard list view */
|
||||||
|
.m-2{
|
||||||
|
margin:2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu > .disabled{
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The real magic ;) */
|
||||||
|
.dropdown-menu > .disabled > a{
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
@@ -254,809 +254,6 @@ th {
|
|||||||
border: 1px solid #ddd !important;
|
border: 1px solid #ddd !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@font-face {
|
|
||||||
font-family: 'Glyphicons Halflings';
|
|
||||||
src: url('../fonts/glyphicons-halflings-regular.eot');
|
|
||||||
src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
|
|
||||||
}
|
|
||||||
.glyphicon {
|
|
||||||
position: relative;
|
|
||||||
top: 1px;
|
|
||||||
display: inline-block;
|
|
||||||
font-family: 'Glyphicons Halflings';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: normal;
|
|
||||||
line-height: 1;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
.glyphicon-asterisk:before {
|
|
||||||
content: "\2a";
|
|
||||||
}
|
|
||||||
.glyphicon-plus:before {
|
|
||||||
content: "\2b";
|
|
||||||
}
|
|
||||||
.glyphicon-euro:before,
|
|
||||||
.glyphicon-eur:before {
|
|
||||||
content: "\20ac";
|
|
||||||
}
|
|
||||||
.glyphicon-minus:before {
|
|
||||||
content: "\2212";
|
|
||||||
}
|
|
||||||
.glyphicon-cloud:before {
|
|
||||||
content: "\2601";
|
|
||||||
}
|
|
||||||
.glyphicon-envelope:before {
|
|
||||||
content: "\2709";
|
|
||||||
}
|
|
||||||
.glyphicon-pencil:before {
|
|
||||||
content: "\270f";
|
|
||||||
}
|
|
||||||
.glyphicon-glass:before {
|
|
||||||
content: "\e001";
|
|
||||||
}
|
|
||||||
.glyphicon-music:before {
|
|
||||||
content: "\e002";
|
|
||||||
}
|
|
||||||
.glyphicon-search:before {
|
|
||||||
content: "\e003";
|
|
||||||
}
|
|
||||||
.glyphicon-heart:before {
|
|
||||||
content: "\e005";
|
|
||||||
}
|
|
||||||
.glyphicon-star:before {
|
|
||||||
content: "\e006";
|
|
||||||
}
|
|
||||||
.glyphicon-star-empty:before {
|
|
||||||
content: "\e007";
|
|
||||||
}
|
|
||||||
.glyphicon-user:before {
|
|
||||||
content: "\e008";
|
|
||||||
}
|
|
||||||
.glyphicon-film:before {
|
|
||||||
content: "\e009";
|
|
||||||
}
|
|
||||||
.glyphicon-th-large:before {
|
|
||||||
content: "\e010";
|
|
||||||
}
|
|
||||||
.glyphicon-th:before {
|
|
||||||
content: "\e011";
|
|
||||||
}
|
|
||||||
.glyphicon-th-list:before {
|
|
||||||
content: "\e012";
|
|
||||||
}
|
|
||||||
.glyphicon-ok:before {
|
|
||||||
content: "\e013";
|
|
||||||
}
|
|
||||||
.glyphicon-remove:before {
|
|
||||||
content: "\e014";
|
|
||||||
}
|
|
||||||
.glyphicon-zoom-in:before {
|
|
||||||
content: "\e015";
|
|
||||||
}
|
|
||||||
.glyphicon-zoom-out:before {
|
|
||||||
content: "\e016";
|
|
||||||
}
|
|
||||||
.glyphicon-off:before {
|
|
||||||
content: "\e017";
|
|
||||||
}
|
|
||||||
.glyphicon-signal:before {
|
|
||||||
content: "\e018";
|
|
||||||
}
|
|
||||||
.glyphicon-cog:before {
|
|
||||||
content: "\e019";
|
|
||||||
}
|
|
||||||
.glyphicon-trash:before {
|
|
||||||
content: "\e020";
|
|
||||||
}
|
|
||||||
.glyphicon-home:before {
|
|
||||||
content: "\e021";
|
|
||||||
}
|
|
||||||
.glyphicon-file:before {
|
|
||||||
content: "\e022";
|
|
||||||
}
|
|
||||||
.glyphicon-time:before {
|
|
||||||
content: "\e023";
|
|
||||||
}
|
|
||||||
.glyphicon-road:before {
|
|
||||||
content: "\e024";
|
|
||||||
}
|
|
||||||
.glyphicon-download-alt:before {
|
|
||||||
content: "\e025";
|
|
||||||
}
|
|
||||||
.glyphicon-download:before {
|
|
||||||
content: "\e026";
|
|
||||||
}
|
|
||||||
.glyphicon-upload:before {
|
|
||||||
content: "\e027";
|
|
||||||
}
|
|
||||||
.glyphicon-inbox:before {
|
|
||||||
content: "\e028";
|
|
||||||
}
|
|
||||||
.glyphicon-play-circle:before {
|
|
||||||
content: "\e029";
|
|
||||||
}
|
|
||||||
.glyphicon-repeat:before {
|
|
||||||
content: "\e030";
|
|
||||||
}
|
|
||||||
.glyphicon-refresh:before {
|
|
||||||
content: "\e031";
|
|
||||||
}
|
|
||||||
.glyphicon-list-alt:before {
|
|
||||||
content: "\e032";
|
|
||||||
}
|
|
||||||
.glyphicon-lock:before {
|
|
||||||
content: "\e033";
|
|
||||||
}
|
|
||||||
.glyphicon-flag:before {
|
|
||||||
content: "\e034";
|
|
||||||
}
|
|
||||||
.glyphicon-headphones:before {
|
|
||||||
content: "\e035";
|
|
||||||
}
|
|
||||||
.glyphicon-volume-off:before {
|
|
||||||
content: "\e036";
|
|
||||||
}
|
|
||||||
.glyphicon-volume-down:before {
|
|
||||||
content: "\e037";
|
|
||||||
}
|
|
||||||
.glyphicon-volume-up:before {
|
|
||||||
content: "\e038";
|
|
||||||
}
|
|
||||||
.glyphicon-qrcode:before {
|
|
||||||
content: "\e039";
|
|
||||||
}
|
|
||||||
.glyphicon-barcode:before {
|
|
||||||
content: "\e040";
|
|
||||||
}
|
|
||||||
.glyphicon-tag:before {
|
|
||||||
content: "\e041";
|
|
||||||
}
|
|
||||||
.glyphicon-tags:before {
|
|
||||||
content: "\e042";
|
|
||||||
}
|
|
||||||
.glyphicon-book:before {
|
|
||||||
content: "\e043";
|
|
||||||
}
|
|
||||||
.glyphicon-bookmark:before {
|
|
||||||
content: "\e044";
|
|
||||||
}
|
|
||||||
.glyphicon-print:before {
|
|
||||||
content: "\e045";
|
|
||||||
}
|
|
||||||
.glyphicon-camera:before {
|
|
||||||
content: "\e046";
|
|
||||||
}
|
|
||||||
.glyphicon-font:before {
|
|
||||||
content: "\e047";
|
|
||||||
}
|
|
||||||
.glyphicon-bold:before {
|
|
||||||
content: "\e048";
|
|
||||||
}
|
|
||||||
.glyphicon-italic:before {
|
|
||||||
content: "\e049";
|
|
||||||
}
|
|
||||||
.glyphicon-text-height:before {
|
|
||||||
content: "\e050";
|
|
||||||
}
|
|
||||||
.glyphicon-text-width:before {
|
|
||||||
content: "\e051";
|
|
||||||
}
|
|
||||||
.glyphicon-align-left:before {
|
|
||||||
content: "\e052";
|
|
||||||
}
|
|
||||||
.glyphicon-align-center:before {
|
|
||||||
content: "\e053";
|
|
||||||
}
|
|
||||||
.glyphicon-align-right:before {
|
|
||||||
content: "\e054";
|
|
||||||
}
|
|
||||||
.glyphicon-align-justify:before {
|
|
||||||
content: "\e055";
|
|
||||||
}
|
|
||||||
.glyphicon-list:before {
|
|
||||||
content: "\e056";
|
|
||||||
}
|
|
||||||
.glyphicon-indent-left:before {
|
|
||||||
content: "\e057";
|
|
||||||
}
|
|
||||||
.glyphicon-indent-right:before {
|
|
||||||
content: "\e058";
|
|
||||||
}
|
|
||||||
.glyphicon-facetime-video:before {
|
|
||||||
content: "\e059";
|
|
||||||
}
|
|
||||||
.glyphicon-picture:before {
|
|
||||||
content: "\e060";
|
|
||||||
}
|
|
||||||
.glyphicon-map-marker:before {
|
|
||||||
content: "\e062";
|
|
||||||
}
|
|
||||||
.glyphicon-adjust:before {
|
|
||||||
content: "\e063";
|
|
||||||
}
|
|
||||||
.glyphicon-tint:before {
|
|
||||||
content: "\e064";
|
|
||||||
}
|
|
||||||
.glyphicon-edit:before {
|
|
||||||
content: "\e065";
|
|
||||||
}
|
|
||||||
.glyphicon-share:before {
|
|
||||||
content: "\e066";
|
|
||||||
}
|
|
||||||
.glyphicon-check:before {
|
|
||||||
content: "\e067";
|
|
||||||
}
|
|
||||||
.glyphicon-move:before {
|
|
||||||
content: "\e068";
|
|
||||||
}
|
|
||||||
.glyphicon-step-backward:before {
|
|
||||||
content: "\e069";
|
|
||||||
}
|
|
||||||
.glyphicon-fast-backward:before {
|
|
||||||
content: "\e070";
|
|
||||||
}
|
|
||||||
.glyphicon-backward:before {
|
|
||||||
content: "\e071";
|
|
||||||
}
|
|
||||||
.glyphicon-play:before {
|
|
||||||
content: "\e072";
|
|
||||||
}
|
|
||||||
.glyphicon-pause:before {
|
|
||||||
content: "\e073";
|
|
||||||
}
|
|
||||||
.glyphicon-stop:before {
|
|
||||||
content: "\e074";
|
|
||||||
}
|
|
||||||
.glyphicon-forward:before {
|
|
||||||
content: "\e075";
|
|
||||||
}
|
|
||||||
.glyphicon-fast-forward:before {
|
|
||||||
content: "\e076";
|
|
||||||
}
|
|
||||||
.glyphicon-step-forward:before {
|
|
||||||
content: "\e077";
|
|
||||||
}
|
|
||||||
.glyphicon-eject:before {
|
|
||||||
content: "\e078";
|
|
||||||
}
|
|
||||||
.glyphicon-chevron-left:before {
|
|
||||||
content: "\e079";
|
|
||||||
}
|
|
||||||
.glyphicon-chevron-right:before {
|
|
||||||
content: "\e080";
|
|
||||||
}
|
|
||||||
.glyphicon-plus-sign:before {
|
|
||||||
content: "\e081";
|
|
||||||
}
|
|
||||||
.glyphicon-minus-sign:before {
|
|
||||||
content: "\e082";
|
|
||||||
}
|
|
||||||
.glyphicon-remove-sign:before {
|
|
||||||
content: "\e083";
|
|
||||||
}
|
|
||||||
.glyphicon-ok-sign:before {
|
|
||||||
content: "\e084";
|
|
||||||
}
|
|
||||||
.glyphicon-question-sign:before {
|
|
||||||
content: "\e085";
|
|
||||||
}
|
|
||||||
.glyphicon-info-sign:before {
|
|
||||||
content: "\e086";
|
|
||||||
}
|
|
||||||
.glyphicon-screenshot:before {
|
|
||||||
content: "\e087";
|
|
||||||
}
|
|
||||||
.glyphicon-remove-circle:before {
|
|
||||||
content: "\e088";
|
|
||||||
}
|
|
||||||
.glyphicon-ok-circle:before {
|
|
||||||
content: "\e089";
|
|
||||||
}
|
|
||||||
.glyphicon-ban-circle:before {
|
|
||||||
content: "\e090";
|
|
||||||
}
|
|
||||||
.glyphicon-arrow-left:before {
|
|
||||||
content: "\e091";
|
|
||||||
}
|
|
||||||
.glyphicon-arrow-right:before {
|
|
||||||
content: "\e092";
|
|
||||||
}
|
|
||||||
.glyphicon-arrow-up:before {
|
|
||||||
content: "\e093";
|
|
||||||
}
|
|
||||||
.glyphicon-arrow-down:before {
|
|
||||||
content: "\e094";
|
|
||||||
}
|
|
||||||
.glyphicon-share-alt:before {
|
|
||||||
content: "\e095";
|
|
||||||
}
|
|
||||||
.glyphicon-resize-full:before {
|
|
||||||
content: "\e096";
|
|
||||||
}
|
|
||||||
.glyphicon-resize-small:before {
|
|
||||||
content: "\e097";
|
|
||||||
}
|
|
||||||
.glyphicon-exclamation-sign:before {
|
|
||||||
content: "\e101";
|
|
||||||
}
|
|
||||||
.glyphicon-gift:before {
|
|
||||||
content: "\e102";
|
|
||||||
}
|
|
||||||
.glyphicon-leaf:before {
|
|
||||||
content: "\e103";
|
|
||||||
}
|
|
||||||
.glyphicon-fire:before {
|
|
||||||
content: "\e104";
|
|
||||||
}
|
|
||||||
.glyphicon-eye-open:before {
|
|
||||||
content: "\e105";
|
|
||||||
}
|
|
||||||
.glyphicon-eye-close:before {
|
|
||||||
content: "\e106";
|
|
||||||
}
|
|
||||||
.glyphicon-warning-sign:before {
|
|
||||||
content: "\e107";
|
|
||||||
}
|
|
||||||
.glyphicon-plane:before {
|
|
||||||
content: "\e108";
|
|
||||||
}
|
|
||||||
.glyphicon-calendar:before {
|
|
||||||
content: "\e109";
|
|
||||||
}
|
|
||||||
.glyphicon-random:before {
|
|
||||||
content: "\e110";
|
|
||||||
}
|
|
||||||
.glyphicon-comment:before {
|
|
||||||
content: "\e111";
|
|
||||||
}
|
|
||||||
.glyphicon-magnet:before {
|
|
||||||
content: "\e112";
|
|
||||||
}
|
|
||||||
.glyphicon-chevron-up:before {
|
|
||||||
content: "\e113";
|
|
||||||
}
|
|
||||||
.glyphicon-chevron-down:before {
|
|
||||||
content: "\e114";
|
|
||||||
}
|
|
||||||
.glyphicon-retweet:before {
|
|
||||||
content: "\e115";
|
|
||||||
}
|
|
||||||
.glyphicon-shopping-cart:before {
|
|
||||||
content: "\e116";
|
|
||||||
}
|
|
||||||
.glyphicon-folder-close:before {
|
|
||||||
content: "\e117";
|
|
||||||
}
|
|
||||||
.glyphicon-folder-open:before {
|
|
||||||
content: "\e118";
|
|
||||||
}
|
|
||||||
.glyphicon-resize-vertical:before {
|
|
||||||
content: "\e119";
|
|
||||||
}
|
|
||||||
.glyphicon-resize-horizontal:before {
|
|
||||||
content: "\e120";
|
|
||||||
}
|
|
||||||
.glyphicon-hdd:before {
|
|
||||||
content: "\e121";
|
|
||||||
}
|
|
||||||
.glyphicon-bullhorn:before {
|
|
||||||
content: "\e122";
|
|
||||||
}
|
|
||||||
.glyphicon-bell:before {
|
|
||||||
content: "\e123";
|
|
||||||
}
|
|
||||||
.glyphicon-certificate:before {
|
|
||||||
content: "\e124";
|
|
||||||
}
|
|
||||||
.glyphicon-thumbs-up:before {
|
|
||||||
content: "\e125";
|
|
||||||
}
|
|
||||||
.glyphicon-thumbs-down:before {
|
|
||||||
content: "\e126";
|
|
||||||
}
|
|
||||||
.glyphicon-hand-right:before {
|
|
||||||
content: "\e127";
|
|
||||||
}
|
|
||||||
.glyphicon-hand-left:before {
|
|
||||||
content: "\e128";
|
|
||||||
}
|
|
||||||
.glyphicon-hand-up:before {
|
|
||||||
content: "\e129";
|
|
||||||
}
|
|
||||||
.glyphicon-hand-down:before {
|
|
||||||
content: "\e130";
|
|
||||||
}
|
|
||||||
.glyphicon-circle-arrow-right:before {
|
|
||||||
content: "\e131";
|
|
||||||
}
|
|
||||||
.glyphicon-circle-arrow-left:before {
|
|
||||||
content: "\e132";
|
|
||||||
}
|
|
||||||
.glyphicon-circle-arrow-up:before {
|
|
||||||
content: "\e133";
|
|
||||||
}
|
|
||||||
.glyphicon-circle-arrow-down:before {
|
|
||||||
content: "\e134";
|
|
||||||
}
|
|
||||||
.glyphicon-globe:before {
|
|
||||||
content: "\e135";
|
|
||||||
}
|
|
||||||
.glyphicon-wrench:before {
|
|
||||||
content: "\e136";
|
|
||||||
}
|
|
||||||
.glyphicon-tasks:before {
|
|
||||||
content: "\e137";
|
|
||||||
}
|
|
||||||
.glyphicon-filter:before {
|
|
||||||
content: "\e138";
|
|
||||||
}
|
|
||||||
.glyphicon-briefcase:before {
|
|
||||||
content: "\e139";
|
|
||||||
}
|
|
||||||
.glyphicon-fullscreen:before {
|
|
||||||
content: "\e140";
|
|
||||||
}
|
|
||||||
.glyphicon-dashboard:before {
|
|
||||||
content: "\e141";
|
|
||||||
}
|
|
||||||
.glyphicon-paperclip:before {
|
|
||||||
content: "\e142";
|
|
||||||
}
|
|
||||||
.glyphicon-heart-empty:before {
|
|
||||||
content: "\e143";
|
|
||||||
}
|
|
||||||
.glyphicon-link:before {
|
|
||||||
content: "\e144";
|
|
||||||
}
|
|
||||||
.glyphicon-phone:before {
|
|
||||||
content: "\e145";
|
|
||||||
}
|
|
||||||
.glyphicon-pushpin:before {
|
|
||||||
content: "\e146";
|
|
||||||
}
|
|
||||||
.glyphicon-usd:before {
|
|
||||||
content: "\e148";
|
|
||||||
}
|
|
||||||
.glyphicon-gbp:before {
|
|
||||||
content: "\e149";
|
|
||||||
}
|
|
||||||
.glyphicon-sort:before {
|
|
||||||
content: "\e150";
|
|
||||||
}
|
|
||||||
.glyphicon-sort-by-alphabet:before {
|
|
||||||
content: "\e151";
|
|
||||||
}
|
|
||||||
.glyphicon-sort-by-alphabet-alt:before {
|
|
||||||
content: "\e152";
|
|
||||||
}
|
|
||||||
.glyphicon-sort-by-order:before {
|
|
||||||
content: "\e153";
|
|
||||||
}
|
|
||||||
.glyphicon-sort-by-order-alt:before {
|
|
||||||
content: "\e154";
|
|
||||||
}
|
|
||||||
.glyphicon-sort-by-attributes:before {
|
|
||||||
content: "\e155";
|
|
||||||
}
|
|
||||||
.glyphicon-sort-by-attributes-alt:before {
|
|
||||||
content: "\e156";
|
|
||||||
}
|
|
||||||
.glyphicon-unchecked:before {
|
|
||||||
content: "\e157";
|
|
||||||
}
|
|
||||||
.glyphicon-expand:before {
|
|
||||||
content: "\e158";
|
|
||||||
}
|
|
||||||
.glyphicon-collapse-down:before {
|
|
||||||
content: "\e159";
|
|
||||||
}
|
|
||||||
.glyphicon-collapse-up:before {
|
|
||||||
content: "\e160";
|
|
||||||
}
|
|
||||||
.glyphicon-log-in:before {
|
|
||||||
content: "\e161";
|
|
||||||
}
|
|
||||||
.glyphicon-flash:before {
|
|
||||||
content: "\e162";
|
|
||||||
}
|
|
||||||
.glyphicon-log-out:before {
|
|
||||||
content: "\e163";
|
|
||||||
}
|
|
||||||
.glyphicon-new-window:before {
|
|
||||||
content: "\e164";
|
|
||||||
}
|
|
||||||
.glyphicon-record:before {
|
|
||||||
content: "\e165";
|
|
||||||
}
|
|
||||||
.glyphicon-save:before {
|
|
||||||
content: "\e166";
|
|
||||||
}
|
|
||||||
.glyphicon-open:before {
|
|
||||||
content: "\e167";
|
|
||||||
}
|
|
||||||
.glyphicon-saved:before {
|
|
||||||
content: "\e168";
|
|
||||||
}
|
|
||||||
.glyphicon-import:before {
|
|
||||||
content: "\e169";
|
|
||||||
}
|
|
||||||
.glyphicon-export:before {
|
|
||||||
content: "\e170";
|
|
||||||
}
|
|
||||||
.glyphicon-send:before {
|
|
||||||
content: "\e171";
|
|
||||||
}
|
|
||||||
.glyphicon-floppy-disk:before {
|
|
||||||
content: "\e172";
|
|
||||||
}
|
|
||||||
.glyphicon-floppy-saved:before {
|
|
||||||
content: "\e173";
|
|
||||||
}
|
|
||||||
.glyphicon-floppy-remove:before {
|
|
||||||
content: "\e174";
|
|
||||||
}
|
|
||||||
.glyphicon-floppy-save:before {
|
|
||||||
content: "\e175";
|
|
||||||
}
|
|
||||||
.glyphicon-floppy-open:before {
|
|
||||||
content: "\e176";
|
|
||||||
}
|
|
||||||
.glyphicon-credit-card:before {
|
|
||||||
content: "\e177";
|
|
||||||
}
|
|
||||||
.glyphicon-transfer:before {
|
|
||||||
content: "\e178";
|
|
||||||
}
|
|
||||||
.glyphicon-cutlery:before {
|
|
||||||
content: "\e179";
|
|
||||||
}
|
|
||||||
.glyphicon-header:before {
|
|
||||||
content: "\e180";
|
|
||||||
}
|
|
||||||
.glyphicon-compressed:before {
|
|
||||||
content: "\e181";
|
|
||||||
}
|
|
||||||
.glyphicon-earphone:before {
|
|
||||||
content: "\e182";
|
|
||||||
}
|
|
||||||
.glyphicon-phone-alt:before {
|
|
||||||
content: "\e183";
|
|
||||||
}
|
|
||||||
.glyphicon-tower:before {
|
|
||||||
content: "\e184";
|
|
||||||
}
|
|
||||||
.glyphicon-stats:before {
|
|
||||||
content: "\e185";
|
|
||||||
}
|
|
||||||
.glyphicon-sd-video:before {
|
|
||||||
content: "\e186";
|
|
||||||
}
|
|
||||||
.glyphicon-hd-video:before {
|
|
||||||
content: "\e187";
|
|
||||||
}
|
|
||||||
.glyphicon-subtitles:before {
|
|
||||||
content: "\e188";
|
|
||||||
}
|
|
||||||
.glyphicon-sound-stereo:before {
|
|
||||||
content: "\e189";
|
|
||||||
}
|
|
||||||
.glyphicon-sound-dolby:before {
|
|
||||||
content: "\e190";
|
|
||||||
}
|
|
||||||
.glyphicon-sound-5-1:before {
|
|
||||||
content: "\e191";
|
|
||||||
}
|
|
||||||
.glyphicon-sound-6-1:before {
|
|
||||||
content: "\e192";
|
|
||||||
}
|
|
||||||
.glyphicon-sound-7-1:before {
|
|
||||||
content: "\e193";
|
|
||||||
}
|
|
||||||
.glyphicon-copyright-mark:before {
|
|
||||||
content: "\e194";
|
|
||||||
}
|
|
||||||
.glyphicon-registration-mark:before {
|
|
||||||
content: "\e195";
|
|
||||||
}
|
|
||||||
.glyphicon-cloud-download:before {
|
|
||||||
content: "\e197";
|
|
||||||
}
|
|
||||||
.glyphicon-cloud-upload:before {
|
|
||||||
content: "\e198";
|
|
||||||
}
|
|
||||||
.glyphicon-tree-conifer:before {
|
|
||||||
content: "\e199";
|
|
||||||
}
|
|
||||||
.glyphicon-tree-deciduous:before {
|
|
||||||
content: "\e200";
|
|
||||||
}
|
|
||||||
.glyphicon-cd:before {
|
|
||||||
content: "\e201";
|
|
||||||
}
|
|
||||||
.glyphicon-save-file:before {
|
|
||||||
content: "\e202";
|
|
||||||
}
|
|
||||||
.glyphicon-open-file:before {
|
|
||||||
content: "\e203";
|
|
||||||
}
|
|
||||||
.glyphicon-level-up:before {
|
|
||||||
content: "\e204";
|
|
||||||
}
|
|
||||||
.glyphicon-copy:before {
|
|
||||||
content: "\e205";
|
|
||||||
}
|
|
||||||
.glyphicon-paste:before {
|
|
||||||
content: "\e206";
|
|
||||||
}
|
|
||||||
.glyphicon-alert:before {
|
|
||||||
content: "\e209";
|
|
||||||
}
|
|
||||||
.glyphicon-equalizer:before {
|
|
||||||
content: "\e210";
|
|
||||||
}
|
|
||||||
.glyphicon-king:before {
|
|
||||||
content: "\e211";
|
|
||||||
}
|
|
||||||
.glyphicon-queen:before {
|
|
||||||
content: "\e212";
|
|
||||||
}
|
|
||||||
.glyphicon-pawn:before {
|
|
||||||
content: "\e213";
|
|
||||||
}
|
|
||||||
.glyphicon-bishop:before {
|
|
||||||
content: "\e214";
|
|
||||||
}
|
|
||||||
.glyphicon-knight:before {
|
|
||||||
content: "\e215";
|
|
||||||
}
|
|
||||||
.glyphicon-baby-formula:before {
|
|
||||||
content: "\e216";
|
|
||||||
}
|
|
||||||
.glyphicon-tent:before {
|
|
||||||
content: "\26fa";
|
|
||||||
}
|
|
||||||
.glyphicon-blackboard:before {
|
|
||||||
content: "\e218";
|
|
||||||
}
|
|
||||||
.glyphicon-bed:before {
|
|
||||||
content: "\e219";
|
|
||||||
}
|
|
||||||
.glyphicon-apple:before {
|
|
||||||
content: "\f8ff";
|
|
||||||
}
|
|
||||||
.glyphicon-erase:before {
|
|
||||||
content: "\e221";
|
|
||||||
}
|
|
||||||
.glyphicon-hourglass:before {
|
|
||||||
content: "\231b";
|
|
||||||
}
|
|
||||||
.glyphicon-lamp:before {
|
|
||||||
content: "\e223";
|
|
||||||
}
|
|
||||||
.glyphicon-duplicate:before {
|
|
||||||
content: "\e224";
|
|
||||||
}
|
|
||||||
.glyphicon-piggy-bank:before {
|
|
||||||
content: "\e225";
|
|
||||||
}
|
|
||||||
.glyphicon-scissors:before {
|
|
||||||
content: "\e226";
|
|
||||||
}
|
|
||||||
.glyphicon-bitcoin:before {
|
|
||||||
content: "\e227";
|
|
||||||
}
|
|
||||||
.glyphicon-btc:before {
|
|
||||||
content: "\e227";
|
|
||||||
}
|
|
||||||
.glyphicon-xbt:before {
|
|
||||||
content: "\e227";
|
|
||||||
}
|
|
||||||
.glyphicon-yen:before {
|
|
||||||
content: "\00a5";
|
|
||||||
}
|
|
||||||
.glyphicon-jpy:before {
|
|
||||||
content: "\00a5";
|
|
||||||
}
|
|
||||||
.glyphicon-ruble:before {
|
|
||||||
content: "\20bd";
|
|
||||||
}
|
|
||||||
.glyphicon-rub:before {
|
|
||||||
content: "\20bd";
|
|
||||||
}
|
|
||||||
.glyphicon-scale:before {
|
|
||||||
content: "\e230";
|
|
||||||
}
|
|
||||||
.glyphicon-ice-lolly:before {
|
|
||||||
content: "\e231";
|
|
||||||
}
|
|
||||||
.glyphicon-ice-lolly-tasted:before {
|
|
||||||
content: "\e232";
|
|
||||||
}
|
|
||||||
.glyphicon-education:before {
|
|
||||||
content: "\e233";
|
|
||||||
}
|
|
||||||
.glyphicon-option-horizontal:before {
|
|
||||||
content: "\e234";
|
|
||||||
}
|
|
||||||
.glyphicon-option-vertical:before {
|
|
||||||
content: "\e235";
|
|
||||||
}
|
|
||||||
.glyphicon-menu-hamburger:before {
|
|
||||||
content: "\e236";
|
|
||||||
}
|
|
||||||
.glyphicon-modal-window:before {
|
|
||||||
content: "\e237";
|
|
||||||
}
|
|
||||||
.glyphicon-oil:before {
|
|
||||||
content: "\e238";
|
|
||||||
}
|
|
||||||
.glyphicon-grain:before {
|
|
||||||
content: "\e239";
|
|
||||||
}
|
|
||||||
.glyphicon-sunglasses:before {
|
|
||||||
content: "\e240";
|
|
||||||
}
|
|
||||||
.glyphicon-text-size:before {
|
|
||||||
content: "\e241";
|
|
||||||
}
|
|
||||||
.glyphicon-text-color:before {
|
|
||||||
content: "\e242";
|
|
||||||
}
|
|
||||||
.glyphicon-text-background:before {
|
|
||||||
content: "\e243";
|
|
||||||
}
|
|
||||||
.glyphicon-object-align-top:before {
|
|
||||||
content: "\e244";
|
|
||||||
}
|
|
||||||
.glyphicon-object-align-bottom:before {
|
|
||||||
content: "\e245";
|
|
||||||
}
|
|
||||||
.glyphicon-object-align-horizontal:before {
|
|
||||||
content: "\e246";
|
|
||||||
}
|
|
||||||
.glyphicon-object-align-left:before {
|
|
||||||
content: "\e247";
|
|
||||||
}
|
|
||||||
.glyphicon-object-align-vertical:before {
|
|
||||||
content: "\e248";
|
|
||||||
}
|
|
||||||
.glyphicon-object-align-right:before {
|
|
||||||
content: "\e249";
|
|
||||||
}
|
|
||||||
.glyphicon-triangle-right:before {
|
|
||||||
content: "\e250";
|
|
||||||
}
|
|
||||||
.glyphicon-triangle-left:before {
|
|
||||||
content: "\e251";
|
|
||||||
}
|
|
||||||
.glyphicon-triangle-bottom:before {
|
|
||||||
content: "\e252";
|
|
||||||
}
|
|
||||||
.glyphicon-triangle-top:before {
|
|
||||||
content: "\e253";
|
|
||||||
}
|
|
||||||
.glyphicon-console:before {
|
|
||||||
content: "\e254";
|
|
||||||
}
|
|
||||||
.glyphicon-superscript:before {
|
|
||||||
content: "\e255";
|
|
||||||
}
|
|
||||||
.glyphicon-subscript:before {
|
|
||||||
content: "\e256";
|
|
||||||
}
|
|
||||||
.glyphicon-menu-left:before {
|
|
||||||
content: "\e257";
|
|
||||||
}
|
|
||||||
.glyphicon-menu-right:before {
|
|
||||||
content: "\e258";
|
|
||||||
}
|
|
||||||
.glyphicon-menu-down:before {
|
|
||||||
content: "\e259";
|
|
||||||
}
|
|
||||||
.glyphicon-menu-up:before {
|
|
||||||
content: "\e260";
|
|
||||||
}
|
|
||||||
* {
|
* {
|
||||||
-webkit-box-sizing: border-box;
|
-webkit-box-sizing: border-box;
|
||||||
-moz-box-sizing: border-box;
|
-moz-box-sizing: border-box;
|
||||||
@@ -7233,7 +6430,7 @@ a {
|
|||||||
}
|
}
|
||||||
html {
|
html {
|
||||||
overflow-x: hidden\0/;
|
overflow-x: hidden\0/;
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: auto;
|
||||||
}
|
}
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
7
client/app/components/app-header/app-header.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.menu-search {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-search input[type="text"] {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
82
client/app/components/app-header/app-header.html
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<nav class="navbar navbar-inverse navbar-fixed-top app-header" role="navigation">
|
||||||
|
<div class="container">
|
||||||
|
<div class="navbar-header">
|
||||||
|
<button type="button" class="navbar-toggle" ng-click="isNavOpen = !isNavOpen">
|
||||||
|
<span class="sr-only">Toggle navigation</span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
</button>
|
||||||
|
<a class="navbar-brand" href="/"><img ng-src="{{$ctrl.logoUrl}}"/></a>
|
||||||
|
</div>
|
||||||
|
<div class="collapse navbar-collapse" uib-collapse="!isNavOpen">
|
||||||
|
<ul class="nav navbar-nav">
|
||||||
|
<li class="dropdown" ng-show="$ctrl.showDashboardsMenu" uib-dropdown>
|
||||||
|
<a href="#" class="dropdown-toggle" uib-dropdown-toggle title="Dashboards">
|
||||||
|
<span class="visible-xs visible-md visible-lg">Dashboards <b class="caret"></b></span>
|
||||||
|
<span class="visible-sm"><i class="zmdi zmdi-view-dashboard"></i> <b class="caret"></b></span>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu" uib-dropdown-menu>
|
||||||
|
<li><a ng-show="$ctrl.currentUser.hasPermission('create_dashboard')" ng-click="$ctrl.newDashboard()">New Dashboard</a></li>
|
||||||
|
<li><a href="dashboards">Dashboards</a></li>
|
||||||
|
<li class="divider" ng-if="$ctrl.dashboards | notEmpty"></li>
|
||||||
|
<li ng-repeat="dashboard in $ctrl.dashboards">
|
||||||
|
<a href="dashboard/{{dashboard.slug}}" ng-bind="dashboard.name"></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown" ng-show="$ctrl.showQueriesMenu" uib-dropdown>
|
||||||
|
<a href="#" class="dropdown-toggle" uib-dropdown-toggle>Queries <b class="caret"></b></a>
|
||||||
|
<ul class="dropdown-menu" uib-dropdown-menu>
|
||||||
|
<li ng-show="$ctrl.showNewQueryMenu"><a href="queries/new">New Query</a></li>
|
||||||
|
<li><a href="queries">Queries</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="alerts">Alerts</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<form class="navbar-form navbar-left" role="search" ng-submit="$ctrl.searchQueries()">
|
||||||
|
<div class="input-group menu-search">
|
||||||
|
<input type="text" ng-model="$ctrl.term" class="form-control" placeholder="Search queries...">
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button type="submit" class="btn btn-default"><span class="zmdi zmdi-search"></span></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<ul class="nav navbar-nav navbar-right">
|
||||||
|
<li ng-show="$ctrl.currentUser.isAdmin">
|
||||||
|
<a href="data_sources" title="Data Sources"><i class="fa fa-database"></i></a>
|
||||||
|
</li>
|
||||||
|
<li ng-show="$ctrl.showSettingsMenu">
|
||||||
|
<a href="users" title="Settings"><i class="fa fa-cog"></i></a>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown" uib-dropdown>
|
||||||
|
<a href="#" class="dropdown-toggle" uib-dropdown-toggle><span ng-bind="$ctrl.currentUser.name"></span> <span
|
||||||
|
class="caret"></span></a>
|
||||||
|
<ul class="dropdown-menu" dropdown-menu>
|
||||||
|
<li style="width:300px">
|
||||||
|
<a ng-href="users/{{$ctrl.currentUser.id}}">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-2">
|
||||||
|
<img ng-src="{{$ctrl.currentUser.gravatar_url}}" size="40px" class="img-circle"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<p><strong>{{$ctrl.currentUser.name}}</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="divider" ng-if="$ctrl.currentUser.hasPermission('super_admin')">
|
||||||
|
<li ng-if="$ctrl.currentUser.hasPermission('super_admin')"><a href="admin/status">System Status</a></li>
|
||||||
|
<li class="divider">
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a ng-click="$ctrl.logout()">Log out</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
50
client/app/components/app-header/index.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
|
import template from './app-header.html';
|
||||||
|
import logoUrl from '../../assets/images/redash_icon_small.png';
|
||||||
|
import './app-header.css';
|
||||||
|
|
||||||
|
const logger = debug('redash:appHeader');
|
||||||
|
|
||||||
|
function controller($rootScope, $location, $uibModal, Auth, currentUser, Dashboard) {
|
||||||
|
// TODO: logoUrl should come from clientconfig
|
||||||
|
this.logoUrl = logoUrl;
|
||||||
|
this.currentUser = currentUser;
|
||||||
|
this.showQueriesMenu = currentUser.hasPermission('view_query');
|
||||||
|
this.showNewQueryMenu = currentUser.hasPermission('create_query');
|
||||||
|
this.showSettingsMenu = currentUser.hasPermission('list_users');
|
||||||
|
this.showDashboardsMenu = currentUser.hasPermission('list_dashboards');
|
||||||
|
|
||||||
|
this.reloadDashboards = () => {
|
||||||
|
logger('Reloading dashboards.');
|
||||||
|
this.dashboards = Dashboard.recent();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.reloadDashboards();
|
||||||
|
|
||||||
|
$rootScope.$on('reloadDashboards', this.reloadDashboards);
|
||||||
|
|
||||||
|
this.newDashboard = () => {
|
||||||
|
$uibModal.open({
|
||||||
|
component: 'editDashboardDialog',
|
||||||
|
resolve: {
|
||||||
|
dashboard: () => ({ name: null, layout: null }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.searchQueries = () => {
|
||||||
|
$location.path('/queries/search').search({ q: this.term });
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logout = () => {
|
||||||
|
Auth.logout();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('appHeader', {
|
||||||
|
template,
|
||||||
|
controller,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -8,15 +8,29 @@
|
|||||||
<select name="type" class="form-control" ng-options="type.type as type.name for type in types" ng-model="target.type"></select>
|
<select name="type" class="form-control" ng-options="type.type as type.name for type in types" ng-model="target.type"></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" ng-class='{"has-error": !inner.input.$valid}' ng-form="inner" ng-repeat="(name, input) in type.configuration_schema.properties">
|
<div class="form-group" ng-class='{"has-error": !inner.input.$valid}' ng-form="inner" ng-repeat="(name, input) in type.configuration_schema.properties">
|
||||||
<label>{{input.title || name | capitalize}}</label>
|
<label ng-if="input.type !== 'checkbox'">{{input.title || name | capitalize}}</label>
|
||||||
<input name="input" type="{{input.type}}" class="form-control" ng-model="target.options[name]" ng-required="input.required"
|
<input name="input" type="{{input.type}}" class="form-control" ng-model="target.options[name]" ng-required="input.required"
|
||||||
ng-if="input.type !== 'file'" accesskey="tab" placeholder="{{input.default}}">
|
ng-if="input.type !== 'file' && input.type !== 'checkbox'" accesskey="tab" placeholder="{{input.default}}">
|
||||||
|
|
||||||
|
<label ng-if="input.type=='checkbox'">
|
||||||
|
<input name="input" type="{{input.type}}" ng-model="target.options[name]" ng-required="input.required"
|
||||||
|
ng-if="input.type !== 'file'" accesskey="tab" placeholder="{{input.default}}">
|
||||||
|
{{input.title || name | capitalize}}
|
||||||
|
</label>
|
||||||
|
|
||||||
<input name="input" type="file" class="form-control" ng-model="files[name]" ng-required="input.required && !target.options[name]"
|
<input name="input" type="file" class="form-control" ng-model="files[name]" ng-required="input.required && !target.options[name]"
|
||||||
base-sixty-four-input
|
base-sixty-four-input
|
||||||
ng-if="input.type === 'file'">
|
ng-if="input.type === 'file'">
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" ng-disabled="!dataSourceForm.$valid" ng-click="saveChanges()">Save</button>
|
<button class="btn btn-primary" ng-disabled="!dataSourceForm.$valid" ng-click="saveChanges()">Save</button>
|
||||||
|
<span ng-repeat="action in actions">
|
||||||
|
<button class="btn"
|
||||||
|
ng-class="action.class"
|
||||||
|
ng-if="target.id"
|
||||||
|
ng-disabled="(action.disableWhenDirty && dataSourceForm.$dirty) || inProgressActions[action.name]"
|
||||||
|
ng-click="action.callback()" ng-bind-html="action.name"></button>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span ng-transclude>
|
<span ng-transclude>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
108
client/app/components/dynamic-form.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { each, contains, find } from 'underscore';
|
||||||
|
import endsWith from 'underscore.string/endsWith';
|
||||||
|
import template from './dynamic-form.html';
|
||||||
|
|
||||||
|
function DynamicForm($http, toastr, $q) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
replace: 'true',
|
||||||
|
transclude: true,
|
||||||
|
template,
|
||||||
|
scope: {
|
||||||
|
target: '=',
|
||||||
|
type: '@type',
|
||||||
|
actions: '=',
|
||||||
|
},
|
||||||
|
link($scope) {
|
||||||
|
function setType(types) {
|
||||||
|
if ($scope.target.type === undefined) {
|
||||||
|
$scope.target.type = types[0].type;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.type = find(types, t => t.type === $scope.target.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.inProgressActions = {};
|
||||||
|
if ($scope.actions) {
|
||||||
|
$scope.actions.forEach((action) => {
|
||||||
|
const originalCallback = action.callback;
|
||||||
|
const name = action.name;
|
||||||
|
action.callback = () => {
|
||||||
|
action.name = `<i class="zmdi zmdi-spinner zmdi-hc-spin"></i> ${name}`;
|
||||||
|
|
||||||
|
$scope.inProgressActions[action.name] = true;
|
||||||
|
function release() {
|
||||||
|
$scope.inProgressActions[action.name] = false;
|
||||||
|
action.name = name;
|
||||||
|
}
|
||||||
|
originalCallback(release);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.files = {};
|
||||||
|
|
||||||
|
$scope.$watchCollection('files', () => {
|
||||||
|
each($scope.files, (v, k) => {
|
||||||
|
// THis is needed because angular-base64-upload sets the value to null at initialization,
|
||||||
|
// causing the field to be marked as dirty even if it wasn't changed.
|
||||||
|
if (!v && $scope.target.options[k]) {
|
||||||
|
$scope.dataSourceForm.$setPristine();
|
||||||
|
}
|
||||||
|
if (v) {
|
||||||
|
$scope.target.options[k] = v.base64;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const typesPromise = $http.get(`api/${$scope.type}/types`);
|
||||||
|
|
||||||
|
$q.all([typesPromise, $scope.target.$promise]).then((responses) => {
|
||||||
|
const types = responses[0].data;
|
||||||
|
setType(types);
|
||||||
|
|
||||||
|
$scope.types = types;
|
||||||
|
|
||||||
|
types.forEach((type) => {
|
||||||
|
each(type.configuration_schema.properties, (prop, name) => {
|
||||||
|
if (name === 'password' || name === 'passwd') {
|
||||||
|
prop.type = 'password';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endsWith(name, 'File')) {
|
||||||
|
prop.type = 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop.type === 'boolean') {
|
||||||
|
prop.type = 'checkbox';
|
||||||
|
}
|
||||||
|
|
||||||
|
prop.required = contains(type.configuration_schema.required, name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch('target.type', (current, prev) => {
|
||||||
|
if (prev !== current) {
|
||||||
|
if (prev !== undefined) {
|
||||||
|
$scope.target.options = {};
|
||||||
|
}
|
||||||
|
setType($scope.types);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.saveChanges = () => {
|
||||||
|
$scope.target.$save(() => {
|
||||||
|
toastr.success('Saved.');
|
||||||
|
$scope.dataSourceForm.$setPristine();
|
||||||
|
}, () => {
|
||||||
|
toastr.error('Failed saving.');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.directive('dynamicForm', DynamicForm);
|
||||||
|
}
|
||||||
7
client/app/components/dynamic-table/dynamic-table.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
dynamic-table > div {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.sortable-column {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
30
client/app/components/dynamic-table/dynamic-table.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<div>
|
||||||
|
<table class="table table-condensed table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th ng-repeat="column in $ctrl.columns" ng-click="$ctrl.orderBy(column)" class="sortable-column">
|
||||||
|
{{column.title}} <span ng-if="$ctrl.sortIcon(column)"><i class="fa fa-sort-{{$ctrl.sortIcon(column)}}"></i></span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="row in $ctrl.rowsToDisplay">
|
||||||
|
<td ng-repeat="column in $ctrl.columns" ng-bind-html="$ctrl.sanitize(column.formatFunction(row[column.name]))">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<ul uib-pagination total-items="$ctrl.rowsCount"
|
||||||
|
items-per-page="$ctrl.itemsPerPage"
|
||||||
|
ng-model="$ctrl.page"
|
||||||
|
max-size="6"
|
||||||
|
class="pagination"
|
||||||
|
boundary-link-numbers="true"
|
||||||
|
rotate="false"
|
||||||
|
next-text='>'
|
||||||
|
previous-text='<'
|
||||||
|
ng-change="$ctrl.pageChanged()"></ul>
|
||||||
|
</div>
|
||||||
77
client/app/components/dynamic-table/index.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { sortBy } from 'underscore';
|
||||||
|
import template from './dynamic-table.html';
|
||||||
|
import './dynamic-table.css';
|
||||||
|
|
||||||
|
function DynamicTable($sanitize) {
|
||||||
|
'ngInject';
|
||||||
|
|
||||||
|
this.itemsPerPage = this.count = 15;
|
||||||
|
this.page = 1;
|
||||||
|
this.rowsCount = 0;
|
||||||
|
this.orderByField = undefined;
|
||||||
|
this.orderByReverse = false;
|
||||||
|
|
||||||
|
this.pageChanged = () => {
|
||||||
|
const first = this.count * (this.page - 1);
|
||||||
|
const last = this.count * (this.page);
|
||||||
|
|
||||||
|
this.rowsToDisplay = this.rows.slice(first, last);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$onChanges = (changes) => {
|
||||||
|
if (changes.columns) {
|
||||||
|
this.columns = changes.columns.currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.rows) {
|
||||||
|
this.rows = changes.rows.currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rowsCount = this.rows.length;
|
||||||
|
|
||||||
|
this.pageChanged();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.orderBy = (column) => {
|
||||||
|
if (column === this.orderByField) {
|
||||||
|
this.orderByReverse = !this.orderByReverse;
|
||||||
|
} else {
|
||||||
|
this.orderByField = column;
|
||||||
|
this.orderByReverse = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.orderByField) {
|
||||||
|
this.allRows = sortBy(this.allRows, this.orderByField.name);
|
||||||
|
if (this.orderByReverse) {
|
||||||
|
this.allRows = this.allRows.reverse();
|
||||||
|
}
|
||||||
|
this.pageChanged();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.sanitize = value => $sanitize(value);
|
||||||
|
|
||||||
|
this.sortIcon = (column) => {
|
||||||
|
if (column !== this.orderByField) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.orderByReverse) {
|
||||||
|
return 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'asc';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('dynamicTable', {
|
||||||
|
template,
|
||||||
|
controller: DynamicTable,
|
||||||
|
bindings: {
|
||||||
|
rows: '<',
|
||||||
|
columns: '<',
|
||||||
|
count: '<',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
94
client/app/components/edit-in-place.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import $ from 'jquery';
|
||||||
|
import { isEmpty } from 'underscore';
|
||||||
|
|
||||||
|
// From: http://jsfiddle.net/joshdmiller/NDFHg/
|
||||||
|
function EditInPlace() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
value: '=',
|
||||||
|
ignoreBlanks: '=',
|
||||||
|
editable: '=',
|
||||||
|
done: '=',
|
||||||
|
},
|
||||||
|
template(tElement, tAttrs) {
|
||||||
|
const elType = tAttrs.editor || 'input';
|
||||||
|
const placeholder = tAttrs.placeholder || 'Click to edit';
|
||||||
|
|
||||||
|
let viewMode = '';
|
||||||
|
|
||||||
|
if (tAttrs.markdown === 'true') {
|
||||||
|
viewMode = '<span ng-click="editable && edit()" ng-bind-html="value|markdown" ng-class="{editable: editable}"></span>';
|
||||||
|
} else {
|
||||||
|
viewMode = '<span ng-click="editable && edit()" ng-bind="value" ng-class="{editable: editable}"></span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholderSpan = `<span ng-click="editable && edit()"
|
||||||
|
ng-show="editable && !value"
|
||||||
|
ng-class="{editable: editable}">${placeholder}</span>`;
|
||||||
|
const editor = '<{elType} ng-model="value" class="rd-form-control"></{elType}>'.replace('{elType}', elType);
|
||||||
|
|
||||||
|
return viewMode + placeholderSpan + editor;
|
||||||
|
},
|
||||||
|
link($scope, element) {
|
||||||
|
// Let's get a reference to the input element, as we'll want to reference it.
|
||||||
|
const inputElement = $(element.children()[2]);
|
||||||
|
|
||||||
|
// This directive should have a set class so we can style it.
|
||||||
|
element.addClass('edit-in-place');
|
||||||
|
|
||||||
|
// Initially, we're not editing.
|
||||||
|
$scope.editing = false;
|
||||||
|
|
||||||
|
// ng-click handler to activate edit-in-place
|
||||||
|
$scope.edit = () => {
|
||||||
|
$scope.oldValue = $scope.value;
|
||||||
|
|
||||||
|
$scope.editing = true;
|
||||||
|
|
||||||
|
// We control display through a class on the directive itself. See the CSS.
|
||||||
|
element.addClass('active');
|
||||||
|
|
||||||
|
// And we must focus the element.
|
||||||
|
// `angular.element()` provides a chainable array, like jQuery so to access
|
||||||
|
// a native DOM function, we have to reference the first element in the array.
|
||||||
|
inputElement[0].focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
if ($scope.editing) {
|
||||||
|
if ($scope.ignoreBlanks && isEmpty($scope.value)) {
|
||||||
|
$scope.value = $scope.oldValue;
|
||||||
|
}
|
||||||
|
$scope.editing = false;
|
||||||
|
element.removeClass('active');
|
||||||
|
|
||||||
|
if ($scope.value !== $scope.oldValue) {
|
||||||
|
if ($scope.done) {
|
||||||
|
$scope.done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(inputElement).keydown((e) => {
|
||||||
|
// 'return' or 'enter' key pressed
|
||||||
|
// allow 'shift' to break lines
|
||||||
|
if (e.which === 13 && !e.shiftKey) {
|
||||||
|
save();
|
||||||
|
} else if (e.which === 27) {
|
||||||
|
$scope.value = $scope.oldValue;
|
||||||
|
$scope.$apply(() => {
|
||||||
|
$(inputElement[0]).blur();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).blur(() => {
|
||||||
|
save();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.directive('editInPlace', EditInPlace);
|
||||||
|
}
|
||||||
13
client/app/components/email-settings-warning/index.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
function controller(clientConfig, currentUser) {
|
||||||
|
this.showMailWarning = clientConfig.mailSettingsMissing && currentUser.isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('emailSettingsWarning', {
|
||||||
|
bindings: {
|
||||||
|
function: '<',
|
||||||
|
},
|
||||||
|
template: '<p class="alert alert-danger" ng-if="$ctrl.showMailWarning">It looks like your mail server isn\'t configured. Make sure to configure it for the {{$ctrl.function}} to work.</p>',
|
||||||
|
controller,
|
||||||
|
});
|
||||||
|
}
|
||||||
20
client/app/components/error-messages.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const ErrorMessagesComponent = {
|
||||||
|
template: `
|
||||||
|
<div class="help-block" ng-messages="$ctrl.input.$error" ng-show="$ctrl.input.$touched || $ctrl.form.$submitted">
|
||||||
|
<span class="error" ng-message="required">This field is required.</span>
|
||||||
|
<span class="error" ng-message="minlength">This field is too short.</span>
|
||||||
|
<span class="error" ng-message="email">This needs to be a valid email.</span>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
replace: true,
|
||||||
|
bindings: {
|
||||||
|
input: '<',
|
||||||
|
form: '<',
|
||||||
|
},
|
||||||
|
controller() {
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('errorMessages', ErrorMessagesComponent);
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
<div class="container bg-white p-5" ng-show="filters">
|
<div class="container bg-white p-5" ng-show="$ctrl.filters | notEmpty">
|
||||||
<div class="row" ng-show="filters">
|
<div class="row">
|
||||||
<div class="col-sm-6 m-t-5" ng-repeat="filter in filters">
|
<div class="col-sm-6 m-t-5" ng-repeat="filter in $ctrl.filters">
|
||||||
<ui-select ng-model="filter.current" ng-if="!filter.multiple">
|
<ui-select ng-model="filter.current" ng-if="!filter.multiple" on-select="$ctrl.filterChangeListener(filter, $model)" on-remove="$ctrl.filterChangeListener(filter, $model)">
|
||||||
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$select.selected | filterValue:filter}}</ui-select-match>
|
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$select.selected | filterValue:filter}}</ui-select-match>
|
||||||
<ui-select-choices repeat="value in filter.values | filter: $select.search">
|
<ui-select-choices repeat="value in filter.values | filter: $select.search">
|
||||||
{{value | filterValue:filter }}
|
{{value | filterValue:filter }}
|
||||||
</ui-select-choices>
|
</ui-select-choices>
|
||||||
</ui-select>
|
</ui-select>
|
||||||
|
|
||||||
<ui-select ng-model="filter.current" multiple ng-if="filter.multiple">
|
<ui-select ng-model="filter.current" multiple ng-if="filter.multiple" on-select="$ctrl.filterChangeListener(filter, $model)" on-remove="$ctrl.filterChangeListener(filter, $model)">
|
||||||
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$item | filterValue:filter}}</ui-select-match>
|
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$item | filterValue:filter}}</ui-select-match>
|
||||||
<ui-select-choices repeat="value in filter.values | filter: $select.search">
|
<ui-select-choices repeat="value in filter.values | filter: $select.search">
|
||||||
{{value | filterValue:filter }}
|
{{value | filterValue:filter }}
|
||||||
@@ -17,3 +17,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
21
client/app/components/filters.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import template from './filters.html';
|
||||||
|
|
||||||
|
const FiltersComponent = {
|
||||||
|
template,
|
||||||
|
bindings: {
|
||||||
|
onChange: '&',
|
||||||
|
filters: '<',
|
||||||
|
},
|
||||||
|
controller() {
|
||||||
|
'ngInject';
|
||||||
|
|
||||||
|
this.filterChangeListener = (filter, modal) => {
|
||||||
|
this.onChange({ filter, $modal: modal });
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('filters', FiltersComponent);
|
||||||
|
}
|
||||||
8
client/app/components/footer/footer.html
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<div id="footer">
|
||||||
|
<a href="http://redash.io">Redash</a> <span ng-bind="$ctrl.version"></span> <small ng-if="$ctrl.newVersionAvailable" ng-cloak class="ng-cloak"><a href="https://version.redash.io/">(New Redash version available)</a></small>
|
||||||
|
|
||||||
|
<ul class="f-menu">
|
||||||
|
<li><a href="https://redash.io/help/">Documentation</a></li>
|
||||||
|
<li><a href="http://github.com/getredash/redash">Contribute</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
13
client/app/components/footer/index.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import template from './footer.html';
|
||||||
|
|
||||||
|
function controller(clientConfig, currentUser) {
|
||||||
|
this.version = clientConfig.version;
|
||||||
|
this.newVersionAvailable = clientConfig.newVersionAvailable && currentUser.isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('footer', {
|
||||||
|
template,
|
||||||
|
controller,
|
||||||
|
});
|
||||||
|
}
|
||||||
20
client/app/components/index.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export { default as appHeader } from './app-header';
|
||||||
|
export { default as footer } from './footer';
|
||||||
|
export { default as pageHeader } from './page-header';
|
||||||
|
export { default as tabNav } from './tab-nav';
|
||||||
|
export { default as emailSettingsWarning } from './email-settings-warning';
|
||||||
|
export { default as rdTab } from './rd-tab';
|
||||||
|
export { default as queryLink } from './query-link';
|
||||||
|
export { default as parameters } from './parameters';
|
||||||
|
export { default as permissionsEditor } from './permissions-editor';
|
||||||
|
export { default as dynamicTable } from './dynamic-table';
|
||||||
|
export { default as paginator } from './paginator';
|
||||||
|
export { default as settingsScreen } from './settings-screen';
|
||||||
|
export { default as errorMessages } from './error-messages';
|
||||||
|
export { default as editInPlace } from './edit-in-place';
|
||||||
|
export { default as dynamicForm } from './dynamic-form';
|
||||||
|
export { default as rdTimer } from './rd-timer';
|
||||||
|
export { default as rdTimeAgo } from './rd-time-ago';
|
||||||
|
export { default as overlay } from './overlay';
|
||||||
|
export { default as routeStatus } from './route-status';
|
||||||
|
export { default as filters } from './filters';
|
||||||
16
client/app/components/overlay.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const Overlay = {
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<div class="overlay"></div>
|
||||||
|
<div style="width: 100%; position:absolute; top:50px; z-index:2000">
|
||||||
|
<div class="well well-lg" style="width: 70%; margin: auto;" ng-transclude>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
transclude: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('overlay', Overlay);
|
||||||
|
}
|
||||||
16
client/app/components/page-header/index.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import template from './page-header.html';
|
||||||
|
|
||||||
|
function controller() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('pageHeader', {
|
||||||
|
template,
|
||||||
|
controller,
|
||||||
|
transclude: true,
|
||||||
|
bindings: {
|
||||||
|
title: '@',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="row bg-white p-10 p-l-15 p-r-15 m-b-10">
|
<div class="row bg-white p-10 p-l-15 p-r-15 m-b-10">
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<h3>{{title}}</h3>
|
<h3>{{$ctrl.title}}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-3 text-right">
|
<div class="col-sm-3 text-right">
|
||||||
<h3 ng-transclude>
|
<h3 ng-transclude>
|
||||||
31
client/app/components/paginator.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
class PaginatorCtrl {
|
||||||
|
constructor() {
|
||||||
|
this.page = this.paginator.page;
|
||||||
|
}
|
||||||
|
pageChanged() {
|
||||||
|
this.paginator.setPage(this.page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('paginator', {
|
||||||
|
template: `
|
||||||
|
<div class="text-center">
|
||||||
|
<ul uib-pagination total-items="$ctrl.paginator.totalCount"
|
||||||
|
items-per-page="$ctrl.paginator.itemsPerPage"
|
||||||
|
ng-model="$ctrl.page"
|
||||||
|
max-size="6"
|
||||||
|
class="pagination"
|
||||||
|
boundary-link-numbers="true"
|
||||||
|
rotate="false"
|
||||||
|
next-text='>'
|
||||||
|
previous-text='<'
|
||||||
|
ng-change="$ctrl.pageChanged()"></ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
bindings: {
|
||||||
|
paginator: '<',
|
||||||
|
},
|
||||||
|
controller: PaginatorCtrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,21 +1,26 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" aria-label="Close" ng-click="close()"><span aria-hidden="true">×</span></button>
|
<button type="button" class="close" aria-label="Close" ng-click="$ctrl.close()"><span aria-hidden="true">×</span></button>
|
||||||
<h4 class="modal-title">{{parameter.name}}</h4>
|
<h4 class="modal-title">{{$ctrl.parameter.name}}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form">
|
<div class="form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Title</label>
|
<label>Title</label>
|
||||||
<input type="text" class="form-control" ng-model="parameter.title">
|
<input type="text" class="form-control" ng-model="$ctrl.parameter.title">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Type</label>
|
<label>Type</label>
|
||||||
<select ng-model="parameter.type" class="form-control">
|
<select ng-model="$ctrl.parameter.type" class="form-control">
|
||||||
<option value="text">Text</option>
|
<option value="text">Text</option>
|
||||||
<option value="number">Number</option>
|
<option value="number">Number</option>
|
||||||
<option value="date">Date</option>
|
<option value="date">Date</option>
|
||||||
<option value="datetime-local">Date and Time</option>
|
<option value="datetime-local">Date and Time</option>
|
||||||
|
<option value="datetime-with-seconds">Date and Time (with seconds)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Global</label>
|
||||||
|
<input type="checkbox" class="form-inline" ng-model="$ctrl.parameter.global">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
12
client/app/components/parameters.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<div class="form-inline bg-white p-5" ng-if="parameters | notEmpty" ui-sortable="{ 'ui-floating': true, 'disabled': !editable }" ng-model="parameters">
|
||||||
|
<div class="form-group" ng-repeat="param in parameters">
|
||||||
|
<label>{{param.title}}</label>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="showParameterSettings(param)" ng-if="editable"><i class="zmdi zmdi-settings"></i></button>
|
||||||
|
<span ng-switch="param.type">
|
||||||
|
<input ng-switch-when="datetime-with-seconds" type="datetime-local" step="1" class="form-control" ng-model="param.ngModel">
|
||||||
|
<input ng-switch-when="datetime-local" type="datetime-local" class="form-control" ng-model="param.ngModel">
|
||||||
|
<input ng-switch-when="date" type="date" class="form-control" ng-model="param.ngModel">
|
||||||
|
<input ng-switch-default type="{{param.type}}" class="form-control" ng-model="param.ngModel">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
59
client/app/components/parameters.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import template from './parameters.html';
|
||||||
|
import parameterSettingsTemplate from './parameter-settings.html';
|
||||||
|
|
||||||
|
const ParameterSettingsComponent = {
|
||||||
|
template: parameterSettingsTemplate,
|
||||||
|
bindings: {
|
||||||
|
resolve: '<',
|
||||||
|
close: '&',
|
||||||
|
dismiss: '&',
|
||||||
|
},
|
||||||
|
controller() {
|
||||||
|
'ngInject';
|
||||||
|
|
||||||
|
this.parameter = this.resolve.parameter;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function ParametersDirective($location, $uibModal) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
transclude: true,
|
||||||
|
scope: {
|
||||||
|
parameters: '=',
|
||||||
|
syncValues: '=?',
|
||||||
|
editable: '=?',
|
||||||
|
changed: '&onChange',
|
||||||
|
},
|
||||||
|
template,
|
||||||
|
link(scope) {
|
||||||
|
// is this the correct location for this logic?
|
||||||
|
if (scope.syncValues !== false) {
|
||||||
|
scope.$watch('parameters', () => {
|
||||||
|
if (scope.changed) {
|
||||||
|
scope.changed({});
|
||||||
|
}
|
||||||
|
scope.parameters.forEach((param) => {
|
||||||
|
if (param.value !== null || param.value !== '') {
|
||||||
|
$location.search(`p_${param.name}`, param.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.showParameterSettings = (param) => {
|
||||||
|
$uibModal.open({
|
||||||
|
component: 'parameterSettings',
|
||||||
|
resolve: {
|
||||||
|
parameter: param,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.directive('parameters', ParametersDirective);
|
||||||
|
ngModule.component('parameterSettings', ParameterSettingsComponent);
|
||||||
|
}
|
||||||
79
client/app/components/permissions-editor/index.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { contains, each } from 'underscore';
|
||||||
|
import template from './permissions-editor.html';
|
||||||
|
|
||||||
|
const PermissionsEditorComponent = {
|
||||||
|
template,
|
||||||
|
bindings: {
|
||||||
|
resolve: '<',
|
||||||
|
close: '&',
|
||||||
|
dismiss: '&',
|
||||||
|
},
|
||||||
|
controller($http, User) {
|
||||||
|
'ngInject';
|
||||||
|
|
||||||
|
this.grantees = [];
|
||||||
|
this.newGrantees = {};
|
||||||
|
this.aclUrl = this.resolve.aclUrl.url;
|
||||||
|
|
||||||
|
// List users that are granted permissions
|
||||||
|
const loadGrantees = () => {
|
||||||
|
$http.get(this.aclUrl).success((result) => {
|
||||||
|
this.grantees = [];
|
||||||
|
|
||||||
|
each(result, (grantees, accessType) => {
|
||||||
|
grantees.forEach((grantee) => {
|
||||||
|
grantee.access_type = accessType;
|
||||||
|
this.grantees.push(grantee);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
loadGrantees();
|
||||||
|
|
||||||
|
// Search for user
|
||||||
|
this.findUser = (search) => {
|
||||||
|
if (search === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.foundUsers === undefined) {
|
||||||
|
User.query((users) => {
|
||||||
|
const existingIds = this.grantees.map(m => m.id);
|
||||||
|
users.forEach((user) => { user.alreadyGrantee = contains(existingIds, user.id); });
|
||||||
|
this.foundUsers = users;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add new user to grantees list
|
||||||
|
this.addGrantee = (user) => {
|
||||||
|
this.newGrantees.selected = undefined;
|
||||||
|
const body = { access_type: 'modify', user_id: user.id };
|
||||||
|
$http.post(this.aclUrl, body).success(() => {
|
||||||
|
user.alreadyGrantee = true;
|
||||||
|
loadGrantees();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove user from grantees list
|
||||||
|
this.removeGrantee = (user) => {
|
||||||
|
const body = { access_type: 'modify', user_id: user.id };
|
||||||
|
$http({ url: this.aclUrl,
|
||||||
|
method: 'DELETE',
|
||||||
|
data: body,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}).success(() => {
|
||||||
|
this.grantees = this.grantees.filter(m => m !== user);
|
||||||
|
|
||||||
|
if (this.foundUsers) {
|
||||||
|
this.foundUsers.forEach((u) => { if (u.id === user.id) { u.alreadyGrantee = false; } });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('permissionsEditor', PermissionsEditorComponent);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" aria-label="Close" ng-click="$ctrl.close()"><span aria-hidden="true">×</span></button>
|
||||||
|
<h4 class="modal-title">Manage Permissions</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div style="overflow: auto; height: 300px">
|
||||||
|
<ui-select ng-model="$ctrl.newGrantee.selected" on-select="$ctrl.addGrantee($item)">
|
||||||
|
<ui-select-match placeholder="Add New User"></ui-select-match>
|
||||||
|
<ui-select-choices repeat="user in $ctrl.foundUsers | filter:$select.search"
|
||||||
|
refresh="$ctrl.findUser($select.search)"
|
||||||
|
refresh-delay="0"
|
||||||
|
ui-disable-choice="user.alreadyGrantee">
|
||||||
|
<div>
|
||||||
|
<img ng-src="{{user.gravatar_url}}" height="24px"> {{user.name}}
|
||||||
|
<small ng-if="user.alreadyGrantee">(already has permission)</small>
|
||||||
|
</div>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<br/>
|
||||||
|
<table class="table table-condensed table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Permission</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="grantee in $ctrl.grantees">
|
||||||
|
<td width="50px"><img ng-src="{{grantee.gravatar_url}}" height="40px"/></td>
|
||||||
|
<td>{{grantee.name}} </td>
|
||||||
|
<td>{{grantee.access_type}}</td>
|
||||||
|
<td><button class="pull-right btn btn-sm btn-danger" ng-click="$ctrl.removeGrantee(grantee)">Remove</button></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
25
client/app/components/query-link.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
function QueryLinkController() {
|
||||||
|
let hash = null;
|
||||||
|
if (this.visualization) {
|
||||||
|
if (this.visualization.type === 'TABLE') {
|
||||||
|
// link to hard-coded table tab instead of the (hidden) visualization tab
|
||||||
|
hash = 'table';
|
||||||
|
} else {
|
||||||
|
hash = this.visualization.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.link = this.query.getUrl(false, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('queryLink', {
|
||||||
|
bindings: {
|
||||||
|
query: '<',
|
||||||
|
visualization: '<',
|
||||||
|
},
|
||||||
|
template: '<a ng-href="{{$ctrl.link}}" class="query-link">{{$ctrl.query.name}}</a>',
|
||||||
|
controller: QueryLinkController,
|
||||||
|
});
|
||||||
|
}
|
||||||
25
client/app/components/rd-tab/index.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
function rdTab($location) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
tabId: '@',
|
||||||
|
name: '@',
|
||||||
|
basePath: '=?',
|
||||||
|
},
|
||||||
|
transclude: true,
|
||||||
|
template: '<li class="rd-tab" ng-class="{active: tabId==selectedTab}"><a href="{{basePath}}#{{tabId}}">{{name}}<span ng-transclude></span></a></li>',
|
||||||
|
replace: true,
|
||||||
|
link(scope) {
|
||||||
|
scope.basePath = scope.basePath || $location.path().substring(1);
|
||||||
|
scope.$watch(() =>
|
||||||
|
scope.$parent.selectedTab
|
||||||
|
, (tab) => {
|
||||||
|
scope.selectedTab = tab;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.directive('rdTab', rdTab);
|
||||||
|
}
|
||||||
15
client/app/components/rd-time-ago.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const RdTimeAgo = {
|
||||||
|
bindings: {
|
||||||
|
value: '=',
|
||||||
|
},
|
||||||
|
controller() {
|
||||||
|
},
|
||||||
|
template: '<span>' +
|
||||||
|
'<span ng-show="$ctrl.value" am-time-ago="$ctrl.value"></span>' +
|
||||||
|
'<span ng-hide="$ctrl.value">-</span>' +
|
||||||
|
'</span>',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('rdTimeAgo', RdTimeAgo);
|
||||||
|
}
|
||||||
30
client/app/components/rd-timer.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
function rdTimer() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: { timestamp: '=' },
|
||||||
|
template: '{{currentTime}}',
|
||||||
|
controller($scope) {
|
||||||
|
$scope.currentTime = '00:00:00';
|
||||||
|
|
||||||
|
// We're using setInterval directly instead of $timeout, to avoid using $apply, to
|
||||||
|
// prevent the digest loop being run every second.
|
||||||
|
let currentTimer = setInterval(() => {
|
||||||
|
$scope.currentTime = moment(moment() - moment($scope.timestamp)).utc().format('HH:mm:ss');
|
||||||
|
$scope.$digest();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
$scope.$on('$destroy', () => {
|
||||||
|
if (currentTimer) {
|
||||||
|
clearInterval(currentTimer);
|
||||||
|
currentTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.directive('rdTimer', rdTimer);
|
||||||
|
}
|
||||||
19
client/app/components/route-status.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('routeStatus', {
|
||||||
|
template: '<overlay ng-if="$ctrl.permissionDenied">You do not have permission to load this page.',
|
||||||
|
|
||||||
|
controller($rootScope) {
|
||||||
|
this.permissionDenied = false;
|
||||||
|
|
||||||
|
$rootScope.$on('$routeChangeSuccess', () => {
|
||||||
|
this.permissionDenied = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$rootScope.$on('$routeChangeError', (event, current, previous, rejection) => {
|
||||||
|
if (rejection.status === 403) {
|
||||||
|
this.permissionDenied = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
<li ng-class="{'active': usersPage }" ng-if="showUsersLink"><a href="users">Users</a></li>
|
<li ng-class="{'active': usersPage }" ng-if="showUsersLink"><a href="users">Users</a></li>
|
||||||
<li ng-class="{'active': groupsPage }" ng-if="showGroupsLink"><a href="groups">Groups</a></li>
|
<li ng-class="{'active': groupsPage }" ng-if="showGroupsLink"><a href="groups">Groups</a></li>
|
||||||
<li ng-class="{'active': destinationsPage }" ng-if="showDestinationsLink"><a href="destinations">Alert Destinations</a></li>
|
<li ng-class="{'active': destinationsPage }" ng-if="showDestinationsLink"><a href="destinations">Alert Destinations</a></li>
|
||||||
|
<li ng-class="{'active': snippetsPage }"><a href="query_snippets">Query Snippets</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div ng-transclude>
|
<div ng-transclude>
|
||||||
24
client/app/components/settings-screen.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import startsWith from 'underscore.string/startsWith';
|
||||||
|
import template from './settings-screen.html';
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.directive('settingsScreen', $location =>
|
||||||
|
({
|
||||||
|
restrict: 'E',
|
||||||
|
transclude: true,
|
||||||
|
template,
|
||||||
|
controller($scope, currentUser) {
|
||||||
|
$scope.usersPage = startsWith($location.path(), '/users');
|
||||||
|
$scope.groupsPage = startsWith($location.path(), '/groups');
|
||||||
|
$scope.dsPage = startsWith($location.path(), '/data_sources');
|
||||||
|
$scope.destinationsPage = startsWith($location.path(), '/destinations');
|
||||||
|
$scope.snippetsPage = startsWith($location.path(), '/query_snippets');
|
||||||
|
|
||||||
|
$scope.showGroupsLink = currentUser.hasPermission('list_users');
|
||||||
|
$scope.showUsersLink = currentUser.hasPermission('list_users');
|
||||||
|
$scope.showDsLink = currentUser.hasPermission('admin');
|
||||||
|
$scope.showDestinationsLink = currentUser.hasPermission('admin');
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
23
client/app/components/tab-nav/index.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import startsWith from 'underscore.string/startsWith';
|
||||||
|
|
||||||
|
function controller($location) {
|
||||||
|
this.tabs.forEach((tab) => {
|
||||||
|
if (tab.isActive) {
|
||||||
|
tab.active = tab.isActive($location.path());
|
||||||
|
} else {
|
||||||
|
tab.active = startsWith($location.path(), `/${tab.path}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('tabNav', {
|
||||||
|
template: '<ul class="tab-nav bg-white">' +
|
||||||
|
'<li ng-repeat="tab in $ctrl.tabs" ng-class="{\'active\': tab.active }"><a ng-href="{{tab.path}}">{{tab.name}}</a></li>' +
|
||||||
|
'</ul>',
|
||||||
|
controller,
|
||||||
|
bindings: {
|
||||||
|
tabs: '<',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
15
client/app/components/visualization-name.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
function VisualizationName(Visualization) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
visualization: '=',
|
||||||
|
},
|
||||||
|
template: '{{name}}',
|
||||||
|
replace: false,
|
||||||
|
link(scope) {
|
||||||
|
if (Visualization.visualizations[scope.visualization.type].name !== scope.visualization.name) {
|
||||||
|
scope.name = scope.visualization.name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
78
client/app/directives/index.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
|
const logger = debug('redash:directives');
|
||||||
|
|
||||||
|
function compareTo() {
|
||||||
|
return {
|
||||||
|
require: 'ngModel',
|
||||||
|
scope: {
|
||||||
|
otherModelValue: '=compareTo',
|
||||||
|
},
|
||||||
|
link(scope, element, attributes, ngModel) {
|
||||||
|
const validate = (value) => {
|
||||||
|
ngModel.$setValidity('compareTo', value === scope.otherModelValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.$watch('otherModelValue', () => {
|
||||||
|
validate(ngModel.$modelValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
ngModel.$parsers.push((value) => {
|
||||||
|
validate(value);
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function autofocus($timeout) {
|
||||||
|
return {
|
||||||
|
link(scope, element) {
|
||||||
|
$timeout(() => {
|
||||||
|
element[0].focus();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function TitleService($rootScope) {
|
||||||
|
const Title = {
|
||||||
|
title: 'Redash',
|
||||||
|
set(newTitle) {
|
||||||
|
this.title = newTitle;
|
||||||
|
$rootScope.$broadcast('$titleChange');
|
||||||
|
},
|
||||||
|
get() {
|
||||||
|
return this.title;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
function title($rootScope, Title) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
link(scope, element) {
|
||||||
|
function updateTitle() {
|
||||||
|
const newTitle = Title.get();
|
||||||
|
logger('Updating title to: %s', newTitle);
|
||||||
|
element.text(newTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootScope.$on('$routeChangeSuccess', (event, to) => {
|
||||||
|
if (to.title) {
|
||||||
|
Title.set(to.title);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$rootScope.$on('$titleChange', updateTitle);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.factory('Title', TitleService);
|
||||||
|
ngModule.directive('title', title);
|
||||||
|
ngModule.directive('compareTo', compareTo);
|
||||||
|
ngModule.directive('autofocus', autofocus);
|
||||||
|
}
|
||||||
15
client/app/filters/datetime.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.filter('toMilliseconds', () => value => value * 1000.0);
|
||||||
|
|
||||||
|
ngModule.filter('dateTime', clientConfig =>
|
||||||
|
function dateTime(value) {
|
||||||
|
if (!value) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
return moment(value).format(clientConfig.dateTimeFormat);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
106
client/app/filters/index.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import _capitalize from 'underscore.string/capitalize';
|
||||||
|
import { isEmpty } from 'underscore';
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const urlPattern = /(^|[\s\n]|<br\/?>)((?:https?|ftp):\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|])/gi;
|
||||||
|
|
||||||
|
export function durationHumanize(duration) {
|
||||||
|
let humanized = '';
|
||||||
|
|
||||||
|
if (duration === undefined) {
|
||||||
|
humanized = '-';
|
||||||
|
} else if (duration < 60) {
|
||||||
|
const seconds = Math.round(duration);
|
||||||
|
humanized = `${seconds}s`;
|
||||||
|
} else if (duration > 3600 * 24) {
|
||||||
|
const days = Math.round(parseFloat(duration) / 60.0 / 60.0 / 24.0);
|
||||||
|
humanized = `${days}days`;
|
||||||
|
} else if (duration >= 3600) {
|
||||||
|
const hours = Math.round(parseFloat(duration) / 60.0 / 60.0);
|
||||||
|
humanized = `${hours}h`;
|
||||||
|
} else {
|
||||||
|
const minutes = Math.round(parseFloat(duration) / 60.0);
|
||||||
|
humanized = `${minutes}m`;
|
||||||
|
}
|
||||||
|
return humanized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scheduleHumanize(schedule) {
|
||||||
|
if (schedule === null) {
|
||||||
|
return 'Never';
|
||||||
|
} else if (schedule.match(/\d\d:\d\d/) !== null) {
|
||||||
|
const parts = schedule.split(':');
|
||||||
|
const localTime = moment.utc()
|
||||||
|
.hour(parts[0])
|
||||||
|
.minute(parts[1])
|
||||||
|
.local()
|
||||||
|
.format('HH:mm');
|
||||||
|
|
||||||
|
return `Every day at ${localTime}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Every ${durationHumanize(parseInt(schedule, 10))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toHuman(text) {
|
||||||
|
return text.replace(/_/g, ' ').replace(/(?:^|\s)\S/g, a =>
|
||||||
|
a.toUpperCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function colWidth(widgetWidth) {
|
||||||
|
if (widgetWidth === 0) {
|
||||||
|
return 0;
|
||||||
|
} else if (widgetWidth === 1) {
|
||||||
|
return 6;
|
||||||
|
} else if (widgetWidth === 2) {
|
||||||
|
return 12;
|
||||||
|
}
|
||||||
|
return widgetWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function capitalize(text) {
|
||||||
|
if (text) {
|
||||||
|
return _capitalize(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function linkify(text) {
|
||||||
|
return text.replace(urlPattern, "$1<a href='$2' target='_blank'>$2</a>");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function remove(items, item) {
|
||||||
|
if (items === undefined) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
let notEquals;
|
||||||
|
|
||||||
|
if (item instanceof Array) {
|
||||||
|
notEquals = other => item.indexOf(other) === -1;
|
||||||
|
} else {
|
||||||
|
notEquals = other => item !== other;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i += 1) {
|
||||||
|
if (notEquals(items[i])) {
|
||||||
|
filtered.push(items[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notEmpty(collection) {
|
||||||
|
return !isEmpty(collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showError(field, form) {
|
||||||
|
return (field.$touched && field.$invalid) || form.$submitted;
|
||||||
|
}
|
||||||
|
|
||||||
18
client/app/filters/markdown.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import marked from 'marked';
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.filter('markdown', ($sce, clientConfig) =>
|
||||||
|
function markdown(text) {
|
||||||
|
if (!text) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = marked(String(text));
|
||||||
|
if (clientConfig.allowScriptsInUserInput) {
|
||||||
|
html = $sce.trustAsHtml(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
19
client/app/index.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html ng-app="app">
|
||||||
|
<head lang="en">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<base href="/">
|
||||||
|
<title>Redash</title>
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<section>
|
||||||
|
<div ng-view></div>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
121
client/app/index.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// This polyfill is needed to support PhantomJS which we use to generate PNGs from embeds.
|
||||||
|
import 'core-js/fn/typed/array-buffer';
|
||||||
|
|
||||||
|
import 'material-design-iconic-font/dist/css/material-design-iconic-font.css';
|
||||||
|
import 'font-awesome/css/font-awesome.css';
|
||||||
|
import 'ui-select/dist/select.css';
|
||||||
|
import 'angular-toastr/dist/angular-toastr.css';
|
||||||
|
import 'angular-resizable/src/angular-resizable.css';
|
||||||
|
import 'angular-gridster/dist/angular-gridster.css';
|
||||||
|
import 'pace-progress/themes/blue/pace-theme-minimal.css';
|
||||||
|
|
||||||
|
import 'pace-progress';
|
||||||
|
import debug from 'debug';
|
||||||
|
import angular from 'angular';
|
||||||
|
import ngSanitize from 'angular-sanitize';
|
||||||
|
import ngRoute from 'angular-route';
|
||||||
|
import ngResource from 'angular-resource';
|
||||||
|
import uiBootstrap from 'angular-ui-bootstrap';
|
||||||
|
import uiSelect from 'ui-select';
|
||||||
|
import ngMessages from 'angular-messages';
|
||||||
|
import toastr from 'angular-toastr';
|
||||||
|
import ngUpload from 'angular-base64-upload';
|
||||||
|
import vsRepeat from 'angular-vs-repeat';
|
||||||
|
import 'angular-moment';
|
||||||
|
import 'brace';
|
||||||
|
import 'angular-ui-ace';
|
||||||
|
import 'angular-resizable';
|
||||||
|
import ngGridster from 'angular-gridster';
|
||||||
|
import { each } from 'underscore';
|
||||||
|
|
||||||
|
import './sortable';
|
||||||
|
|
||||||
|
import './assets/css/superflat_redash.css';
|
||||||
|
import './assets/css/redash.css';
|
||||||
|
import './assets/css/main.scss';
|
||||||
|
|
||||||
|
import * as pages from './pages';
|
||||||
|
import * as components from './components';
|
||||||
|
import * as filters from './filters';
|
||||||
|
import * as services from './services';
|
||||||
|
import registerDirectives from './directives';
|
||||||
|
import registerVisualizations from './visualizations';
|
||||||
|
import markdownFilter from './filters/markdown';
|
||||||
|
import dateTimeFilter from './filters/datetime';
|
||||||
|
|
||||||
|
const logger = debug('redash');
|
||||||
|
|
||||||
|
const requirements = [
|
||||||
|
ngRoute, ngResource, ngSanitize, uiBootstrap, ngMessages, uiSelect, 'angularMoment', toastr, 'ui.ace',
|
||||||
|
ngUpload, 'angularResizable', vsRepeat, 'ui.sortable', ngGridster.name,
|
||||||
|
];
|
||||||
|
|
||||||
|
const ngModule = angular.module('app', requirements);
|
||||||
|
|
||||||
|
function registerComponents() {
|
||||||
|
each(components, (register) => {
|
||||||
|
register(ngModule);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerServices() {
|
||||||
|
each(services, (register) => {
|
||||||
|
register(ngModule);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerPages() {
|
||||||
|
each(pages, (registerPage) => {
|
||||||
|
const routes = registerPage(ngModule);
|
||||||
|
|
||||||
|
ngModule.config(($routeProvider) => {
|
||||||
|
each(routes, (route, path) => {
|
||||||
|
logger('Route: ', path);
|
||||||
|
// This is a workaround, to make sure app-header and footer are loaded only
|
||||||
|
// for the authenticated routes.
|
||||||
|
// We should look into switching to ui-router, that has built in support for
|
||||||
|
// such things.
|
||||||
|
route.template = `<app-header></app-header><route-status></route-status>${route.template}<footer></footer>`;
|
||||||
|
route.authenticated = true;
|
||||||
|
$routeProvider.when(path, route);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerFilters() {
|
||||||
|
each(filters, (filter, name) => {
|
||||||
|
ngModule.filter(name, () => filter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
registerDirectives(ngModule);
|
||||||
|
registerServices();
|
||||||
|
registerFilters();
|
||||||
|
markdownFilter(ngModule);
|
||||||
|
dateTimeFilter(ngModule);
|
||||||
|
registerComponents();
|
||||||
|
registerPages();
|
||||||
|
registerVisualizations(ngModule);
|
||||||
|
|
||||||
|
ngModule.config(($routeProvider, $locationProvider, $compileProvider,
|
||||||
|
uiSelectConfig, toastrConfig) => {
|
||||||
|
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|http|data):/);
|
||||||
|
$locationProvider.html5Mode(true);
|
||||||
|
uiSelectConfig.theme = 'bootstrap';
|
||||||
|
|
||||||
|
Object.assign(toastrConfig, {
|
||||||
|
positionClass: 'toast-bottom-right',
|
||||||
|
timeOut: 2000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update ui-select's template to use Font-Awesome instead of glyphicon.
|
||||||
|
ngModule.run(($templateCache, OfflineListener) => { // eslint-disable-line no-unused-vars
|
||||||
|
const templateName = 'bootstrap/match.tpl.html';
|
||||||
|
let template = $templateCache.get(templateName);
|
||||||
|
template = template.replace('glyphicon glyphicon-remove', 'fa fa-remove');
|
||||||
|
$templateCache.put(templateName, template);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ngModule;
|
||||||
10
client/app/pages/admin/index.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import registerStatusPage from './status';
|
||||||
|
import registerOutdatedQueriesPage from './outdated-queries';
|
||||||
|
import registerTasksPage from './tasks';
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
const routes = Object.assign({}, registerStatusPage(ngModule),
|
||||||
|
registerOutdatedQueriesPage(ngModule),
|
||||||
|
registerTasksPage(ngModule));
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
45
client/app/pages/admin/outdated-queries/index.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import { Paginator } from '../../../utils';
|
||||||
|
import template from './outdated-queries.html';
|
||||||
|
|
||||||
|
function OutdatedQueriesCtrl($scope, Events, $http, $timeout) {
|
||||||
|
Events.record('view', 'page', 'admin/outdated_queries');
|
||||||
|
$scope.autoUpdate = true;
|
||||||
|
|
||||||
|
this.queries = new Paginator([], { itemsPerPage: 50 });
|
||||||
|
|
||||||
|
const refresh = () => {
|
||||||
|
if ($scope.autoUpdate) {
|
||||||
|
$scope.refresh_time = moment().add(1, 'minutes');
|
||||||
|
$http.get('/api/admin/queries/outdated').success((data) => {
|
||||||
|
this.queries.updateRows(data.queries);
|
||||||
|
$scope.updatedAt = data.updated_at * 1000.0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = $timeout(refresh, 59 * 1000);
|
||||||
|
|
||||||
|
$scope.$on('$destroy', () => {
|
||||||
|
if (timer) {
|
||||||
|
$timeout.cancel(timer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('outdatedQueriesPage', {
|
||||||
|
template,
|
||||||
|
controller: OutdatedQueriesCtrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
'/admin/queries/outdated': {
|
||||||
|
template: '<outdated-queries-page></outdated-queries-page>',
|
||||||
|
title: 'Outdated Queries',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<page-header title="Admin">
|
||||||
|
</page-header>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="container bg-white p-5">
|
||||||
|
<ul class="tab-nav">
|
||||||
|
<li><a href="admin/status">System Status</a></li>
|
||||||
|
<li><a href="admin/queries/tasks">Queries Queue</a></li>
|
||||||
|
<li class="active"><a href="admin/queries/outdated">Outdated Queries</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<table class="table table-condensed table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Created By</th>
|
||||||
|
<th>Runtime</th>
|
||||||
|
<th>Last Executed At</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
<th>Update Schedule</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="row in $ctrl.queries.getPageRows()">
|
||||||
|
<td>
|
||||||
|
{{row.data_source_id}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a ng-href="queries/{{row.id}}">{{row.name}}</a>
|
||||||
|
</td>
|
||||||
|
<td>{{row.user.name}}</td>
|
||||||
|
<td>{{row.runtime | durationHumanize}}</td>
|
||||||
|
<td>{{row.retrieved_at | dateTime}}</td>
|
||||||
|
<td>{{row.created_at | dateTime }}</td>
|
||||||
|
<td>{{row.schedule | scheduleHumanize}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<paginator paginator="$ctrl.queries"></paginator>
|
||||||
|
|
||||||
|
<div class="badge">
|
||||||
|
Last update: <span am-time-ago="updatedAt"></span>
|
||||||
|
</div>
|
||||||
|
(<label><input type="checkbox" ng-model="autoUpdate"> Auto Update</label>)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
40
client/app/pages/admin/status/index.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import template from './status.html';
|
||||||
|
|
||||||
|
// TODO: switch to $ctrl instead of $scope.
|
||||||
|
function AdminStatusCtrl($scope, $http, $timeout, currentUser, Events) {
|
||||||
|
Events.record('view', 'page', 'admin/status');
|
||||||
|
|
||||||
|
const refresh = () => {
|
||||||
|
$http.get('/status.json').success((data) => {
|
||||||
|
$scope.workers = data.workers;
|
||||||
|
delete data.workers;
|
||||||
|
$scope.manager = data.manager;
|
||||||
|
delete data.manager;
|
||||||
|
$scope.status = data;
|
||||||
|
});
|
||||||
|
|
||||||
|
const timer = $timeout(refresh, 59 * 1000);
|
||||||
|
|
||||||
|
$scope.$on('$destroy', () => {
|
||||||
|
if (timer) {
|
||||||
|
$timeout.cancel(timer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('statusPage', {
|
||||||
|
template,
|
||||||
|
controller: AdminStatusCtrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
'/admin/status': {
|
||||||
|
template: '<status-page></status-page>',
|
||||||
|
title: 'System Status',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<li class="list-group-item active">Queues</li>
|
<li class="list-group-item active">Queues</li>
|
||||||
<li class="list-group-item" ng-repeat="(name, value) in manager.queues">
|
<li class="list-group-item" ng-repeat="(name, value) in manager.queues">
|
||||||
<span class="badge">{{value.size}}</span>
|
<span class="badge">{{value.size}}</span>
|
||||||
{{name}} ({{value.data_sources}})
|
{{name}} <span uib-popover="{{value.data_sources}}" popover-trigger="'mouseenter'"><i class="fa fa-question-circle"></i></span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
32
client/app/pages/admin/tasks/cancel-query-button/index.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
function cancelQueryButton() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
queryId: '=',
|
||||||
|
taskId: '=',
|
||||||
|
},
|
||||||
|
transclude: true,
|
||||||
|
template: '<button class="btn btn-default" ng-disabled="inProgress" ng-click="cancelExecution()"><i class="zmdi zmdi-spinner zmdi-hc-spin" ng-if="inProgress"></i> Cancel</button>',
|
||||||
|
replace: true,
|
||||||
|
controller($scope, $http, currentUser, Events) {
|
||||||
|
$scope.inProgress = false;
|
||||||
|
|
||||||
|
$scope.cancelExecution = () => {
|
||||||
|
$http.delete(`api/jobs/${$scope.taskId}`).success(() => {
|
||||||
|
});
|
||||||
|
|
||||||
|
let queryId = $scope.queryId;
|
||||||
|
if ($scope.queryId === 'adhoc') {
|
||||||
|
queryId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Events.record('cancel_execute', 'query', queryId, { admin: true });
|
||||||
|
$scope.inProgress = true;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.directive('cancelQueryButton', cancelQueryButton);
|
||||||
|
}
|
||||||
63
client/app/pages/admin/tasks/index.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import { Paginator } from '../../../utils';
|
||||||
|
import template from './tasks.html';
|
||||||
|
import registerCancelQueryButton from './cancel-query-button';
|
||||||
|
|
||||||
|
function TasksCtrl($scope, $location, $http, $timeout, Events) {
|
||||||
|
Events.record('view', 'page', 'admin/tasks');
|
||||||
|
$scope.autoUpdate = true;
|
||||||
|
|
||||||
|
$scope.selectedTab = 'in_progress';
|
||||||
|
|
||||||
|
$scope.tasks = {
|
||||||
|
waiting: [],
|
||||||
|
in_progress: [],
|
||||||
|
done: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
this.tasksPaginator = new Paginator([], { itemsPerPage: 50 });
|
||||||
|
|
||||||
|
$scope.setTab = (tab) => {
|
||||||
|
$scope.selectedTab = tab;
|
||||||
|
this.tasksPaginator.updateRows($scope.tasks[tab]);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.setTab($location.hash() || 'in_progress');
|
||||||
|
|
||||||
|
const refresh = () => {
|
||||||
|
if ($scope.autoUpdate) {
|
||||||
|
$scope.refresh_time = moment().add(1, 'minutes');
|
||||||
|
$http.get('/api/admin/queries/tasks').success((data) => {
|
||||||
|
$scope.tasks = data;
|
||||||
|
this.tasksPaginator.updateRows($scope.tasks[$scope.selectedTab]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = $timeout(refresh, 5 * 1000);
|
||||||
|
|
||||||
|
$scope.$on('$destroy', () => {
|
||||||
|
if (timer) {
|
||||||
|
$timeout.cancel(timer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('tasksPage', {
|
||||||
|
template,
|
||||||
|
controller: TasksCtrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
registerCancelQueryButton(ngModule);
|
||||||
|
|
||||||
|
return {
|
||||||
|
'/admin/queries/tasks': {
|
||||||
|
template: '<tasks-page></tasks-page>',
|
||||||
|
title: 'Running Queries',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
54
client/app/pages/admin/tasks/tasks.html
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<page-header title="Admin">
|
||||||
|
</page-header>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="container bg-white p-5">
|
||||||
|
<ul class="tab-nav">
|
||||||
|
<li><a href="admin/status">System Status</a></li>
|
||||||
|
<li class="active"><a href="admin/queries/tasks">Queries Queue</a></li>
|
||||||
|
<li><a href="admin/queries/outdated">Outdated Queries</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="tab-nav">
|
||||||
|
<rd-tab tab-id="in_progress" name="In Progress ({{tasks.in_progress.length}})" ng-click="setTab('in_progress')"></rd-tab>
|
||||||
|
<rd-tab tab-id="waiting" name="Waiting ({{tasks.waiting.length}})" ng-click="setTab('waiting')"></rd-tab>
|
||||||
|
<rd-tab tab-id="done" name="Done" ng-click="setTab('done')"></rd-tab>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<table class="table table-condensed table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Data Source ID</th>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>State</th>
|
||||||
|
<th>Query ID</th>
|
||||||
|
<th>Query Hash</th>
|
||||||
|
<th>Runtime</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
<th>Started At</th>
|
||||||
|
<th>Updated At</th>
|
||||||
|
<th ng-if="selectedTab === 'in_progress'"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="row in $ctrl.tasksPaginator.getPageRows()">
|
||||||
|
<td>{{row.data_source_id}}</td>
|
||||||
|
<td>{{row.username}}</td>
|
||||||
|
<td>{{row.state}} <span ng-if="row.state === 'failed'" uib-popover="{{row.error}}" popover-trigger="mouseenter" class="zmdi zmdi-help"></span></td>
|
||||||
|
<td>{{row.query_id}}</td>
|
||||||
|
<td>{{row.query_hash}}</td>
|
||||||
|
<td>{{row.run_time | durationHumanize}}</td>
|
||||||
|
<td>{{row.created_at | toMilliseconds | dateTime }}</td>
|
||||||
|
<td>{{row.started_at | toMilliseconds | dateTime }}</td>
|
||||||
|
<td>{{row.updated_at | toMilliseconds | dateTime }}</td>
|
||||||
|
<td ng-if="selectedTab === 'in_progress'">
|
||||||
|
<cancel-query-button query-id="dataRow.query_id" task-id="dataRow.task_id"></cancel-query-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<paginator paginator="$ctrl.tasksPaginator"></paginator>
|
||||||
|
|
||||||
|
<label><input type="checkbox" ng-model="autoUpdate"> Auto Update</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||