Compare commits
1841 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c32c2d43f7 | ||
|
|
4e2e3f9077 | ||
|
|
2a27422df9 | ||
|
|
f9e0ce8e9c | ||
|
|
a1d49f13d3 | ||
|
|
26aa199f9c | ||
|
|
4c77f3f914 | ||
|
|
d6be792595 | ||
|
|
59c1ea7f16 | ||
|
|
4d24005eff | ||
|
|
2dab35b614 | ||
|
|
0b61b88f5f | ||
|
|
e5cb58207c | ||
|
|
fc17d1af81 | ||
|
|
e6650e1e2d | ||
|
|
3aa1cd0133 | ||
|
|
e04833c327 | ||
|
|
b743cceb60 | ||
|
|
a0e134d3b5 | ||
|
|
d7fb2d7458 | ||
|
|
b913ce6022 | ||
|
|
1eb7945d16 | ||
|
|
37d0026ee4 | ||
|
|
9cdc2cb2f7 | ||
|
|
a9bff9063e | ||
|
|
380126ee44 | ||
|
|
d8377375b8 | ||
|
|
98ff701f9a | ||
|
|
f5ea3e97d3 | ||
|
|
719e96dd2f | ||
|
|
6c6c0256ba | ||
|
|
723df51cdd | ||
|
|
a0f4e263b2 | ||
|
|
4706bf8060 | ||
|
|
f96a9f659a | ||
|
|
63c273f896 | ||
|
|
622ac6d781 | ||
|
|
8dc564a8bc | ||
|
|
3ae5baef22 | ||
|
|
8d819068b5 | ||
|
|
585e056265 | ||
|
|
1914ed7c7c | ||
|
|
bd216e93e7 | ||
|
|
5e351de896 | ||
|
|
de0e534c77 | ||
|
|
5fa1f9440d | ||
|
|
b3ddc5f8b9 | ||
|
|
8cde5f9673 | ||
|
|
1bb53ca497 | ||
|
|
0a3cd9267f | ||
|
|
075d843354 | ||
|
|
b14e5e8c0e | ||
|
|
c9da4be422 | ||
|
|
276ee7c27a | ||
|
|
334040532a | ||
|
|
335a3a98b5 | ||
|
|
b17080a7f5 | ||
|
|
8441c12b01 | ||
|
|
3b4af1b6fa | ||
|
|
c3deb8e2fa | ||
|
|
a60b1686da | ||
|
|
b56e87ceb2 | ||
|
|
fc89bcdaf3 | ||
|
|
15ec8321bb | ||
|
|
e6ba62485c | ||
|
|
9077b01fb9 | ||
|
|
f45281be96 | ||
|
|
a1c8ef9037 | ||
|
|
f46e8af23f | ||
|
|
30a89bfd2c | ||
|
|
6312f8738d | ||
|
|
9e3d5c10c5 | ||
|
|
59b87ec4fd | ||
|
|
27ecf5f25c | ||
|
|
105971c4c8 | ||
|
|
690f8323c3 | ||
|
|
20eb110ce3 | ||
|
|
571c9d0aee | ||
|
|
0ee7292f16 | ||
|
|
8c28392dfd | ||
|
|
671f1f4478 | ||
|
|
557d3748be | ||
|
|
f00d080ed2 | ||
|
|
4e76c1305f | ||
|
|
36ef388e92 | ||
|
|
2e1ee7f76c | ||
|
|
fc1e38772d | ||
|
|
0e631a5121 | ||
|
|
d74175efca | ||
|
|
bf5fe7d2c7 | ||
|
|
0f022aba92 | ||
|
|
0b6e55e55a | ||
|
|
e1c409366c | ||
|
|
3b942118e9 | ||
|
|
7f1543db8f | ||
|
|
74a5121be2 | ||
|
|
26fe136a1a | ||
|
|
83fb189b05 | ||
|
|
5e8d0d36c0 | ||
|
|
4ae4cffa04 | ||
|
|
bc433e88fe | ||
|
|
513ef501a4 | ||
|
|
f2bdcbedfb | ||
|
|
fd056edb2a | ||
|
|
0f0acfdd12 | ||
|
|
1e3b507b2b | ||
|
|
84d95272f3 | ||
|
|
3b08e9e214 | ||
|
|
f4be83b06f | ||
|
|
4918d0430c | ||
|
|
e25b86b10d | ||
|
|
d3d305a843 | ||
|
|
825b93bfe9 | ||
|
|
8c98282200 | ||
|
|
768ac9eb04 | ||
|
|
71011d2fca | ||
|
|
9683a8ed82 | ||
|
|
10a6ac9313 | ||
|
|
dba325e9a2 | ||
|
|
fcd9ab533c | ||
|
|
68e3e8e1c5 | ||
|
|
7f8b738b9e | ||
|
|
8a35dcedfa | ||
|
|
ef763b7157 | ||
|
|
498e1d4474 | ||
|
|
73de936c75 | ||
|
|
e32b709a41 | ||
|
|
60652f63c4 | ||
|
|
d0d4101f90 | ||
|
|
646875794f | ||
|
|
cdad4be0d5 | ||
|
|
8f4285be62 | ||
|
|
acfa55e2d0 | ||
|
|
0b7cd07db0 | ||
|
|
6297ffd523 | ||
|
|
368f4fdbef | ||
|
|
f52044a209 | ||
|
|
9fb33cf746 | ||
|
|
e3c5da5bc5 | ||
|
|
e675690cc6 | ||
|
|
edc1622cf5 | ||
|
|
5ab3d4a40d | ||
|
|
cb29d87b63 | ||
|
|
6ff6bdad9f | ||
|
|
e3cc3ef9a4 | ||
|
|
1fe4f291f2 | ||
|
|
a54119f4a2 | ||
|
|
c5b7fe5321 | ||
|
|
d487ec9153 | ||
|
|
fa19b1ddc8 | ||
|
|
267c32b390 | ||
|
|
aeff3f1494 | ||
|
|
e80e52f6c9 | ||
|
|
fe41a70602 | ||
|
|
976d9abe2d | ||
|
|
041bc1100a | ||
|
|
5d095ff6ab | ||
|
|
ef01b61b29 | ||
|
|
faad6b656b | ||
|
|
0bc775584b | ||
|
|
f2d96d61a1 | ||
|
|
09bf2dd608 | ||
|
|
ad1b9b06cf | ||
|
|
a4bceae60b | ||
|
|
9385449feb | ||
|
|
562e1bb8c9 | ||
|
|
082b718303 | ||
|
|
c0872899e9 | ||
|
|
086bbf129d | ||
|
|
4b7561e538 | ||
|
|
407c5a839b | ||
|
|
b8aefd26b8 | ||
|
|
85a762bcd2 | ||
|
|
4f1b3d5beb | ||
|
|
9218a7c437 | ||
|
|
71a3f066a5 | ||
|
|
89436d779c | ||
|
|
3631e938da | ||
|
|
c0a9db68f0 | ||
|
|
bec9c9e14e | ||
|
|
47bbc25277 | ||
|
|
f02c2588d2 | ||
|
|
7db5449dad | ||
|
|
7f6c7f0634 | ||
|
|
73955c74f7 | ||
|
|
7de85da8ef | ||
|
|
0aab35252a | ||
|
|
141dbc9e70 | ||
|
|
2e513c347c | ||
|
|
335c136ec2 | ||
|
|
df1170eb9b | ||
|
|
69bcaddbe0 | ||
|
|
67958cc27b | ||
|
|
6c716f23d9 | ||
|
|
bea11b0ac2 | ||
|
|
4927386299 | ||
|
|
30a8550f6b | ||
|
|
0389a45be4 | ||
|
|
707c169867 | ||
|
|
fca034ac0d | ||
|
|
97691ea5ee | ||
|
|
40335a0e21 | ||
|
|
9344cbd078 | ||
|
|
9442fd9465 | ||
|
|
c816f1003d | ||
|
|
2107b79a80 | ||
|
|
8fae6de8c7 | ||
|
|
d798c77574 | ||
|
|
0abce27381 | ||
|
|
8a171ba39a | ||
|
|
20af276772 | ||
|
|
4058342763 | ||
|
|
af64657260 | ||
|
|
b6bd46e59e | ||
|
|
31fe547e03 | ||
|
|
aff324071e | ||
|
|
131266e408 | ||
|
|
b1f97e8c8d | ||
|
|
9783d6e839 | ||
|
|
8eea2fb367 | ||
|
|
b585480c81 | ||
|
|
89e307daba | ||
|
|
a5eb0e293c | ||
|
|
48d1113225 | ||
|
|
d82d5c3bdc | ||
|
|
dfe58b3953 | ||
|
|
44019b8357 | ||
|
|
3c15a44faf | ||
|
|
8d113dadd2 | ||
|
|
c1dd26aee7 | ||
|
|
b2228c2a39 | ||
|
|
d9618cb09c | ||
|
|
c8ca683d3a | ||
|
|
888963ffaa | ||
|
|
ae947a8310 | ||
|
|
bee9cde347 | ||
|
|
c131dab125 | ||
|
|
e113642ae4 | ||
|
|
b76906b168 | ||
|
|
3960005002 | ||
|
|
3dde578b86 | ||
|
|
813f0e74ff | ||
|
|
1e4e37c2ce | ||
|
|
a00c80eab2 | ||
|
|
496e5ebe8c | ||
|
|
18cc8434a0 | ||
|
|
5eba318019 | ||
|
|
63274dbb17 | ||
|
|
4c73e788ae | ||
|
|
b71a2b3651 | ||
|
|
521a32dfff | ||
|
|
fd6ebe6e12 | ||
|
|
6fb97675ad | ||
|
|
c0c102207d | ||
|
|
3b9d9ac75d | ||
|
|
2536fd57ed | ||
|
|
d941e5e5b1 | ||
|
|
039b0a89bb | ||
|
|
febf9939c8 | ||
|
|
bb84c6dab8 | ||
|
|
cddc00e2cc | ||
|
|
091e3d41e1 | ||
|
|
9dc3a35c1a | ||
|
|
f8878d3006 | ||
|
|
1c0d596f26 | ||
|
|
1afd2ab388 | ||
|
|
4aa9500402 | ||
|
|
4a8a4482fc | ||
|
|
d83849a1b5 | ||
|
|
44272f5d66 | ||
|
|
83727ae931 | ||
|
|
0b0b88a255 | ||
|
|
f23d709f4e | ||
|
|
88abbc7ea6 | ||
|
|
16f0413af8 | ||
|
|
f47020a64d | ||
|
|
55e1ef81f7 | ||
|
|
6bb43d0411 | ||
|
|
f51c2328c9 | ||
|
|
fd37188ace | ||
|
|
758e27ce91 | ||
|
|
9a3b25eb50 | ||
|
|
6da890dfb8 | ||
|
|
0d35ec7139 | ||
|
|
dc0f9a63cb | ||
|
|
21c042996e | ||
|
|
5f22adadf2 | ||
|
|
4e8888ce2f | ||
|
|
0a69609d38 | ||
|
|
2dbcd88313 | ||
|
|
6b0775f7c7 | ||
|
|
e85d3c3c9f | ||
|
|
e20f57bba8 | ||
|
|
933ace2e38 | ||
|
|
4c1e5aed6b | ||
|
|
77d982b4aa | ||
|
|
02c8163265 | ||
|
|
ef868dbb6e | ||
|
|
b2bab33baa | ||
|
|
149e0835f8 | ||
|
|
50bed1d8f2 | ||
|
|
d4b5d78743 | ||
|
|
7fc82a2562 | ||
|
|
92fb138c2c | ||
|
|
71b4b45a3c | ||
|
|
07f4a1b227 | ||
|
|
e116e88e98 | ||
|
|
2278a181ca | ||
|
|
98dc75a404 | ||
|
|
536918aab3 | ||
|
|
c75ac80c7a | ||
|
|
522d8542e9 | ||
|
|
562df44c22 | ||
|
|
86e6798c96 | ||
|
|
db7a287e82 | ||
|
|
518206f208 | ||
|
|
bcee1e12b4 | ||
|
|
410f4f35e2 | ||
|
|
84ea9fec43 | ||
|
|
cda82b7adc | ||
|
|
f2d8c2020b | ||
|
|
1b82ecbc46 | ||
|
|
e381331c36 | ||
|
|
ff58247987 | ||
|
|
dcf0d2cbe3 | ||
|
|
eb99fa5671 | ||
|
|
ce3e19f212 | ||
|
|
44dca6da01 | ||
|
|
34c9fee540 | ||
|
|
e0b13b2ffa | ||
|
|
df362c12b6 | ||
|
|
0d1f8c948a | ||
|
|
f523378326 | ||
|
|
b0f9e49709 | ||
|
|
b6dbb4e3f8 | ||
|
|
3f6a0e8ffa | ||
|
|
a7bcc6d31e | ||
|
|
8aa2d8e70a | ||
|
|
4720e12be7 | ||
|
|
5463591f0d | ||
|
|
2a0198fba8 | ||
|
|
652f214b25 | ||
|
|
aa49780134 | ||
|
|
f483b61cfb | ||
|
|
38a189b671 | ||
|
|
c2331988db | ||
|
|
eff5bdb454 | ||
|
|
bd1babec3a | ||
|
|
d43c2bbf62 | ||
|
|
87db8099d6 | ||
|
|
ebea118c7d | ||
|
|
297ac5c9bd | ||
|
|
9b23fb4235 | ||
|
|
0a71f5e22d | ||
|
|
0a8aaceb85 | ||
|
|
00979f3ad7 | ||
|
|
c7b48837f2 | ||
|
|
418c5322c1 | ||
|
|
dc5b4c26a3 | ||
|
|
9ed0a5ba85 | ||
|
|
db0770fc17 | ||
|
|
9bb58e71d2 | ||
|
|
560598eaad | ||
|
|
f9144fc927 | ||
|
|
883bf173c0 | ||
|
|
3f2bb65b32 | ||
|
|
3917af019a | ||
|
|
e88837e835 | ||
|
|
7abdc2543e | ||
|
|
91ab90a6fe | ||
|
|
7fd2bd3d24 | ||
|
|
3ed1ea1e33 | ||
|
|
a4486c56b9 | ||
|
|
3da0ecf36c | ||
|
|
11a1095b18 | ||
|
|
b43485f322 | ||
|
|
d83675326b | ||
|
|
8d7b9a552e | ||
|
|
e1eb75b786 | ||
|
|
34a3c9e91c | ||
|
|
e007a2891d | ||
|
|
febe6e4aa7 | ||
|
|
8099dafc68 | ||
|
|
ce3d5e637f | ||
|
|
4a52ccd4fa | ||
|
|
a0c81f8a31 | ||
|
|
ce13b79bdc | ||
|
|
c580db277d | ||
|
|
5e944e9a8f | ||
|
|
4b94cf706a | ||
|
|
364c51456d | ||
|
|
1274d36abc | ||
|
|
f6bd562dd2 | ||
|
|
065d2bc2c6 | ||
|
|
653ed1c57a | ||
|
|
7dc1176628 | ||
|
|
365b8a8c93 | ||
|
|
6e1e0a9967 | ||
|
|
170640a63f | ||
|
|
5e970b73d5 | ||
|
|
a4643472a5 | ||
|
|
7aa01f2bd2 | ||
|
|
cb4b0e0296 | ||
|
|
2c05e921c4 | ||
|
|
c4877f254e | ||
|
|
9fc59de35f | ||
|
|
eb50f3fc94 | ||
|
|
12fe59827f | ||
|
|
d32caff31d | ||
|
|
ba540ff380 | ||
|
|
2112faab02 | ||
|
|
34c6be398a | ||
|
|
3f9c2a5592 | ||
|
|
8076b7f0b7 | ||
|
|
8940d66b0b | ||
|
|
948e2247e4 | ||
|
|
eba2ba1918 | ||
|
|
59d5ba9273 | ||
|
|
4aba24a976 | ||
|
|
762c331ddf | ||
|
|
9592610f8b | ||
|
|
8b7399ddc9 | ||
|
|
f6221da9dc | ||
|
|
10c84d2cd0 | ||
|
|
60d784d7bc | ||
|
|
b28e4be8d7 | ||
|
|
e74b36996f | ||
|
|
4c28d11259 | ||
|
|
b1e1a32f37 | ||
|
|
a12b43265d | ||
|
|
c2d621ae0f | ||
|
|
d93e07061b | ||
|
|
cb59973b9a | ||
|
|
72e41a94e4 | ||
|
|
9013497fc7 | ||
|
|
a74ae32122 | ||
|
|
9cfae349da | ||
|
|
a16718917b | ||
|
|
e2e365d9ff | ||
|
|
5310498d0f | ||
|
|
bb1d2f8805 | ||
|
|
0d5f001d38 | ||
|
|
236f7f9c04 | ||
|
|
74bf8e5239 | ||
|
|
71e125b4b0 | ||
|
|
6a8befc641 | ||
|
|
a79aa382d7 | ||
|
|
5698f9692a | ||
|
|
b2381f6933 | ||
|
|
9a732a4dbf | ||
|
|
17eb7e4146 | ||
|
|
16a6c96c22 | ||
|
|
bc0a5160ac | ||
|
|
62ab1fda80 | ||
|
|
b5309833ee | ||
|
|
7b932507a6 | ||
|
|
c9fda5e6f1 | ||
|
|
a274bde092 | ||
|
|
b4024ec880 | ||
|
|
6367943d31 | ||
|
|
eaa83556c3 | ||
|
|
7e720bcecd | ||
|
|
003c285d11 | ||
|
|
54687e72bd | ||
|
|
8c59386dc9 | ||
|
|
0369c557a4 | ||
|
|
1ca95dc497 | ||
|
|
85ea9060b0 | ||
|
|
19b4ec7102 | ||
|
|
b2fea7f2fe | ||
|
|
d5947669ab | ||
|
|
4cb97db98e | ||
|
|
9b5d43067a | ||
|
|
8731a8d273 | ||
|
|
08a06b0792 | ||
|
|
90157157df | ||
|
|
f5ea1f1559 | ||
|
|
cf89e6b184 | ||
|
|
5920747122 | ||
|
|
2fff4f4036 | ||
|
|
442ece5a4f | ||
|
|
4bbf04b68a | ||
|
|
f74af231ce | ||
|
|
ffa679e04b | ||
|
|
8f1d267c00 | ||
|
|
af61517384 | ||
|
|
15a7374a4b | ||
|
|
c0fe4a7c84 | ||
|
|
2a18c4493b | ||
|
|
fc60c1b86a | ||
|
|
5b998269b3 | ||
|
|
914378cc65 | ||
|
|
30f98e9796 | ||
|
|
2b524075d9 | ||
|
|
3641e332b0 | ||
|
|
4ce3f4eaa9 | ||
|
|
0b173e67a5 | ||
|
|
2af234d180 | ||
|
|
d751fd8c8c | ||
|
|
35552f9b77 | ||
|
|
1cc36b481a | ||
|
|
c9b95bc359 | ||
|
|
86d64c35ab | ||
|
|
8712c8567c | ||
|
|
b0cc646b5e | ||
|
|
8e1c852b0d | ||
|
|
349f67337d | ||
|
|
4af979d3eb | ||
|
|
727cc67f19 | ||
|
|
f51df00564 | ||
|
|
8d7044a81a | ||
|
|
d1c62b106d | ||
|
|
a1dcf94d4d | ||
|
|
53fc9bbf54 | ||
|
|
7755e9859d | ||
|
|
21f3a80940 | ||
|
|
06910d9002 | ||
|
|
5777070bec | ||
|
|
8e3adcd283 | ||
|
|
381ab62505 | ||
|
|
93491004e2 | ||
|
|
d1f0ae9538 | ||
|
|
94bb55d66b | ||
|
|
9de6996dc8 | ||
|
|
9636359497 | ||
|
|
9a6b40aff9 | ||
|
|
82dee49a43 | ||
|
|
9b4482f25d | ||
|
|
4caf1ac3d3 | ||
|
|
0cda4a6632 | ||
|
|
a80618fbe2 | ||
|
|
310808f1fb | ||
|
|
939168773a | ||
|
|
c6a415535e | ||
|
|
ce87c7b736 | ||
|
|
036eb46ea4 | ||
|
|
95ad15057b | ||
|
|
459309ee4e | ||
|
|
4e0069810e | ||
|
|
5a62e90f17 | ||
|
|
cf689c424f | ||
|
|
dad9eb21a0 | ||
|
|
8b581368dc | ||
|
|
ca093ec235 | ||
|
|
c6e210f107 | ||
|
|
e2d0285496 | ||
|
|
16125327b1 | ||
|
|
d8d666c971 | ||
|
|
772ea94b59 | ||
|
|
e499e8099d | ||
|
|
75bc9bb318 | ||
|
|
f79362c7a3 | ||
|
|
2c34ecde35 | ||
|
|
1610d9b782 | ||
|
|
17dd4efb27 | ||
|
|
7a2af73bea | ||
|
|
81d027611f | ||
|
|
9ef941bc63 | ||
|
|
cb0d27e691 | ||
|
|
03767bbc0a | ||
|
|
0042b73cd9 | ||
|
|
1c095bcd99 | ||
|
|
4287d9a2e2 | ||
|
|
e297faab7c | ||
|
|
c0329cc0ef | ||
|
|
dc7050d4ef | ||
|
|
3a2f2be95d | ||
|
|
b4432ee21d | ||
|
|
d9b0e84bbe | ||
|
|
e8c946b88b | ||
|
|
7b94260135 | ||
|
|
51c59dad63 | ||
|
|
2d398696d0 | ||
|
|
ceb08808f8 | ||
|
|
e7c6ba8c1d | ||
|
|
3cee9c9b3a | ||
|
|
509edf651b | ||
|
|
28224a0ba1 | ||
|
|
4e8cd93905 | ||
|
|
069fe38354 | ||
|
|
05c915cf00 | ||
|
|
37512b5fdd | ||
|
|
0fa22500be | ||
|
|
3fbc73d181 | ||
|
|
4d4f41733d | ||
|
|
113821cc97 | ||
|
|
3f9ba7ff00 | ||
|
|
37bf79c9eb | ||
|
|
073deb8315 | ||
|
|
38293fc155 | ||
|
|
7793b3fe41 | ||
|
|
52f44588e6 | ||
|
|
25de0303a1 | ||
|
|
0ffda9d002 | ||
|
|
a37aa11baf | ||
|
|
e7331633a4 | ||
|
|
1ae40981fe | ||
|
|
19743f387b | ||
|
|
17bb5eac91 | ||
|
|
77d628d2db | ||
|
|
e5348bcf9f | ||
|
|
bcce69904d | ||
|
|
ee7e452c70 | ||
|
|
7b4c04024c | ||
|
|
73402a4f3c | ||
|
|
a40da45b1e | ||
|
|
42a3309731 | ||
|
|
638fb123ec | ||
|
|
f2e06e6191 | ||
|
|
f95a09a015 | ||
|
|
a10a38575b | ||
|
|
b74f4639a0 | ||
|
|
c7efe3a99f | ||
|
|
a7b10db3f4 | ||
|
|
cc544e9343 | ||
|
|
0a301bd997 | ||
|
|
2abffff9fd | ||
|
|
174eb2408e | ||
|
|
e91c9a00b1 | ||
|
|
3b6af18009 | ||
|
|
c9608dfa4f | ||
|
|
ab2fa1e352 | ||
|
|
bd0b5c7136 | ||
|
|
9a025a7e05 | ||
|
|
d198a99419 | ||
|
|
96081de51f | ||
|
|
16c461c15f | ||
|
|
1bf56899f3 | ||
|
|
c874a2218b | ||
|
|
79b4c86520 | ||
|
|
d92d994532 | ||
|
|
1704914d6b | ||
|
|
9c43b55668 | ||
|
|
cddd7e909d | ||
|
|
9a6852db78 | ||
|
|
2270042c0f | ||
|
|
6ae3a7552a | ||
|
|
8e5e37ee1b | ||
|
|
146131761f | ||
|
|
855aecd85f | ||
|
|
cdf6a1994b | ||
|
|
a7ce5246a6 | ||
|
|
6efd830bd4 | ||
|
|
a8ea811fed | ||
|
|
f39a848aa2 | ||
|
|
a71b99a873 | ||
|
|
9f2fc1f90a | ||
|
|
391c220604 | ||
|
|
fd9d71b927 | ||
|
|
e5bf431987 | ||
|
|
ba8a39db57 | ||
|
|
f23b434972 | ||
|
|
191ad19cac | ||
|
|
ef366df1fb | ||
|
|
14112fd45b | ||
|
|
2caf02b4e0 | ||
|
|
676cf32c22 | ||
|
|
b7a0b7454a | ||
|
|
289d38b2a6 | ||
|
|
fa2986a154 | ||
|
|
850ac9f4c8 | ||
|
|
084e9f8394 | ||
|
|
4ffd21be09 | ||
|
|
3e87fff8b1 | ||
|
|
a37c1eb589 | ||
|
|
7d0324be91 | ||
|
|
63c85deb5c | ||
|
|
2938e57980 | ||
|
|
ac89584083 | ||
|
|
413dd61491 | ||
|
|
74f9d85752 | ||
|
|
08d6a90469 | ||
|
|
b85c535c6f | ||
|
|
f50799cc7b | ||
|
|
e8aba6b682 | ||
|
|
a2dbc76116 | ||
|
|
163ee33ae6 | ||
|
|
83933e24ac | ||
|
|
a9f24669b7 | ||
|
|
638df29d95 | ||
|
|
73d99031b7 | ||
|
|
2e01d57c9b | ||
|
|
6f6c1678ff | ||
|
|
d26b822f6c | ||
|
|
976dc1e496 | ||
|
|
c49fbe1ac2 | ||
|
|
6a7e322b97 | ||
|
|
4b6b1984aa | ||
|
|
0e564bc8f8 | ||
|
|
8a546b4193 | ||
|
|
6fe733aeaa | ||
|
|
31c09dd7ce | ||
|
|
af18670131 | ||
|
|
98f0bc0188 | ||
|
|
362e5b820e | ||
|
|
36d27dfd74 | ||
|
|
2204c437a2 | ||
|
|
9edd8313ec | ||
|
|
95bcffc28a | ||
|
|
790cbd95b1 | ||
|
|
efdaf4cf3a | ||
|
|
5dd8b102e1 | ||
|
|
04d92ce14b | ||
|
|
43496ecdb2 | ||
|
|
fec6c8b6a7 | ||
|
|
ff099b4314 | ||
|
|
78da5ae92e | ||
|
|
6ab4c4551a | ||
|
|
59a8c0c2c2 | ||
|
|
851c080c13 | ||
|
|
cb800c5907 | ||
|
|
0daf715152 | ||
|
|
31cc6fdaeb | ||
|
|
e335398ba7 | ||
|
|
1a8611a3c0 | ||
|
|
8178900d56 | ||
|
|
258e3c957d | ||
|
|
9f9d78fd7a | ||
|
|
1d83021ab3 | ||
|
|
d9af5d3943 | ||
|
|
7ed9dc90d3 | ||
|
|
433e004295 | ||
|
|
f3628f7bba | ||
|
|
314a75f8a2 | ||
|
|
185b1c9df0 | ||
|
|
a686baa372 | ||
|
|
881e44fbb6 | ||
|
|
a4518dc2aa | ||
|
|
d7e1328fc0 | ||
|
|
9b8c3872c6 | ||
|
|
2c7a6004c0 | ||
|
|
5a0f524b5e | ||
|
|
6d62f0d2c9 | ||
|
|
0551e992fa | ||
|
|
8615429e0c | ||
|
|
1b0d315b30 | ||
|
|
bd67c2ff21 | ||
|
|
577fdffc7f | ||
|
|
65e8bef22c | ||
|
|
241d31f608 | ||
|
|
c84f18449b | ||
|
|
57a23a1181 | ||
|
|
718577f565 | ||
|
|
c2e4e19004 | ||
|
|
69f14c3a61 | ||
|
|
52441ec5b4 | ||
|
|
fcda122107 | ||
|
|
01b908539b | ||
|
|
d7f6b589cd | ||
|
|
eca62cd1f2 | ||
|
|
4de9bf2d61 | ||
|
|
67ec5614e1 | ||
|
|
599f12fdc2 | ||
|
|
a92ef02b07 | ||
|
|
18d16bb92d | ||
|
|
45d11d3227 | ||
|
|
26365054bf | ||
|
|
3cefa004cd | ||
|
|
58a22c0a97 | ||
|
|
d3852db164 | ||
|
|
cce4a08b54 | ||
|
|
b242295de0 | ||
|
|
f80a940ff4 | ||
|
|
a37142426c | ||
|
|
794d8ddfcf | ||
|
|
271d577074 | ||
|
|
7adf4bf763 | ||
|
|
2fd3033418 | ||
|
|
e50aa536c2 | ||
|
|
74de143636 | ||
|
|
2d3348b1a9 | ||
|
|
81ca8b9012 | ||
|
|
df733d3e9c | ||
|
|
0167bebf04 | ||
|
|
b1d6a5a45a | ||
|
|
5de1795380 | ||
|
|
3bb26c5906 | ||
|
|
99a9fdde25 | ||
|
|
e2f9b7565b | ||
|
|
3e6dd8e929 | ||
|
|
6556f22e91 | ||
|
|
c0fc7c8222 | ||
|
|
e5377abf0f | ||
|
|
1eb2d562a5 | ||
|
|
b4625f1c78 | ||
|
|
82f5f15c2a | ||
|
|
63037c62a0 | ||
|
|
a696e10ef7 | ||
|
|
617bbc213f | ||
|
|
87933bd8ac | ||
|
|
9e3cb6e581 | ||
|
|
29f01a5780 | ||
|
|
d4dfc67059 | ||
|
|
23a3a7f20e | ||
|
|
5ec2d2fe97 | ||
|
|
b2e7813d87 | ||
|
|
0b093415ca | ||
|
|
ff9fadd55a | ||
|
|
77f226e4a2 | ||
|
|
40adba4242 | ||
|
|
71a4d5288d | ||
|
|
d4d118af17 | ||
|
|
72c74101da | ||
|
|
ace657d95a | ||
|
|
1bb12b87ac | ||
|
|
fd3e9e3fcb | ||
|
|
ec40436a65 | ||
|
|
3243f277f2 | ||
|
|
7cd129db52 | ||
|
|
7ac76c2996 | ||
|
|
904c54003d | ||
|
|
84b0590ec5 | ||
|
|
ba63048fc0 | ||
|
|
a46c651dad | ||
|
|
ecb80df10a | ||
|
|
11ba93cc80 | ||
|
|
782919788d | ||
|
|
23760ffa86 | ||
|
|
37dbdf494f | ||
|
|
5ad2bd048c | ||
|
|
9717a686be | ||
|
|
839abe627e | ||
|
|
55167adef6 | ||
|
|
9305b76b85 | ||
|
|
001e2a8887 | ||
|
|
61a196fafc | ||
|
|
a503e20c92 | ||
|
|
0a05d31b17 | ||
|
|
80a5804c9c | ||
|
|
001950a116 | ||
|
|
89cbaf0ac5 | ||
|
|
3670c7c3a7 | ||
|
|
f2f61a1fc9 | ||
|
|
3dc8d9a842 | ||
|
|
b93132e5d9 | ||
|
|
fbb8943eeb | ||
|
|
156bf96788 | ||
|
|
84d07903f6 | ||
|
|
4d1908dceb | ||
|
|
1571676d7a | ||
|
|
870cc142a9 | ||
|
|
8cb0472497 | ||
|
|
eade74ffb0 | ||
|
|
de41dc84af | ||
|
|
880412da94 | ||
|
|
5ae2b88cec | ||
|
|
a9dae21483 | ||
|
|
0a22fb61dc | ||
|
|
0578273f7e | ||
|
|
5d37f1a34b | ||
|
|
cf9fe300fe | ||
|
|
bbe17f3a09 | ||
|
|
1bea6a9627 | ||
|
|
21ad5bbb4a | ||
|
|
5ce4fcb974 | ||
|
|
977193b009 | ||
|
|
028a3e9d62 | ||
|
|
16a83f6134 | ||
|
|
fa2438f40d | ||
|
|
e0af1f20af | ||
|
|
10bccfb4ad | ||
|
|
ca415c50ad | ||
|
|
3c0972b8ac | ||
|
|
c4cbe06c12 | ||
|
|
98ac23a843 | ||
|
|
34fb58d403 | ||
|
|
df458c1052 | ||
|
|
cddf69e422 | ||
|
|
dd86711b32 | ||
|
|
6a1c5aeae7 | ||
|
|
4493d22ec9 | ||
|
|
f3411a46a5 | ||
|
|
5ffd2615e7 | ||
|
|
7616738fc6 | ||
|
|
e996b4fa22 | ||
|
|
5d03ce6b50 | ||
|
|
bcca2aa341 | ||
|
|
3ad8114a28 | ||
|
|
602d935559 | ||
|
|
37d56a2bf6 | ||
|
|
af9318fbd1 | ||
|
|
cff07a3e3d | ||
|
|
2ba4bcd98e | ||
|
|
a1f81705dd | ||
|
|
fac9082a03 | ||
|
|
b8dba48759 | ||
|
|
9ac335116c | ||
|
|
ae8706ab85 | ||
|
|
fbc325bf07 | ||
|
|
af85943c08 | ||
|
|
cad34f63bf | ||
|
|
d7a453e8b1 | ||
|
|
d9964d84b3 | ||
|
|
725a8f2bb5 | ||
|
|
9379f76562 | ||
|
|
5979d91875 | ||
|
|
21e02ee04e | ||
|
|
86b95a404a | ||
|
|
214806d31b | ||
|
|
366cdbf616 | ||
|
|
cea1a73ad6 | ||
|
|
addaf97489 | ||
|
|
e37fa7e5a0 | ||
|
|
6989c7d2fd | ||
|
|
b079b27875 | ||
|
|
166b1a7c6b | ||
|
|
3c895310f4 | ||
|
|
2d3a0cc917 | ||
|
|
ae9e80d6a8 | ||
|
|
f58ffd884b | ||
|
|
9f0abd0bc6 | ||
|
|
afb1b3f16f | ||
|
|
3bedfe75a8 | ||
|
|
93f87f0922 | ||
|
|
76ce8b0876 | ||
|
|
872cee2228 | ||
|
|
fcebbb4856 | ||
|
|
99b7e3126b | ||
|
|
1b02f58247 | ||
|
|
8d8dafade3 | ||
|
|
687b3be784 | ||
|
|
ee3150fc6b | ||
|
|
4922be1422 | ||
|
|
515eb28d4d | ||
|
|
062e65732a | ||
|
|
f186c8cb5f | ||
|
|
c40a73726e | ||
|
|
193587dcfb | ||
|
|
e8d453e2d4 | ||
|
|
3f91ebea5f | ||
|
|
0c4d0cb5c5 | ||
|
|
7f118635b4 | ||
|
|
7efa48b3d7 | ||
|
|
0c199431a9 | ||
|
|
000c482f1b | ||
|
|
4fffcab8aa | ||
|
|
c919648412 | ||
|
|
7eb849affb | ||
|
|
6b57d4a2f7 | ||
|
|
579ca28d6d | ||
|
|
21b52e0b80 | ||
|
|
679921dc8e | ||
|
|
7bd5604607 | ||
|
|
259ea39d55 | ||
|
|
bb83157cbe | ||
|
|
f637ddf8ca | ||
|
|
ca7af014ae | ||
|
|
08b92e1f3d | ||
|
|
a429487894 | ||
|
|
d4e4afb97d | ||
|
|
12f2dc8795 | ||
|
|
dad207912e | ||
|
|
ec76ea307f | ||
|
|
6c9322624d | ||
|
|
499909e09e | ||
|
|
8ae41c0b6a | ||
|
|
baad4742ef | ||
|
|
b6dbc3356d | ||
|
|
a8773a9582 | ||
|
|
2e078294c9 | ||
|
|
efbb78ad7f | ||
|
|
1d001407a0 | ||
|
|
8d41180f4c | ||
|
|
0b994de531 | ||
|
|
5a07ac38da | ||
|
|
caa198964c | ||
|
|
163f483a56 | ||
|
|
c7ded66057 | ||
|
|
e2ce0809da | ||
|
|
8c80e99d3b | ||
|
|
bea85d0f62 | ||
|
|
f87119e31a | ||
|
|
3f2ac6ab76 | ||
|
|
6a5b3a89d9 | ||
|
|
b97c9ee3c9 | ||
|
|
48b0c60cf1 | ||
|
|
f9fbff3fa5 | ||
|
|
9b31e193ee | ||
|
|
cdac5fbf52 | ||
|
|
20d12c0498 | ||
|
|
aa7e010342 | ||
|
|
fec57ecf59 | ||
|
|
74d667b942 | ||
|
|
1c52d533d4 | ||
|
|
9a04535e6b | ||
|
|
c26fdb5dad | ||
|
|
f3d46355af | ||
|
|
db35b6f4e8 | ||
|
|
44621e4f37 | ||
|
|
690d4b8f50 | ||
|
|
a99e290bc5 | ||
|
|
5b0f124307 | ||
|
|
2b5291900d | ||
|
|
cc9d10b12b | ||
|
|
19209d16aa | ||
|
|
5ee924a770 | ||
|
|
a2257999a7 | ||
|
|
d6337ec472 | ||
|
|
d3e87a3d28 | ||
|
|
05f1a6b7ea | ||
|
|
d435d122eb | ||
|
|
dc364981c8 | ||
|
|
362c899632 | ||
|
|
dd8478fe0a | ||
|
|
a80ed6998e | ||
|
|
97d614659a | ||
|
|
c7540ba87b | ||
|
|
3b11f010b5 | ||
|
|
06e282102c | ||
|
|
0b0d2bcdfc | ||
|
|
607123e67a | ||
|
|
3451deee03 | ||
|
|
67e4d24c11 | ||
|
|
2d995d0935 | ||
|
|
0e3c6ac275 | ||
|
|
3b34b1c2d9 | ||
|
|
549f9288a1 | ||
|
|
ae3151d3a7 | ||
|
|
86ba16fbb8 | ||
|
|
f07428a0df | ||
|
|
cb74a2c6ae | ||
|
|
0ab59033b5 | ||
|
|
97b163bc95 | ||
|
|
09f2e89bc4 | ||
|
|
13f3a5e172 | ||
|
|
3066327b0e | ||
|
|
3bcd8bf2d5 | ||
|
|
52d7650d61 | ||
|
|
b0c50bd817 | ||
|
|
aaa38689b3 | ||
|
|
3d95d6b8c9 | ||
|
|
bf62b52183 | ||
|
|
cff710ee52 | ||
|
|
0961d13ac2 | ||
|
|
5003f36337 | ||
|
|
e976f39d2b | ||
|
|
2854a1c8c0 | ||
|
|
c34889ced9 | ||
|
|
5eeaf6853e | ||
|
|
a569a2c2c1 | ||
|
|
08b6141d06 | ||
|
|
356128fbf5 | ||
|
|
6cbc2736d8 | ||
|
|
a1ac2d512b | ||
|
|
2db600b8d7 | ||
|
|
c3fc9879e0 | ||
|
|
5df3dbde1a | ||
|
|
126d6f7f60 | ||
|
|
417571ecd6 | ||
|
|
3d726fe7b0 | ||
|
|
6fa5668cbc | ||
|
|
c6ba21ad4c | ||
|
|
07b8d3d157 | ||
|
|
be3bad7b90 | ||
|
|
d6bd19438c | ||
|
|
2f53c7924d | ||
|
|
0f29506dda | ||
|
|
08d46bbbe3 | ||
|
|
f420c91909 | ||
|
|
db94db2957 | ||
|
|
6c00b8a853 | ||
|
|
c87dcf8aac | ||
|
|
38f20d7eba | ||
|
|
0e1dbc9624 | ||
|
|
19b97f63e5 | ||
|
|
0b90b7ea79 | ||
|
|
fa4258f75c | ||
|
|
2b652cac1f | ||
|
|
583546a7ca | ||
|
|
6c40610d34 | ||
|
|
a6f527bd51 | ||
|
|
f1aec05835 | ||
|
|
56672a862f | ||
|
|
4860ea1b4e | ||
|
|
b5e5fb2bde | ||
|
|
53dcd8b7b2 | ||
|
|
cf82b4899a | ||
|
|
e8e2aab8e3 | ||
|
|
554b21241b | ||
|
|
8d1b523b94 | ||
|
|
d6068395fa | ||
|
|
31c59467db | ||
|
|
4836e5c239 | ||
|
|
54c5a7dcb3 | ||
|
|
0ff4de1e10 | ||
|
|
d4287558f9 | ||
|
|
c91368229a | ||
|
|
da496975bc | ||
|
|
324205ed37 | ||
|
|
aaafb0f465 | ||
|
|
950989b139 | ||
|
|
7618fc97d2 | ||
|
|
498027301e | ||
|
|
f01d224bdf | ||
|
|
35f4be1abc | ||
|
|
08355ff8af | ||
|
|
c9a8f7bd82 | ||
|
|
f2ebfaba3e | ||
|
|
7ad20ccff6 | ||
|
|
67f4c78d61 | ||
|
|
1d4d5b4c1f | ||
|
|
02cf984711 | ||
|
|
2fa37a9732 | ||
|
|
ef86f44215 | ||
|
|
51db8346d3 | ||
|
|
315803dde2 | ||
|
|
e0c330fb29 | ||
|
|
f8280552a0 | ||
|
|
61316c40e5 | ||
|
|
4adfc4353b | ||
|
|
e57fabbd1d | ||
|
|
7d9a7eafc6 | ||
|
|
6ee4e6cd8e | ||
|
|
97b727dcc0 | ||
|
|
2cbee1bf82 | ||
|
|
81525fa61b | ||
|
|
30b4628593 | ||
|
|
87bb092c9d | ||
|
|
5e72cc61b6 | ||
|
|
02f376b6d3 | ||
|
|
db1df07337 | ||
|
|
10f2bc3df5 | ||
|
|
ceb2e0cfb3 | ||
|
|
3e7b1cdc15 | ||
|
|
5e981a579b | ||
|
|
234b15765c | ||
|
|
2b03973cf0 | ||
|
|
53d81aebed | ||
|
|
462aaad9c0 | ||
|
|
afac41d3e6 | ||
|
|
4f72a61ea6 | ||
|
|
f54d08a628 | ||
|
|
bc1ae8b496 | ||
|
|
5b42a4b36e | ||
|
|
98ee88c1bb | ||
|
|
7c89ff5c1b | ||
|
|
bd8abbbdbd | ||
|
|
9249dfee4c | ||
|
|
1ac945ad66 | ||
|
|
e270d2534f | ||
|
|
c2b038c1c0 | ||
|
|
d5862f476b | ||
|
|
02b5179eb3 | ||
|
|
100fd2c9f0 | ||
|
|
a2f55b9838 | ||
|
|
4fef4a8d33 | ||
|
|
933f799952 | ||
|
|
3018f8c521 | ||
|
|
826fccbc94 | ||
|
|
54453ee9e5 | ||
|
|
be0b5bb0d1 | ||
|
|
cc957cc3e8 | ||
|
|
2b274b706e | ||
|
|
dd5fd72bd2 | ||
|
|
3ab1f9b5a3 | ||
|
|
9d4655cc00 | ||
|
|
e512fef78c | ||
|
|
3320de07f2 | ||
|
|
448e82108d | ||
|
|
68482afa5c | ||
|
|
be93e77b2f | ||
|
|
bfeded207a | ||
|
|
5aed2b6baf | ||
|
|
a5971b0c69 | ||
|
|
00b5aba88a | ||
|
|
6d93ccc0d0 | ||
|
|
9c0edfdb9d | ||
|
|
69f5de6478 | ||
|
|
b40e2e0a6f | ||
|
|
4630a8d18d | ||
|
|
d73130ebac | ||
|
|
79e40a667b | ||
|
|
13016c7476 | ||
|
|
2c904641a5 | ||
|
|
667eb3035b | ||
|
|
1303163aee | ||
|
|
13f2ee2ae8 | ||
|
|
14ecfd2cc8 | ||
|
|
1b46c39a27 | ||
|
|
a91eb9435b | ||
|
|
5d19096e0c | ||
|
|
b5d2285b99 | ||
|
|
3f79189410 | ||
|
|
fece24a50a | ||
|
|
1940099d3c | ||
|
|
7d77da8339 | ||
|
|
240e0780a0 | ||
|
|
e43366f422 | ||
|
|
3e38ef959b | ||
|
|
c7af5bdce9 | ||
|
|
9e2af21d5e | ||
|
|
3f302ee4a3 | ||
|
|
3aa4d4c36c | ||
|
|
53ef0f3f2d | ||
|
|
81866cb6d3 | ||
|
|
c6dbb8d7c8 | ||
|
|
bee20a5478 | ||
|
|
f4088e0b38 | ||
|
|
b43e32169b | ||
|
|
d3d46aa023 | ||
|
|
4d99541f7c | ||
|
|
55cc3dd90e | ||
|
|
089b67c40e | ||
|
|
0822789002 | ||
|
|
9ca0f4a4fa | ||
|
|
ffb2ec9bd1 | ||
|
|
0e1a0b4798 | ||
|
|
2bcb56d249 | ||
|
|
467ae5c8fa | ||
|
|
8ccbe9c069 | ||
|
|
a3bf50e15e | ||
|
|
85f98f7405 | ||
|
|
9d44a73d02 | ||
|
|
ac946fd014 | ||
|
|
8e9d537882 | ||
|
|
3680d0c65d | ||
|
|
774b9cc368 | ||
|
|
8130d28442 | ||
|
|
00e3b06004 | ||
|
|
9cac38d5da | ||
|
|
3014ba8eec | ||
|
|
81122c9865 | ||
|
|
823f0b8db5 | ||
|
|
b8a0077b1d | ||
|
|
af1b1c0edb | ||
|
|
62108e3dac | ||
|
|
dd4c3f152a | ||
|
|
0c9fa8b51b | ||
|
|
0a511e4f8a | ||
|
|
aa2bf4fe22 | ||
|
|
524c2b8203 | ||
|
|
e82f561c03 | ||
|
|
578d9c6785 | ||
|
|
d348fe9012 | ||
|
|
c7efad2197 | ||
|
|
7271b7a5f0 | ||
|
|
adda8707ba | ||
|
|
522536cfe0 | ||
|
|
640d0082da | ||
|
|
f557b53ce2 | ||
|
|
f5bd7f113f | ||
|
|
1277da7e92 | ||
|
|
8b1978fb26 | ||
|
|
f334122e41 | ||
|
|
812e8cca9a | ||
|
|
269cbe839b | ||
|
|
63bc04e800 | ||
|
|
2a3bcc2ecb | ||
|
|
7eb776bc3f | ||
|
|
5babab85c8 | ||
|
|
56981a5333 | ||
|
|
8debd01a36 | ||
|
|
54cd4723ba | ||
|
|
51a37cae3d | ||
|
|
c9f8b04a12 | ||
|
|
3c24e76eb4 | ||
|
|
11e970ee8a | ||
|
|
6dc9f8ea2b | ||
|
|
3d7367aa04 | ||
|
|
157b1ca0b4 | ||
|
|
2bcf5b2fc5 | ||
|
|
8be95262d4 | ||
|
|
39bc4d7151 | ||
|
|
3cbdae6e5c | ||
|
|
f08e58a301 | ||
|
|
edcf0661a6 | ||
|
|
a49270630c | ||
|
|
6d14c5c555 | ||
|
|
f703517f70 | ||
|
|
a0662d5323 | ||
|
|
6c1ca3036b | ||
|
|
cbd1cf7c25 | ||
|
|
6ed80a9b92 | ||
|
|
a55225b5e8 | ||
|
|
42fa5c2ee7 | ||
|
|
b81c3ba614 | ||
|
|
8f34b241d4 | ||
|
|
2d0998a995 | ||
|
|
b0d6ce61b0 | ||
|
|
766840de68 | ||
|
|
9defa45428 | ||
|
|
791f2e0b34 | ||
|
|
52bcb8dfb6 | ||
|
|
9241a7c35d | ||
|
|
1f90f13b81 | ||
|
|
dda92477cf | ||
|
|
0a522863dc | ||
|
|
07455e5821 | ||
|
|
e8a974813d | ||
|
|
1b9aae0137 | ||
|
|
50da387936 | ||
|
|
30b86ea781 | ||
|
|
489869ee42 | ||
|
|
a186d44d8f | ||
|
|
316b2a1b1c | ||
|
|
574f75b293 | ||
|
|
a1625f7125 | ||
|
|
252ae7455a | ||
|
|
63379d9b24 | ||
|
|
d73dbdeee0 | ||
|
|
d812f26e81 | ||
|
|
72065c0ee2 | ||
|
|
4ba3152a99 | ||
|
|
07caee1d12 | ||
|
|
d4f48cdc21 | ||
|
|
4c3904760c | ||
|
|
dc0cc3af65 | ||
|
|
8ad2c2a59e | ||
|
|
27031c96b5 | ||
|
|
e5a365ba41 | ||
|
|
b1ca28fbb5 | ||
|
|
fc0b118188 | ||
|
|
1b7bfb42fc | ||
|
|
a207b93d0d | ||
|
|
ea65204eaa | ||
|
|
b1d588b1f2 | ||
|
|
4351e5a642 | ||
|
|
95a6bab8b5 | ||
|
|
f35289624c | ||
|
|
c82433e6b4 | ||
|
|
47c322cb31 | ||
|
|
2e84852519 | ||
|
|
88f1237990 | ||
|
|
da746d15a0 | ||
|
|
4740a8b520 | ||
|
|
1b519269d8 | ||
|
|
521b6ab851 | ||
|
|
5ffaf1aead | ||
|
|
9e328551e4 | ||
|
|
b704406164 | ||
|
|
44eaffd110 | ||
|
|
5c9fe40702 | ||
|
|
cb964b5888 | ||
|
|
fe7c4f96aa | ||
|
|
81cbc7b87c | ||
|
|
83909a07fa | ||
|
|
8fa45749a9 | ||
|
|
cd99927881 | ||
|
|
910ea4caec | ||
|
|
8bbb485d5b | ||
|
|
0bff263c4b | ||
|
|
b2ec77668e | ||
|
|
38f85d3cc8 | ||
|
|
f8302ab65a | ||
|
|
83002d09a4 | ||
|
|
e632cf1c42 | ||
|
|
a567178987 | ||
|
|
640557df4f | ||
|
|
13c47639da | ||
|
|
9b7227a88b | ||
|
|
74b0535b31 | ||
|
|
aabc912862 | ||
|
|
cbd7799b44 | ||
|
|
02d6567347 | ||
|
|
98a8c4752b | ||
|
|
6f8767d1fc | ||
|
|
b2debb32d1 | ||
|
|
bc787efc86 | ||
|
|
098f3f6e4c | ||
|
|
e0d46c3942 | ||
|
|
e8c7f728a2 | ||
|
|
5a2bed29aa | ||
|
|
387ffbb0fc | ||
|
|
8fbcd0c34d | ||
|
|
d2d4f6186f | ||
|
|
97df37536c | ||
|
|
d5cd02cab3 | ||
|
|
373b9c6a97 | ||
|
|
d831710b0a | ||
|
|
009726c62d | ||
|
|
d5316b2c4d | ||
|
|
69c07a41e9 | ||
|
|
7c4bedf371 | ||
|
|
64afd62a1f | ||
|
|
7018ed28fb | ||
|
|
4318468957 | ||
|
|
7213e62937 | ||
|
|
1af3fc1c96 | ||
|
|
219ea98f33 | ||
|
|
1e11f8032a | ||
|
|
f6cbc36112 | ||
|
|
a1a7ca8a0a | ||
|
|
93bc54e275 | ||
|
|
52758fa66e | ||
|
|
44cd109ba3 | ||
|
|
fa43ff1365 | ||
|
|
482168f98e | ||
|
|
bd15162fb7 | ||
|
|
f9b9c7136e | ||
|
|
cc980edc66 | ||
|
|
84ec26f648 | ||
|
|
7fd094ba39 | ||
|
|
fcfe5da506 | ||
|
|
68ef489d8c | ||
|
|
1e4bdb367e | ||
|
|
21ff1d7482 | ||
|
|
d3ee55a971 | ||
|
|
669b1d9a63 | ||
|
|
3a967c5985 | ||
|
|
29531a361c | ||
|
|
92f5df4704 | ||
|
|
c40cf2e7e8 | ||
|
|
2e8789de3b | ||
|
|
7bf391e772 | ||
|
|
b7827f3eea | ||
|
|
fbb84af955 | ||
|
|
8c101a1bbf | ||
|
|
d954eb63ef | ||
|
|
ee216dbf64 | ||
|
|
1b14161535 | ||
|
|
54675117de | ||
|
|
bcf854604b | ||
|
|
30d5b46daf | ||
|
|
f265d9174a | ||
|
|
45ec489080 | ||
|
|
970e0e2d04 | ||
|
|
93fe613a9a | ||
|
|
9055865e1c | ||
|
|
704f2c176d | ||
|
|
f9b6aca8e8 | ||
|
|
d538134bb9 | ||
|
|
d084b5a03c | ||
|
|
6e38050ac4 | ||
|
|
a6ab0ff2aa | ||
|
|
f3c87ef313 | ||
|
|
1bce924d83 | ||
|
|
09a2136f02 | ||
|
|
f571e8ac6e | ||
|
|
5c7331d0a4 | ||
|
|
27bf2e642b | ||
|
|
187ea86c24 | ||
|
|
d4ca903a07 | ||
|
|
48639adc42 | ||
|
|
0f8bbdc9f2 | ||
|
|
509412dee6 | ||
|
|
fb9f814b00 | ||
|
|
44a95c4888 | ||
|
|
b4f88196dc | ||
|
|
0f3400a6b7 | ||
|
|
78e748548c | ||
|
|
a55bbc5e8c | ||
|
|
199cddfbdb | ||
|
|
8dad478a19 | ||
|
|
c0ca602017 | ||
|
|
31208c2af1 | ||
|
|
3471b9853e | ||
|
|
11f57b02e6 | ||
|
|
6765d7d89f | ||
|
|
86a99e2337 | ||
|
|
250aa17e63 | ||
|
|
3470d38d7c | ||
|
|
2942d20ac3 | ||
|
|
e6959e75f9 | ||
|
|
d32799b2dc | ||
|
|
1e4f70747b | ||
|
|
ff62fbbcf4 | ||
|
|
6ee3bc099d | ||
|
|
69ec362a8d | ||
|
|
13d44ee3e8 | ||
|
|
41d00543d0 | ||
|
|
fc9bffddbd | ||
|
|
f890e590e1 | ||
|
|
64d573e28e | ||
|
|
2aec982577 | ||
|
|
b2781a1ea6 | ||
|
|
b66d5daad0 | ||
|
|
04cdc75841 | ||
|
|
6ff07b99dc | ||
|
|
bb7bb40e76 | ||
|
|
1586860e15 | ||
|
|
a4055364e4 | ||
|
|
99dac8f6fd | ||
|
|
71da6e4528 | ||
|
|
5fb910b886 | ||
|
|
5c113284e2 | ||
|
|
fb826ec838 | ||
|
|
b2cb3bcf1d | ||
|
|
5198cc17d3 | ||
|
|
1821f90664 | ||
|
|
261ecfcb11 | ||
|
|
a66a8982ee | ||
|
|
6582bce0d3 | ||
|
|
0a83a1f168 | ||
|
|
db91ca82c1 | ||
|
|
e97d3172eb | ||
|
|
cb7fbc16b0 | ||
|
|
7c838bf54e | ||
|
|
c6c639f16f | ||
|
|
4a5c5143b3 | ||
|
|
cb5968bc5f | ||
|
|
c02afbb4f9 | ||
|
|
693b25efc5 | ||
|
|
b647bc9b41 | ||
|
|
6eddaeda61 | ||
|
|
c36b90db0f | ||
|
|
349bfa9139 | ||
|
|
ddf3959d4d | ||
|
|
b0f75678ee | ||
|
|
b5f88c199c | ||
|
|
0a0f7d7365 | ||
|
|
a0586457da | ||
|
|
6d1ff98bda | ||
|
|
288d1f7e5a | ||
|
|
065324d256 | ||
|
|
38c28bccdb | ||
|
|
69f7c3417e | ||
|
|
e8b0178ae4 | ||
|
|
806f57c627 | ||
|
|
9eeebf93fa | ||
|
|
e4c7844cae | ||
|
|
c1ccf02ff9 | ||
|
|
6ebfa16740 | ||
|
|
6533aa2826 | ||
|
|
43cfdb8727 | ||
|
|
ece1a51530 | ||
|
|
b31c5be70e | ||
|
|
1d4a407161 | ||
|
|
d84d047470 | ||
|
|
9f5678c711 | ||
|
|
42a0659012 | ||
|
|
819ac84c2a | ||
|
|
6386f0f9aa | ||
|
|
fe90f3703e | ||
|
|
9aaf17d478 | ||
|
|
0e956a605f | ||
|
|
1f908f9040 | ||
|
|
32210d89f8 | ||
|
|
b51ef059f5 | ||
|
|
18a77c995f | ||
|
|
a9e135c94f | ||
|
|
9f36234c52 | ||
|
|
212ade2da7 | ||
|
|
0b74d9e998 | ||
|
|
f939bf6108 | ||
|
|
54d545094f | ||
|
|
3360cd934b | ||
|
|
c239c476af | ||
|
|
f35a0970ac | ||
|
|
a382a0cd44 | ||
|
|
97ca722a11 | ||
|
|
0fee59a6ed | ||
|
|
e554c9bdd7 | ||
|
|
e18226d108 | ||
|
|
567a732e1e | ||
|
|
b079952491 | ||
|
|
5b532d03a0 | ||
|
|
d2da71c22a | ||
|
|
cd838e5a7e | ||
|
|
9eb2a6a535 | ||
|
|
bb096be00c | ||
|
|
dd5ef7ec72 | ||
|
|
7b78bfe191 | ||
|
|
c2cbcd3727 | ||
|
|
a45ba0bf30 | ||
|
|
5c7baf9e05 | ||
|
|
5ce3699a58 | ||
|
|
e5f5e18ecc | ||
|
|
1cd836ac8d | ||
|
|
dae30037b6 | ||
|
|
c83705119d | ||
|
|
30eba3bfae | ||
|
|
fdd2cfe1d1 | ||
|
|
77c0486f8c | ||
|
|
8327baa2f6 | ||
|
|
e00475520a | ||
|
|
84df2fb85c | ||
|
|
bf90a6247e | ||
|
|
cab6f9e58d | ||
|
|
3185cc041a | ||
|
|
d2ace5c1cf | ||
|
|
f64b9084f5 | ||
|
|
5eddddb7b5 | ||
|
|
dc09561f30 | ||
|
|
6408b9e5e1 | ||
|
|
e154cbe1ba | ||
|
|
b0159c8246 | ||
|
|
1f9ac49e27 | ||
|
|
b056e49ec5 | ||
|
|
a7de923cea | ||
|
|
fef5c287d7 | ||
|
|
a75430106e | ||
|
|
09c65ee9dc | ||
|
|
bc816100a0 | ||
|
|
a2385a1779 | ||
|
|
33de209497 | ||
|
|
95529ce8f0 | ||
|
|
8401e25504 | ||
|
|
1a6e5b425a | ||
|
|
db14c695e6 | ||
|
|
87e0962c5a | ||
|
|
7a61b2ec80 | ||
|
|
1625149221 | ||
|
|
1e16e58f37 | ||
|
|
4d60c735ed | ||
|
|
e84ca44178 | ||
|
|
1d28b7901c | ||
|
|
644c03503b | ||
|
|
2d55f59120 | ||
|
|
d88288302a | ||
|
|
16534e2932 | ||
|
|
42e0797b5b | ||
|
|
ed361838ab | ||
|
|
8826d41922 | ||
|
|
e7145d9d48 | ||
|
|
26d2d6f403 | ||
|
|
2a9374e10f | ||
|
|
438386de5d | ||
|
|
95759addda | ||
|
|
99197396f1 | ||
|
|
184cc443eb | ||
|
|
3770463499 | ||
|
|
08d02838d3 | ||
|
|
d3979a5a5a | ||
|
|
2b13ef1063 | ||
|
|
e5bba73ea8 | ||
|
|
964d22e540 | ||
|
|
cd925d1896 | ||
|
|
248ba615ed | ||
|
|
82fe6f6fa7 | ||
|
|
441f9c677a | ||
|
|
c05cf29a37 | ||
|
|
d4e484fd82 | ||
|
|
160f491cc5 | ||
|
|
0f5b4887ee | ||
|
|
d652013572 | ||
|
|
9ce6b81ae0 | ||
|
|
c970503f61 | ||
|
|
1865ca945b | ||
|
|
5218f4f182 | ||
|
|
01e393cf8c | ||
|
|
9230a77f96 | ||
|
|
9b6b6a6cd7 | ||
|
|
f8cc78eca5 | ||
|
|
f7d4c285f5 | ||
|
|
a9f9af3cb8 | ||
|
|
cfccce3c64 | ||
|
|
ec71621d93 | ||
|
|
9b557657b1 | ||
|
|
52376993df | ||
|
|
faad6766a8 | ||
|
|
74a5253c69 | ||
|
|
b8fb6200f0 | ||
|
|
2aebc023d1 | ||
|
|
e028dfe2f9 | ||
|
|
8dfd453381 | ||
|
|
ad97b74026 | ||
|
|
899cb9d4cf | ||
|
|
1c1efbbc61 | ||
|
|
e34021c0be | ||
|
|
aa7d066f6c | ||
|
|
041d5da13b | ||
|
|
4a1563365d | ||
|
|
d421848795 | ||
|
|
7e197d2a57 | ||
|
|
96185e9c60 | ||
|
|
c16c0dd454 | ||
|
|
5bd8ef2e5d | ||
|
|
a35df2aed8 | ||
|
|
3dae7e9523 | ||
|
|
4102cab43b | ||
|
|
7d4660173e | ||
|
|
e928a29b98 | ||
|
|
612c6a331b | ||
|
|
33fec327a9 | ||
|
|
0c852a145e | ||
|
|
f0169978d4 | ||
|
|
ed2d3a27e7 | ||
|
|
deeb113cdd | ||
|
|
de162817af | ||
|
|
447f1fab1f | ||
|
|
fd1acd6533 | ||
|
|
5ba32cfdc1 | ||
|
|
7282f61133 | ||
|
|
ba838863bf | ||
|
|
0687d9ed98 | ||
|
|
b9f3d0ca56 | ||
|
|
e45a3ebdb4 | ||
|
|
36e5df00ab | ||
|
|
b72f9f054d | ||
|
|
b1f9995ce2 | ||
|
|
92b9fb60e9 | ||
|
|
52888c4724 | ||
|
|
08951ab515 | ||
|
|
efe75c4134 | ||
|
|
c2d2bd0ea1 | ||
|
|
b4b61f9eb6 | ||
|
|
ff6204c98e | ||
|
|
5db8e66ca6 | ||
|
|
c08831ca13 | ||
|
|
30c3df0829 | ||
|
|
c8ef72e4d2 | ||
|
|
a6ab94113f | ||
|
|
b1bd52423a | ||
|
|
7315d9671d | ||
|
|
4b980b8076 | ||
|
|
75f1ab6183 | ||
|
|
63baa20403 | ||
|
|
c6bf7511e1 | ||
|
|
612aca217c | ||
|
|
5b286b74b0 | ||
|
|
92b56c99a3 | ||
|
|
a3cf6711a5 | ||
|
|
349b18d63a | ||
|
|
74142187c1 | ||
|
|
11d331c051 | ||
|
|
82fc2eb812 | ||
|
|
63851b16af | ||
|
|
5d68f46a72 | ||
|
|
4384eed09f | ||
|
|
0a185cf051 | ||
|
|
e746805eaa | ||
|
|
9113233c9e | ||
|
|
6c480178fe | ||
|
|
304a14e5a1 | ||
|
|
7e94cc7ff8 | ||
|
|
6d8b256d10 | ||
|
|
db20eeb555 | ||
|
|
42498391ce | ||
|
|
9794f12a9b | ||
|
|
d4fa34fe4b | ||
|
|
9af88076e6 | ||
|
|
27294fd81c | ||
|
|
290ae85128 | ||
|
|
ba0291dd3e | ||
|
|
5c78760649 | ||
|
|
d46c2b086c | ||
|
|
3cb8365ef3 | ||
|
|
f5f5258422 | ||
|
|
38e95a7f07 | ||
|
|
3bd8b177be | ||
|
|
6d392b1c91 | ||
|
|
42c3fcf248 | ||
|
|
a8f7028c22 | ||
|
|
337acd0b7f | ||
|
|
35c7366b96 | ||
|
|
442da290a4 | ||
|
|
137bd43821 | ||
|
|
02f5979e9a | ||
|
|
08c9a0630d | ||
|
|
93162fed85 | ||
|
|
abdc9f75cc | ||
|
|
eea0c2e060 | ||
|
|
ecaae1b934 | ||
|
|
f08ebf2256 | ||
|
|
fc06f8c88e | ||
|
|
6e0d1c613c | ||
|
|
0fc62f07cc | ||
|
|
0f09bbc253 | ||
|
|
4afb12669a | ||
|
|
c50639d137 | ||
|
|
030864b72b | ||
|
|
5075795704 | ||
|
|
0bf6e39c66 | ||
|
|
668c60e1e8 | ||
|
|
0d6613b998 | ||
|
|
1e92240cbd | ||
|
|
99875ff746 | ||
|
|
7e73b307f0 | ||
|
|
05bb0fcf43 | ||
|
|
cc86cb5ffa | ||
|
|
bce60758e9 | ||
|
|
7068e68b8f | ||
|
|
7b85e78636 | ||
|
|
fc7412adae | ||
|
|
4fa6ef828c | ||
|
|
fef4dadd58 | ||
|
|
08ca3431ac | ||
|
|
c10c7a959d | ||
|
|
cfcc21b1cb | ||
|
|
25c6bc2252 | ||
|
|
4ea54ef5ce | ||
|
|
cd03948164 | ||
|
|
fc65920462 | ||
|
|
206709b703 | ||
|
|
88a7ff62af | ||
|
|
e47f78f657 | ||
|
|
1c75ae08bc | ||
|
|
1e3be5b4b8 | ||
|
|
5ea63534f7 | ||
|
|
e8aa8e094f | ||
|
|
95805169dc | ||
|
|
a604fcf8a4 | ||
|
|
bcd018d8de | ||
|
|
8381bd14c5 | ||
|
|
34627f5e60 | ||
|
|
7ef1ed400b | ||
|
|
0ae1692f99 | ||
|
|
a88582a579 | ||
|
|
6becbee27a | ||
|
|
1131df9c78 | ||
|
|
78633b06de | ||
|
|
24492bbe2d | ||
|
|
78bf265d7a | ||
|
|
0f89a12a3d | ||
|
|
1690a25262 | ||
|
|
0a7949540c | ||
|
|
f76f284ce2 | ||
|
|
b88dbb1a6f | ||
|
|
5080b754d4 | ||
|
|
8177cfed55 | ||
|
|
bdb97182e4 | ||
|
|
340284e09b | ||
|
|
c668ed8a2b | ||
|
|
5d3e401302 | ||
|
|
10a1350bb3 | ||
|
|
584c0d8509 | ||
|
|
c10fb2916f | ||
|
|
770a8a8d57 | ||
|
|
91185abb4c | ||
|
|
2b8a3f66e9 | ||
|
|
e402b06c6c | ||
|
|
7ca1b5e761 | ||
|
|
6a09adf11c | ||
|
|
d6d3dfcb35 | ||
|
|
ba7ba751fd | ||
|
|
e2e61b7c59 | ||
|
|
ba3c02c912 | ||
|
|
6f6bd256b5 | ||
|
|
c8d1780ee8 | ||
|
|
31e904c21a | ||
|
|
6773488644 | ||
|
|
84b0d52510 | ||
|
|
db9aa4bc38 | ||
|
|
04e1534001 | ||
|
|
74d4928fb0 | ||
|
|
d31d422eb0 | ||
|
|
eb5b62b670 | ||
|
|
53ef4fee1e | ||
|
|
b3cdc4f5fc | ||
|
|
63abb61248 | ||
|
|
59e16866fb | ||
|
|
9fc36bd6fa | ||
|
|
4051fae33b | ||
|
|
b014dadfe3 | ||
|
|
900b084156 | ||
|
|
fa96c94085 | ||
|
|
bd1d287c87 | ||
|
|
b74f7e4eac | ||
|
|
7a57132c1c | ||
|
|
46c2367e50 | ||
|
|
7378f85297 |
5
.coveragerc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[report]
|
||||||
|
omit =
|
||||||
|
*/settings.py
|
||||||
|
*/python?.?/*
|
||||||
|
*/site-packages/nose/*
|
||||||
9
.env.example
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
REDASH_CONNECTION_ADAPTER=pg
|
||||||
|
REDASH_CONNECTION_STRING="dbname=data"
|
||||||
|
REDASH_STATIC_ASSETS_PATH=../rd_ui/app/
|
||||||
|
REDASH_GOOGLE_APPS_DOMAIN=
|
||||||
|
REDASH_ADMINS=
|
||||||
|
REDASH_WORKERS_COUNT=2
|
||||||
|
REDASH_COOKIE_SECRET=
|
||||||
|
REDASH_DATABASE_URL='postgresql://rd'
|
||||||
|
REDASH_LOG_LEVEL = "INFO"
|
||||||
18
.gitignore
vendored
@@ -1,4 +1,20 @@
|
|||||||
|
.coveralls.yml
|
||||||
.idea
|
.idea
|
||||||
*.pyc
|
*.pyc
|
||||||
rd_service/settings.py
|
.coverage
|
||||||
rd_ui/dist
|
rd_ui/dist
|
||||||
|
.DS_Store
|
||||||
|
celerybeat-schedule*
|
||||||
|
.#*
|
||||||
|
\#*#
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Vagrant related
|
||||||
|
.vagrant
|
||||||
|
Berksfile.lock
|
||||||
|
redash/dump.rdb
|
||||||
|
.env
|
||||||
|
.ruby-version
|
||||||
|
venv
|
||||||
|
|
||||||
|
dump.rdb
|
||||||
|
|||||||
2
.landscape.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ignore-paths:
|
||||||
|
- migrations
|
||||||
23
Makefile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
NAME=redash
|
||||||
|
VERSION=`python ./manage.py version`
|
||||||
|
FULL_VERSION=$(VERSION)+b$(CIRCLE_BUILD_NUM)
|
||||||
|
BASE_VERSION=$(shell python ./manage.py version | cut -d + -f 1)
|
||||||
|
# VERSION gets evaluated every time it's referenced, therefore we need to use VERSION here instead of FULL_VERSION.
|
||||||
|
FILENAME=$(CIRCLE_ARTIFACTS)/$(NAME).$(VERSION).tar.gz
|
||||||
|
|
||||||
|
deps:
|
||||||
|
cd rd_ui && npm install
|
||||||
|
cd rd_ui && npm install -g bower grunt-cli
|
||||||
|
cd rd_ui && bower install
|
||||||
|
cd rd_ui && grunt build
|
||||||
|
|
||||||
|
pack:
|
||||||
|
sed -ri "s/^__version__ = '([0-9.]*)'/__version__ = '$(FULL_VERSION)'/" redash/__init__.py
|
||||||
|
tar -zcv -f $(FILENAME) --exclude=".git*" --exclude="*.pyc" --exclude="*.pyo" --exclude="venv" --exclude="rd_ui/node_modules" --exclude="rd_ui/dist/bower_components" --exclude="rd_ui/app" *
|
||||||
|
|
||||||
|
upload:
|
||||||
|
python bin/release_manager.py $(CIRCLE_SHA1) $(BASE_VERSION) $(FILENAME)
|
||||||
|
|
||||||
|
test:
|
||||||
|
nosetests --with-coverage --cover-package=redash tests/*.py
|
||||||
|
#cd rd_ui && grunt test
|
||||||
2
Procfile.dev
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
web: ./manage.py runserver -p $PORT --host 0.0.0.0
|
||||||
|
worker: ./bin/run celery worker --app=redash.worker --beat -Qqueries,celery,scheduled_queries
|
||||||
2
Procfile.heroku
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
web: ./manage.py runserver -p $PORT --host 0.0.0.0 -d -r
|
||||||
|
worker: ./bin/run celery worker --app=redash.worker --beat -Qqueries,celery,scheduled_queries
|
||||||
104
README.md
@@ -1,107 +1,45 @@
|
|||||||
# [_re:dash_](https://github.com/everythingme/redash)
|
<p align="center">
|
||||||
|
<img title="re:dash" src='http://redash.io/static/img/redash_logo.png' width="200px"/>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img title="Build Status" src='https://circleci.com/gh/EverythingMe/redash.png?circle-token=8a695aa5ec2cbfa89b48c275aea298318016f040'/>
|
||||||
|
</p>
|
||||||
|
|
||||||
**_re:dash_** is our take on freeing the data within our company in a way that will better fit our culture and usage patterns.
|
**_re:dash_** 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 tranditional 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 **_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.
|
||||||
|
|
||||||
**_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).
|
**_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).
|
||||||
|
Today **_re:dash_** has support for querying multiple databases, including: Redshift, Google BigQuery, PostgreSQL, MySQL, Graphite and custom scripts.
|
||||||
|
|
||||||
**_re:dash_** consists of two parts:
|
**_re:dash_** 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 bar 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.
|
||||||
|
|
||||||
This is the first release, which is more than usable but still 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.
|
**_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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
You can try out the demo instance: http://rd-demo.herokuapp.com/ (login with any Google account).
|
You can try out the demo instance: http://demo.redash.io/ (login with any Google account).
|
||||||
|
|
||||||
Due to Heroku dev plan limits, it has a small database of flights (see schema [here](http://rd-demo.herokuapp.com/dashboard/schema)). Also due to another Heroku limitation, it is running with the regular user, hence you can DELETE or INSERT data/tables. Please be nice and don't do this.
|
|
||||||
|
|
||||||
## Technology
|
|
||||||
|
|
||||||
* [AngularJS](http://angularjs.org/)
|
|
||||||
* [Tornado](http://tornadoweb.org)
|
|
||||||
* [PostgreSQL](http://www.postgresql.org/) / [AWS Redshift](http://aws.amazon.com/redshift/)
|
|
||||||
* [Redis](http://redis.io)
|
|
||||||
|
|
||||||
PostgreSQL is used both as the operatinal database for the system, but also as the data store that is being queried. To be exact, we built this system to use on top of Amazon's Redshift, which supports the PG driver. But it's quite simple to add support for other datastores, and we do plan to do so.
|
|
||||||
|
|
||||||
This is our first large scale AngularJS project, and we learned a lot during the development of it. There are still things we need to iron out, and comments on the way we use AngularJS are more than welcome (and pull requests just as well).
|
|
||||||
|
|
||||||
### HighCharts
|
|
||||||
|
|
||||||
HighCharts is really great, but it's not free for commercial use. Please refer to their [licensing options](http://shop.highsoft.com/highcharts.html), to see what applies for your use.
|
|
||||||
|
|
||||||
It's very likely that in the future we will switch to [D3.js](http://d3js.org/) instead.
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
1. Clone the repo:
|
* [Setting up re:dash instance](http://redash.io/deployment/setup.html) (includes links to ready made AWS/GCE images).
|
||||||
```bash
|
* Additional documentation in the [Wiki](https://github.com/everythingme/redash/wiki).
|
||||||
git clone git@github.com:EverythingMe/redash.git
|
|
||||||
```
|
|
||||||
2. Create settings file from the example one (& update relevant settings):
|
|
||||||
```bash
|
|
||||||
cp rd_service/settings_example.py rd_service/settings.py
|
|
||||||
```
|
|
||||||
> It's highly recommended that the user you use to connect to the data database (the one you query) is read-only.
|
|
||||||
|
|
||||||
3. Install `npm` packages (mainly: Bower & Grunt):
|
|
||||||
```bash
|
## Getting help
|
||||||
cd rd_ui
|
|
||||||
npm install
|
* [Google Group (mailing list)](https://groups.google.com/forum/#!forum/redash-users): the best place to get updates about new releases or ask general questions.
|
||||||
```
|
* Find us [on gitter](https://gitter.im/EverythingMe/redash#) (chat).
|
||||||
4. Install `bower` packages:
|
* Contact Arik, the maintainer directly: arik@everything.me.
|
||||||
```bash
|
|
||||||
bower install
|
|
||||||
```
|
|
||||||
5. Build the UI:
|
|
||||||
```bash
|
|
||||||
grunt build
|
|
||||||
```
|
|
||||||
6. Install PIP packages:
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
6. Start the API server:
|
|
||||||
```bash
|
|
||||||
cd ../rd_service
|
|
||||||
python server.py
|
|
||||||
```
|
|
||||||
7. Start the workers:
|
|
||||||
```bash
|
|
||||||
python cli.py worker
|
|
||||||
```
|
|
||||||
8. Open `http://localhost:8888/` and query away.
|
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
We plan to release new minor version every 2-3 weeks. Of course, if we get additional help from contributors it will help speed things up.
|
TBD.
|
||||||
|
|
||||||
Below you can see the "big" features of the next 3 releases (for full list, click on the link):
|
|
||||||
|
|
||||||
### [v0.2](https://github.com/EverythingMe/redash/issues?milestone=1&state=open)
|
|
||||||
|
|
||||||
- Ability to generate multiple visualizations for a single query (dataset) in a more flexible way than today. Also easier extensbility points to add additional visualizations.
|
|
||||||
- Dashboard filters: ability to filter/slice the data you see in a single dashboard using filters (date or selectors).
|
|
||||||
- UI Improvements (better notifications & flows, improved queries page)
|
|
||||||
- Comments on queries.
|
|
||||||
|
|
||||||
### [v0.3](https://github.com/EverythingMe/redash/issues?milestone=2&state=open)
|
|
||||||
|
|
||||||
- Support for API access using API keys, instead of Google Login.
|
|
||||||
- Multiple databases support (including other database type than PostgreSQL).
|
|
||||||
- Scheduled reports by email.
|
|
||||||
|
|
||||||
### [v0.4](https://github.com/EverythingMe/redash/issues?milestone=3&state=open)
|
|
||||||
|
|
||||||
- Query versioning.
|
|
||||||
- More "realtime" UI (using websockets).
|
|
||||||
- More visualizations.
|
|
||||||
|
|
||||||
## Reporting Bugs and Contributing Code
|
## Reporting Bugs and Contributing Code
|
||||||
|
|
||||||
|
|||||||
11
Vagrantfile
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# -*- 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
|
||||||
|
end
|
||||||
147
bin/release_manager.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import requests
|
||||||
|
|
||||||
|
github_token = os.environ['GITHUB_TOKEN']
|
||||||
|
auth = (github_token, 'x-oauth-basic')
|
||||||
|
repo = 'EverythingMe/redash'
|
||||||
|
|
||||||
|
def _github_request(method, path, params=None, headers={}):
|
||||||
|
if not path.startswith('https://api.github.com'):
|
||||||
|
url = "https://api.github.com/{}".format(path)
|
||||||
|
else:
|
||||||
|
url = path
|
||||||
|
|
||||||
|
if params is not None:
|
||||||
|
params = json.dumps(params)
|
||||||
|
|
||||||
|
response = requests.request(method, url, data=params, auth=auth)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def exception_from_error(message, response):
|
||||||
|
return Exception("({}) {}: {}".format(response.status_code, message, response.json().get('message', '?')))
|
||||||
|
|
||||||
|
def rc_tag_name(version):
|
||||||
|
return "v{}-rc".format(version)
|
||||||
|
|
||||||
|
def get_rc_release(version):
|
||||||
|
tag = rc_tag_name(version)
|
||||||
|
response = _github_request('get', 'repos/{}/releases/tags/{}'.format(repo, tag))
|
||||||
|
|
||||||
|
if response.status_code == 404:
|
||||||
|
return None
|
||||||
|
elif response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
raise exception_from_error("Unknown error while looking RC release: ", response)
|
||||||
|
|
||||||
|
def create_release(version, commit_sha):
|
||||||
|
tag = rc_tag_name(version)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'tag_name': tag,
|
||||||
|
'name': "{} - RC".format(version),
|
||||||
|
'target_commitish': commit_sha,
|
||||||
|
'prerelease': True
|
||||||
|
}
|
||||||
|
|
||||||
|
response = _github_request('post', 'repos/{}/releases'.format(repo), params)
|
||||||
|
|
||||||
|
if response.status_code != 201:
|
||||||
|
raise exception_from_error("Failed creating new release", response)
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def upload_asset(release, filepath):
|
||||||
|
upload_url = release['upload_url'].replace('{?name}', '')
|
||||||
|
filename = filepath.split('/')[-1]
|
||||||
|
|
||||||
|
with open(filepath) as file_content:
|
||||||
|
headers = {'Content-Type': 'application/gzip'}
|
||||||
|
response = requests.post(upload_url, file_content, params={'name': filename}, headers=headers, auth=auth, verify=False)
|
||||||
|
|
||||||
|
if response.status_code != 201: # not 200/201/...
|
||||||
|
raise exception_from_error('Failed uploading asset', response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def remove_previous_builds(release):
|
||||||
|
for asset in release['assets']:
|
||||||
|
response = _github_request('delete', asset['url'])
|
||||||
|
if response.status_code != 204:
|
||||||
|
raise exception_from_error("Failed deleting asset", response)
|
||||||
|
|
||||||
|
def get_changelog(commit_sha):
|
||||||
|
latest_release = _github_request('get', 'repos/{}/releases/latest'.format(repo))
|
||||||
|
if latest_release.status_code != 200:
|
||||||
|
raise exception_from_error('Failed getting latest release', latest_release)
|
||||||
|
|
||||||
|
latest_release = latest_release.json()
|
||||||
|
previous_sha = latest_release['target_commitish']
|
||||||
|
|
||||||
|
args = ['git', '--no-pager', 'log', '--merges', '--grep', 'Merge pull request', '--pretty=format:"%h|%s|%b|%p"', '{}...{}'.format(previous_sha, commit_sha)]
|
||||||
|
log = subprocess.check_output(args)
|
||||||
|
changes = ["Changes since {}:".format(latest_release['name'])]
|
||||||
|
|
||||||
|
for line in log.split('\n'):
|
||||||
|
try:
|
||||||
|
sha, subject, body, parents = line[1:-1].split('|')
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
pull_request = re.match("Merge pull request #(\d+)", subject).groups()[0]
|
||||||
|
pull_request = " #{}".format(pull_request)
|
||||||
|
except Exception, ex:
|
||||||
|
pull_request = ""
|
||||||
|
|
||||||
|
author = subprocess.check_output(['git', 'log', '-1', '--pretty=format:"%an"', parents.split(' ')[-1]])[1:-1]
|
||||||
|
|
||||||
|
changes.append("{}{}: {} ({})".format(sha, pull_request, body.strip(), author))
|
||||||
|
|
||||||
|
return "\n".join(changes)
|
||||||
|
|
||||||
|
def update_release_commit_sha(release, commit_sha):
|
||||||
|
params = {
|
||||||
|
'target_commitish': commit_sha,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = _github_request('patch', 'repos/{}/releases/{}'.format(repo, release['id']), params)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise exception_from_error("Failed updating commit sha for existing release", response)
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def update_release(version, build_filepath, commit_sha):
|
||||||
|
try:
|
||||||
|
release = get_rc_release(version)
|
||||||
|
if release:
|
||||||
|
release = update_release_commit_sha(release, commit_sha)
|
||||||
|
else:
|
||||||
|
release = create_release(version, commit_sha)
|
||||||
|
|
||||||
|
print "Using release id: {}".format(release['id'])
|
||||||
|
|
||||||
|
remove_previous_builds(release)
|
||||||
|
response = upload_asset(release, build_filepath)
|
||||||
|
|
||||||
|
changelog = get_changelog(commit_sha)
|
||||||
|
|
||||||
|
response = _github_request('patch', release['url'], {'body': changelog})
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise exception_from_error("Failed updating release description", response)
|
||||||
|
|
||||||
|
except Exception, ex:
|
||||||
|
print ex
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
commit_sha = sys.argv[1]
|
||||||
|
version = sys.argv[2]
|
||||||
|
filepath = sys.argv[3]
|
||||||
|
|
||||||
|
# TODO: make sure running from git directory & remote = repo
|
||||||
|
update_release(version, filepath, commit_sha)
|
||||||
10
bin/run
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Ideally I would use stdin with source, but in older bash versions this
|
||||||
|
# wasn't supported properly.
|
||||||
|
TEMP_ENV_FILE=`mktemp /tmp/redash_env.XXXXXX`
|
||||||
|
sed 's/^REDASH/export REDASH/' .env > $TEMP_ENV_FILE
|
||||||
|
source $TEMP_ENV_FILE
|
||||||
|
rm $TEMP_ENV_FILE
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
63
bin/test_multithreading.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
"""
|
||||||
|
Script to test concurrency (multithreading/multiprocess) issues with the workers. Use with caution.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import atfork
|
||||||
|
atfork.monkeypatch_os_fork_functions()
|
||||||
|
import atfork.stdlib_fixer
|
||||||
|
atfork.stdlib_fixer.fix_logging_module()
|
||||||
|
|
||||||
|
import time
|
||||||
|
from redash.data import worker
|
||||||
|
from redash import models, data_manager, redis_connection
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
models.create_db(True, False)
|
||||||
|
|
||||||
|
print "Creating data source..."
|
||||||
|
data_source = models.DataSource.create(name="Concurrency", type="pg", options="dbname=postgres")
|
||||||
|
|
||||||
|
print "Clear jobs/hashes:"
|
||||||
|
redis_connection.delete("jobs")
|
||||||
|
query_hashes = redis_connection.keys("query_hash_*")
|
||||||
|
if query_hashes:
|
||||||
|
redis_connection.delete(*query_hashes)
|
||||||
|
|
||||||
|
starting_query_results_count = models.QueryResult.select().count()
|
||||||
|
jobs_count = 5000
|
||||||
|
workers_count = 10
|
||||||
|
|
||||||
|
print "Creating jobs..."
|
||||||
|
for i in xrange(jobs_count):
|
||||||
|
query = "SELECT {}".format(i)
|
||||||
|
print "Inserting: {}".format(query)
|
||||||
|
data_manager.add_job(query=query, priority=worker.Job.LOW_PRIORITY,
|
||||||
|
data_source=data_source)
|
||||||
|
|
||||||
|
print "Starting workers..."
|
||||||
|
workers = data_manager.start_workers(workers_count)
|
||||||
|
|
||||||
|
print "Waiting for jobs to be done..."
|
||||||
|
keep_waiting = True
|
||||||
|
while keep_waiting:
|
||||||
|
results_count = models.QueryResult.select().count() - starting_query_results_count
|
||||||
|
print "QueryResults: {}".format(results_count)
|
||||||
|
time.sleep(5)
|
||||||
|
if results_count == jobs_count:
|
||||||
|
print "Yay done..."
|
||||||
|
keep_waiting = False
|
||||||
|
|
||||||
|
data_manager.stop_workers()
|
||||||
|
|
||||||
|
qr_count = 0
|
||||||
|
for qr in models.QueryResult.select():
|
||||||
|
number = int(qr.query.split()[1])
|
||||||
|
data_number = json.loads(qr.data)['rows'][0].values()[0]
|
||||||
|
|
||||||
|
if number != data_number:
|
||||||
|
print "Oops? {} != {} ({})".format(number, data_number, qr.id)
|
||||||
|
qr_count += 1
|
||||||
|
|
||||||
|
print "Verified {} query results.".format(qr_count)
|
||||||
|
|
||||||
|
print "Done."
|
||||||
35
circle.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
machine:
|
||||||
|
node:
|
||||||
|
version:
|
||||||
|
0.10.24
|
||||||
|
python:
|
||||||
|
version:
|
||||||
|
2.7.3
|
||||||
|
dependencies:
|
||||||
|
pre:
|
||||||
|
- wget http://downloads.sourceforge.net/project/optipng/OptiPNG/optipng-0.7.5/optipng-0.7.5.tar.gz
|
||||||
|
- tar xvf optipng-0.7.5.tar.gz
|
||||||
|
- cd optipng-0.7.5; ./configure; make; sudo checkinstall -y;
|
||||||
|
- make deps
|
||||||
|
- pip install -r dev_requirements.txt
|
||||||
|
- pip install -r requirements.txt
|
||||||
|
cache_directories:
|
||||||
|
- rd_ui/node_modules/
|
||||||
|
- rd_ui/app/bower_components/
|
||||||
|
test:
|
||||||
|
override:
|
||||||
|
- make test
|
||||||
|
post:
|
||||||
|
- make pack
|
||||||
|
deployment:
|
||||||
|
github:
|
||||||
|
branch: master
|
||||||
|
commands:
|
||||||
|
- make upload
|
||||||
|
notify:
|
||||||
|
webhooks:
|
||||||
|
- url: https://webhooks.gitter.im/e/895d09c3165a0913ac2f
|
||||||
|
general:
|
||||||
|
branches:
|
||||||
|
ignore:
|
||||||
|
- gh-pages
|
||||||
3
dev_requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
nose==1.3.0
|
||||||
|
coverage==3.7.1
|
||||||
|
mock==1.0.1
|
||||||
55
manage.py
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
CLI to manage redash.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
from flask.ext.script import Manager
|
||||||
|
|
||||||
|
from redash import settings, models, __version__
|
||||||
|
from redash.wsgi import app
|
||||||
|
from redash.import_export import import_manager
|
||||||
|
from redash.cli import users, database, data_sources
|
||||||
|
from redash.monitor import get_status
|
||||||
|
|
||||||
|
manager = Manager(app)
|
||||||
|
manager.add_command("database", database.manager)
|
||||||
|
manager.add_command("users", users.manager)
|
||||||
|
manager.add_command("import", import_manager)
|
||||||
|
manager.add_command("ds", data_sources.manager)
|
||||||
|
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def version():
|
||||||
|
"""Displays re:dash version."""
|
||||||
|
print __version__
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def status():
|
||||||
|
print json.dumps(get_status(), indent=2)
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def runworkers():
|
||||||
|
"""Start workers (deprecated)."""
|
||||||
|
print "** This command is deprecated. Please use Celery's CLI to control the workers. **"
|
||||||
|
|
||||||
|
|
||||||
|
@manager.shell
|
||||||
|
def make_shell_context():
|
||||||
|
from redash.models import db
|
||||||
|
return dict(app=app, db=db, models=models)
|
||||||
|
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def check_settings():
|
||||||
|
"""Show the settings as re:dash sees them (useful for debugging)."""
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
for name in dir(settings):
|
||||||
|
item = getattr(settings, name)
|
||||||
|
if not callable(item) and not name.startswith("__") and not isinstance(item, ModuleType):
|
||||||
|
print "{} = {}".format(name, item)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
manager.run()
|
||||||
15
migrations/0001_allow_delete_query.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from playhouse.migrate import PostgresqlMigrator, migrate
|
||||||
|
|
||||||
|
from redash.models import db
|
||||||
|
from redash import models
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
migrator = PostgresqlMigrator(db.database)
|
||||||
|
|
||||||
|
with db.database.transaction():
|
||||||
|
migrate(
|
||||||
|
migrator.add_column('queries', 'is_archived', models.Query.is_archived)
|
||||||
|
)
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
21
migrations/0002_fix_timestamp_fields.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from redash.models import db
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
columns = (
|
||||||
|
('activity_log', 'created_at'),
|
||||||
|
('dashboards', 'created_at'),
|
||||||
|
('data_sources', 'created_at'),
|
||||||
|
('events', 'created_at'),
|
||||||
|
('groups', 'created_at'),
|
||||||
|
('queries', 'created_at'),
|
||||||
|
('widgets', 'created_at'),
|
||||||
|
('query_results', 'retrieved_at')
|
||||||
|
)
|
||||||
|
|
||||||
|
with db.database.transaction():
|
||||||
|
for column in columns:
|
||||||
|
db.database.execute_sql("ALTER TABLE {} ALTER COLUMN {} TYPE timestamp with time zone;".format(*column))
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
|
|
||||||
73
migrations/0003_update_data_source_config.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from redash import query_runner
|
||||||
|
from redash.models import DataSource
|
||||||
|
|
||||||
|
|
||||||
|
def update(data_source):
|
||||||
|
print "[%s] Old options: %s" % (data_source.name, data_source.options)
|
||||||
|
|
||||||
|
if query_runner.validate_configuration(data_source.type, data_source.options):
|
||||||
|
print "[%s] configuration already valid. skipping." % data_source.name
|
||||||
|
return
|
||||||
|
|
||||||
|
if data_source.type == 'pg':
|
||||||
|
values = data_source.options.split(" ")
|
||||||
|
configuration = {}
|
||||||
|
for value in values:
|
||||||
|
k, v = value.split("=", 1)
|
||||||
|
configuration[k] = v
|
||||||
|
if k == 'port':
|
||||||
|
configuration[k] = int(v)
|
||||||
|
|
||||||
|
data_source.options = json.dumps(configuration)
|
||||||
|
|
||||||
|
elif data_source.type == 'mysql':
|
||||||
|
mapping = {
|
||||||
|
'Server': 'host',
|
||||||
|
'User': 'user',
|
||||||
|
'Pwd': 'passwd',
|
||||||
|
'Database': 'db'
|
||||||
|
}
|
||||||
|
|
||||||
|
values = data_source.options.split(";")
|
||||||
|
configuration = {}
|
||||||
|
for value in values:
|
||||||
|
k, v = value.split("=", 1)
|
||||||
|
configuration[mapping[k]] = v
|
||||||
|
data_source.options = json.dumps(configuration)
|
||||||
|
|
||||||
|
elif data_source.type == 'graphite':
|
||||||
|
old_config = json.loads(data_source.options)
|
||||||
|
|
||||||
|
configuration = {
|
||||||
|
"url": old_config["url"]
|
||||||
|
}
|
||||||
|
|
||||||
|
if "verify" in old_config:
|
||||||
|
configuration['verify'] = old_config['verify']
|
||||||
|
|
||||||
|
if "auth" in old_config:
|
||||||
|
configuration['username'], configuration['password'] = old_config["auth"]
|
||||||
|
|
||||||
|
data_source.options = json.dumps(configuration)
|
||||||
|
|
||||||
|
elif data_source.type == 'url':
|
||||||
|
data_source.options = json.dumps({"url": data_source.options})
|
||||||
|
|
||||||
|
elif data_source.type == 'script':
|
||||||
|
data_source.options = json.dumps({"path": data_source.options})
|
||||||
|
|
||||||
|
elif data_source.type == 'mongo':
|
||||||
|
data_source.type = 'mongodb'
|
||||||
|
|
||||||
|
else:
|
||||||
|
print "[%s] No need to convert type of: %s" % (data_source.name, data_source.type)
|
||||||
|
|
||||||
|
print "[%s] New options: %s" % (data_source.name, data_source.options)
|
||||||
|
data_source.save()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
for data_source in DataSource.all():
|
||||||
|
update(data_source)
|
||||||
12
migrations/0004_allow_null_in_event_user.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from playhouse.migrate import PostgresqlMigrator, migrate
|
||||||
|
|
||||||
|
from redash.models import db
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
migrator = PostgresqlMigrator(db.database)
|
||||||
|
|
||||||
|
with db.database.transaction():
|
||||||
|
migrate(
|
||||||
|
migrator.drop_not_null('events', 'user_id')
|
||||||
|
)
|
||||||
26
migrations/0005_add_updated_at.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from playhouse.migrate import PostgresqlMigrator, migrate
|
||||||
|
|
||||||
|
from redash.models import db
|
||||||
|
from redash import models
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
migrator = PostgresqlMigrator(db.database)
|
||||||
|
|
||||||
|
with db.database.transaction():
|
||||||
|
migrate(
|
||||||
|
migrator.add_column('queries', 'updated_at', models.Query.updated_at),
|
||||||
|
migrator.add_column('dashboards', 'updated_at', models.Dashboard.updated_at),
|
||||||
|
migrator.add_column('widgets', 'updated_at', models.Widget.updated_at),
|
||||||
|
migrator.add_column('users', 'created_at', models.User.created_at),
|
||||||
|
migrator.add_column('users', 'updated_at', models.User.updated_at),
|
||||||
|
migrator.add_column('visualizations', 'created_at', models.Visualization.created_at),
|
||||||
|
migrator.add_column('visualizations', 'updated_at', models.Visualization.updated_at)
|
||||||
|
)
|
||||||
|
|
||||||
|
db.database.execute_sql("UPDATE queries SET updated_at = created_at;")
|
||||||
|
db.database.execute_sql("UPDATE dashboards SET updated_at = created_at;")
|
||||||
|
db.database.execute_sql("UPDATE widgets SET updated_at = created_at;")
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
|
|
||||||
19
migrations/0006_queries_last_edit_by.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from playhouse.migrate import PostgresqlMigrator, migrate
|
||||||
|
|
||||||
|
from redash.models import db
|
||||||
|
from redash import models
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
migrator = PostgresqlMigrator(db.database)
|
||||||
|
|
||||||
|
with db.database.transaction():
|
||||||
|
migrate(
|
||||||
|
migrator.add_column('queries', 'last_modified_by_id', models.Query.last_modified_by)
|
||||||
|
)
|
||||||
|
|
||||||
|
db.database.execute_sql("UPDATE queries SET last_modified_by_id = user_id;")
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
|
|
||||||
|
|
||||||
23
migrations/0007_add_schedule_to_queries.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from playhouse.migrate import PostgresqlMigrator, migrate
|
||||||
|
|
||||||
|
from redash.models import db
|
||||||
|
from redash import models
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
migrator = PostgresqlMigrator(db.database)
|
||||||
|
|
||||||
|
with db.database.transaction():
|
||||||
|
migrate(
|
||||||
|
migrator.add_column('queries', 'schedule', models.Query.schedule),
|
||||||
|
)
|
||||||
|
|
||||||
|
db.database.execute_sql("UPDATE queries SET schedule = ttl WHERE ttl > 0;")
|
||||||
|
|
||||||
|
migrate(
|
||||||
|
migrator.drop_column('queries', 'ttl')
|
||||||
|
)
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
|
|
||||||
|
|
||||||
20
migrations/0008_make_ds_name_unique.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from redash.models import db
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
|
||||||
|
with db.database.transaction():
|
||||||
|
# Make sure all data sources names are unique.
|
||||||
|
db.database.execute_sql("""
|
||||||
|
UPDATE data_sources
|
||||||
|
SET name = new_names.name
|
||||||
|
FROM (
|
||||||
|
SELECT id, name || ' ' || id as name
|
||||||
|
FROM (SELECT id, name, rank() OVER (PARTITION BY name ORDER BY created_at ASC) FROM data_sources) ds WHERE rank > 1
|
||||||
|
) AS new_names
|
||||||
|
WHERE data_sources.id = new_names.id;
|
||||||
|
""")
|
||||||
|
# Add unique constraint on data_sources.name.
|
||||||
|
db.database.execute_sql("ALTER TABLE data_sources ADD CONSTRAINT unique_name UNIQUE (name);")
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
13
migrations/add_created_at_field.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from playhouse.migrate import Migrator
|
||||||
|
from redash import db
|
||||||
|
from redash import models
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
migrator = Migrator(db.database)
|
||||||
|
with db.database.transaction():
|
||||||
|
migrator.add_column(models.Dashboard, models.Dashboard.created_at, 'created_at')
|
||||||
|
migrator.add_column(models.Widget, models.Widget.created_at, 'created_at')
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
12
migrations/add_global_filters_to_dashboard.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from playhouse.migrate import Migrator
|
||||||
|
from redash import models
|
||||||
|
from redash.models import db
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
migrator = Migrator(db.database)
|
||||||
|
with db.database.transaction():
|
||||||
|
migrator.add_column(models.Dashboard, models.Dashboard.dashboard_filters_enabled, 'dashboard_filters_enabled')
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
12
migrations/add_password_to_users.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from playhouse.migrate import Migrator
|
||||||
|
from redash import db
|
||||||
|
from redash import models
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
migrator = Migrator(db.database)
|
||||||
|
with db.database.transaction():
|
||||||
|
migrator.add_column(models.User, models.User.password_hash, 'password_hash')
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
13
migrations/add_permissions_to_user.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from playhouse.migrate import Migrator
|
||||||
|
from redash import db
|
||||||
|
from redash import models
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
migrator = Migrator(db.database)
|
||||||
|
with db.database.transaction():
|
||||||
|
migrator.add_column(models.User, models.User.permissions, 'permissions')
|
||||||
|
models.User.update(permissions=['admin'] + models.User.DEFAULT_PERMISSIONS).where(models.User.is_admin == True).execute()
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
13
migrations/add_queue_name_to_data_source.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from playhouse.migrate import Migrator
|
||||||
|
from redash.models import db
|
||||||
|
from redash import models
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
migrator = Migrator(db.database)
|
||||||
|
with db.database.transaction():
|
||||||
|
migrator.add_column(models.DataSource, models.DataSource.queue_name, 'queue_name')
|
||||||
|
migrator.add_column(models.DataSource, models.DataSource.scheduled_queue_name, 'scheduled_queue_name')
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
13
migrations/add_text_to_widgets.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from playhouse.migrate import Migrator
|
||||||
|
from redash.models import db
|
||||||
|
from redash import models
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
migrator = Migrator(db.database)
|
||||||
|
with db.database.transaction():
|
||||||
|
migrator.add_column(models.Widget, models.Widget.text, 'text')
|
||||||
|
migrator.set_nullable(models.Widget, models.Widget.visualization, True)
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
13
migrations/add_view_query_permission.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import peewee
|
||||||
|
from redash import db
|
||||||
|
from redash import models
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
|
||||||
|
previous_default_permissions = models.User.DEFAULT_PERMISSIONS[:]
|
||||||
|
previous_default_permissions.remove('view_query')
|
||||||
|
models.User.update(permissions=peewee.fn.array_append(models.User.permissions, 'view_query')).where(peewee.SQL("'view_source' = any(permissions)")).execute()
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
12
migrations/change_queries_description_to_nullable.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from playhouse.migrate import Migrator
|
||||||
|
from redash import db
|
||||||
|
from redash import models
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
migrator = Migrator(db.database)
|
||||||
|
with db.database.transaction():
|
||||||
|
migrator.set_nullable(models.Query, models.Query.description, True)
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
13
migrations/change_query_id_on_widgets_to_null.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from playhouse.migrate import Migrator
|
||||||
|
from redash import db
|
||||||
|
from redash import models
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
migrator = Migrator(db.database)
|
||||||
|
with db.database.transaction():
|
||||||
|
migrator.set_nullable(models.Widget, models.Widget.query_id, True)
|
||||||
|
migrator.set_nullable(models.Widget, models.Widget.type, True)
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
11
migrations/create_activity_log.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from redash import db
|
||||||
|
from redash import models
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
|
||||||
|
if not models.ActivityLog.table_exists():
|
||||||
|
print "Creating activity_log table..."
|
||||||
|
models.ActivityLog.create_table()
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
48
migrations/create_data_sources.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import logging
|
||||||
|
import peewee
|
||||||
|
from playhouse.migrate import Migrator
|
||||||
|
from redash import db
|
||||||
|
from redash import models
|
||||||
|
from redash import settings
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
|
||||||
|
if not models.DataSource.table_exists():
|
||||||
|
print "Creating data_sources table..."
|
||||||
|
models.DataSource.create_table()
|
||||||
|
|
||||||
|
default_data_source = models.DataSource.create(name="Default",
|
||||||
|
type=settings.CONNECTION_ADAPTER,
|
||||||
|
options=settings.CONNECTION_STRING)
|
||||||
|
else:
|
||||||
|
default_data_source = models.DataSource.select().first()
|
||||||
|
|
||||||
|
migrator = Migrator(db.database)
|
||||||
|
models.Query.data_source.null = True
|
||||||
|
models.QueryResult.data_source.null = True
|
||||||
|
try:
|
||||||
|
with db.database.transaction():
|
||||||
|
migrator.add_column(models.Query, models.Query.data_source, "data_source_id")
|
||||||
|
except peewee.ProgrammingError:
|
||||||
|
print "Failed to create data_source_id column -- assuming it already exists"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with db.database.transaction():
|
||||||
|
migrator.add_column(models.QueryResult, models.QueryResult.data_source, "data_source_id")
|
||||||
|
except peewee.ProgrammingError:
|
||||||
|
print "Failed to create data_source_id column -- assuming it already exists"
|
||||||
|
|
||||||
|
print "Updating data source to existing one..."
|
||||||
|
models.Query.update(data_source=default_data_source.id).execute()
|
||||||
|
models.QueryResult.update(data_source=default_data_source.id).execute()
|
||||||
|
|
||||||
|
with db.database.transaction():
|
||||||
|
print "Setting data source to non nullable..."
|
||||||
|
migrator.set_nullable(models.Query, models.Query.data_source, False)
|
||||||
|
|
||||||
|
with db.database.transaction():
|
||||||
|
print "Setting data source to non nullable..."
|
||||||
|
migrator.set_nullable(models.QueryResult, models.QueryResult.data_source, False)
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
12
migrations/create_events.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from redash.models import db
|
||||||
|
from redash import models
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
|
||||||
|
if not models.Event.table_exists():
|
||||||
|
print "Creating events table..."
|
||||||
|
models.Event.create_table()
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
56
migrations/create_users.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import json
|
||||||
|
import itertools
|
||||||
|
import peewee
|
||||||
|
from playhouse.migrate import Migrator
|
||||||
|
from redash import db, settings
|
||||||
|
from redash import models
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
|
||||||
|
if not models.User.table_exists():
|
||||||
|
print "Creating user table..."
|
||||||
|
models.User.create_table()
|
||||||
|
|
||||||
|
migrator = Migrator(db.database)
|
||||||
|
with db.database.transaction():
|
||||||
|
print "Creating user field on dashboard and queries..."
|
||||||
|
try:
|
||||||
|
migrator.rename_column(models.Query, '"user"', "user_email")
|
||||||
|
migrator.rename_column(models.Dashboard, '"user"', "user_email")
|
||||||
|
except peewee.ProgrammingError:
|
||||||
|
print "Failed to rename user column -- assuming it already exists"
|
||||||
|
|
||||||
|
with db.database.transaction():
|
||||||
|
models.Query.user.null = True
|
||||||
|
models.Dashboard.user.null = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
migrator.add_column(models.Query, models.Query.user, "user_id")
|
||||||
|
migrator.add_column(models.Dashboard, models.Dashboard.user, "user_id")
|
||||||
|
except peewee.ProgrammingError:
|
||||||
|
print "Failed to create user_id column -- assuming it already exists"
|
||||||
|
|
||||||
|
print "Creating user for all queries and dashboards..."
|
||||||
|
for obj in itertools.chain(models.Query.select(), models.Dashboard.select()):
|
||||||
|
# Some old databases might have queries with empty string as user email:
|
||||||
|
email = obj.user_email or settings.ADMINS[0]
|
||||||
|
email = email.split(',')[0]
|
||||||
|
|
||||||
|
print ".. {} , {}, {}".format(type(obj), obj.id, email)
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = models.User.get(models.User.email == email)
|
||||||
|
except models.User.DoesNotExist:
|
||||||
|
is_admin = email in settings.ADMINS
|
||||||
|
user = models.User.create(email=email, name=email, is_admin=is_admin)
|
||||||
|
|
||||||
|
obj.user = user
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
print "Set user_id to non null..."
|
||||||
|
with db.database.transaction():
|
||||||
|
migrator.set_nullable(models.Query, models.Query.user, False)
|
||||||
|
migrator.set_nullable(models.Dashboard, models.Dashboard.user, False)
|
||||||
|
migrator.set_nullable(models.Query, models.Query.user_email, True)
|
||||||
|
migrator.set_nullable(models.Dashboard, models.Dashboard.user_email, True)
|
||||||
70
migrations/create_visualizations.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import json
|
||||||
|
from playhouse.migrate import Migrator
|
||||||
|
from redash import db
|
||||||
|
from redash import models
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
default_options = {"series": {"type": "column"}}
|
||||||
|
|
||||||
|
db.connect_db()
|
||||||
|
|
||||||
|
if not models.Visualization.table_exists():
|
||||||
|
print "Creating visualization table..."
|
||||||
|
models.Visualization.create_table()
|
||||||
|
|
||||||
|
with db.database.transaction():
|
||||||
|
migrator = Migrator(db.database)
|
||||||
|
print "Adding visualization_id to widgets:"
|
||||||
|
field = models.Widget.visualization
|
||||||
|
field.null = True
|
||||||
|
migrator.add_column(models.Widget, models.Widget.visualization, 'visualization_id')
|
||||||
|
|
||||||
|
print 'Creating TABLE visualizations for all queries...'
|
||||||
|
for query in models.Query.select():
|
||||||
|
vis = models.Visualization(query=query, name="Table",
|
||||||
|
description=query.description or "",
|
||||||
|
type="TABLE", options="{}")
|
||||||
|
vis.save()
|
||||||
|
|
||||||
|
print 'Creating COHORT visualizations for all queries named like %cohort%...'
|
||||||
|
for query in models.Query.select().where(models.Query.name ** "%cohort%"):
|
||||||
|
vis = models.Visualization(query=query, name="Cohort",
|
||||||
|
description=query.description or "",
|
||||||
|
type="COHORT", options="{}")
|
||||||
|
vis.save()
|
||||||
|
|
||||||
|
print 'Create visualization for all widgets (unless exists already):'
|
||||||
|
for widget in models.Widget.select():
|
||||||
|
print 'Processing widget id: %d:' % widget.id
|
||||||
|
vis_type = widget.type.upper()
|
||||||
|
if vis_type == 'GRID':
|
||||||
|
vis_type = 'TABLE'
|
||||||
|
|
||||||
|
query = models.Query.get_by_id(widget.query_id)
|
||||||
|
vis = query.visualizations.where(models.Visualization.type == vis_type).first()
|
||||||
|
if vis:
|
||||||
|
print '... visualization type (%s) found.' % vis_type
|
||||||
|
widget.visualization = vis
|
||||||
|
widget.save()
|
||||||
|
else:
|
||||||
|
vis_name = vis_type.title()
|
||||||
|
|
||||||
|
options = json.loads(widget.options)
|
||||||
|
vis_options = {"series": options} if options else default_options
|
||||||
|
vis_options = json.dumps(vis_options)
|
||||||
|
|
||||||
|
vis = models.Visualization(query=query, name=vis_name,
|
||||||
|
description=query.description or "",
|
||||||
|
type=vis_type, options=vis_options)
|
||||||
|
|
||||||
|
print '... Created visualization for type: %s' % vis_type
|
||||||
|
vis.save()
|
||||||
|
widget.visualization = vis
|
||||||
|
widget.save()
|
||||||
|
|
||||||
|
with db.database.transaction():
|
||||||
|
migrator = Migrator(db.database)
|
||||||
|
print "Setting visualization_id as not null..."
|
||||||
|
migrator.set_nullable(models.Widget, models.Widget.visualization, False)
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
29
migrations/permissions_migration.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import peewee
|
||||||
|
from playhouse.migrate import Migrator
|
||||||
|
from redash import models
|
||||||
|
from redash.models import db
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect_db()
|
||||||
|
migrator = Migrator(db.database)
|
||||||
|
|
||||||
|
if not models.Group.table_exists():
|
||||||
|
print "Creating groups table..."
|
||||||
|
models.Group.create_table()
|
||||||
|
|
||||||
|
with db.database.transaction():
|
||||||
|
models.Group.insert(name='admin', permissions=['admin'], tables=['*']).execute()
|
||||||
|
models.Group.insert(name='api', permissions=['view_query'], tables=['*']).execute()
|
||||||
|
models.Group.insert(name='default', permissions=models.Group.DEFAULT_PERMISSIONS, tables=['*']).execute()
|
||||||
|
|
||||||
|
migrator.add_column(models.User, models.User.groups, 'groups')
|
||||||
|
|
||||||
|
models.User.update(groups=['admin', 'default']).where(peewee.SQL("is_admin = true")).execute()
|
||||||
|
models.User.update(groups=['admin', 'default']).where(peewee.SQL("'admin' = any(permissions)")).execute()
|
||||||
|
models.User.update(groups=['default']).where(peewee.SQL("is_admin = false")).execute()
|
||||||
|
|
||||||
|
migrator.drop_column(models.User, 'permissions')
|
||||||
|
migrator.drop_column(models.User, 'is_admin')
|
||||||
|
|
||||||
|
db.close_db(None)
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
"""
|
|
||||||
CLI to start the workers.
|
|
||||||
|
|
||||||
TODO: move API server startup here.
|
|
||||||
"""
|
|
||||||
import argparse
|
|
||||||
import logging
|
|
||||||
import urlparse
|
|
||||||
import redis
|
|
||||||
import time
|
|
||||||
import settings
|
|
||||||
import data
|
|
||||||
|
|
||||||
|
|
||||||
def start_workers(data_manager):
|
|
||||||
try:
|
|
||||||
data_manager.start_workers(settings.WORKERS_COUNT, settings.CONNECTION_STRING,
|
|
||||||
settings.MAX_CONNECTIONS)
|
|
||||||
logging.info("Workers started.")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
data_manager.refresh_queries()
|
|
||||||
except Exception:
|
|
||||||
logging.error("Something went wrong with refreshing queries...");
|
|
||||||
time.sleep(60)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
logging.warning("Exiting; waiting for threads")
|
|
||||||
data_manager.stop_workers()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
channel = logging.StreamHandler()
|
|
||||||
logging.getLogger().addHandler(channel)
|
|
||||||
logging.getLogger().setLevel("DEBUG")
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("command")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
url = urlparse.urlparse(settings.REDIS_URL)
|
|
||||||
redis_connection = redis.StrictRedis(host=url.hostname, port=url.port, db=0, password=url.password)
|
|
||||||
data_manager = data.Manager(redis_connection, settings.INTERNAL_DB_CONNECTION_STRING, settings.MAX_CONNECTIONS)
|
|
||||||
|
|
||||||
if args.command == "worker":
|
|
||||||
start_workers(data_manager)
|
|
||||||
else:
|
|
||||||
print "Unknown command"
|
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
from manager import Manager
|
|
||||||
from worker import Job
|
|
||||||
import models
|
|
||||||
import utils
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
"""
|
|
||||||
Data manager. Used to manage and coordinate execution of queries.
|
|
||||||
"""
|
|
||||||
import collections
|
|
||||||
from contextlib import contextmanager
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import psycopg2
|
|
||||||
import psycopg2.pool
|
|
||||||
import qr
|
|
||||||
import redis
|
|
||||||
import query_runner
|
|
||||||
import worker
|
|
||||||
from utils import gen_query_hash
|
|
||||||
|
|
||||||
|
|
||||||
class QueryResult(collections.namedtuple('QueryData', 'id query data runtime retrieved_at query_hash')):
|
|
||||||
def to_dict(self, parse_data=False):
|
|
||||||
d = self._asdict()
|
|
||||||
|
|
||||||
if parse_data and d['data']:
|
|
||||||
d['data'] = json.loads(d['data'])
|
|
||||||
|
|
||||||
return d
|
|
||||||
|
|
||||||
|
|
||||||
class Manager(object):
|
|
||||||
def __init__(self, redis_connection, db_connection_string, db_max_connections):
|
|
||||||
self.redis_connection = redis_connection
|
|
||||||
self.workers = []
|
|
||||||
self.db_connection_pool = psycopg2.pool.ThreadedConnectionPool(1, db_max_connections,
|
|
||||||
db_connection_string)
|
|
||||||
self.queue = qr.PriorityQueue("jobs")
|
|
||||||
self.max_retries = 5
|
|
||||||
|
|
||||||
# TODO: Use our Django Models
|
|
||||||
def get_query_result_by_id(self, query_result_id):
|
|
||||||
with self.db_transaction() as cursor:
|
|
||||||
sql = "SELECT id, query, data, runtime, retrieved_at, query_hash FROM query_results " \
|
|
||||||
"WHERE id=%s LIMIT 1"
|
|
||||||
cursor.execute(sql, (query_result_id,))
|
|
||||||
query_result = cursor.fetchone()
|
|
||||||
|
|
||||||
if query_result:
|
|
||||||
query_result = QueryResult(*query_result)
|
|
||||||
|
|
||||||
return query_result
|
|
||||||
|
|
||||||
def get_query_result(self, query, ttl=0):
|
|
||||||
query_hash = gen_query_hash(query)
|
|
||||||
|
|
||||||
with self.db_transaction() as cursor:
|
|
||||||
sql = "SELECT id, query, data, runtime, retrieved_at, query_hash FROM query_results " \
|
|
||||||
"WHERE query_hash=%s " \
|
|
||||||
"AND retrieved_at < now() at time zone 'utc' - interval '%s second'" \
|
|
||||||
"ORDER BY retrieved_at DESC LIMIT 1"
|
|
||||||
cursor.execute(sql, (query_hash, psycopg2.extensions.AsIs(ttl)))
|
|
||||||
query_result = cursor.fetchone()
|
|
||||||
|
|
||||||
if query_result:
|
|
||||||
query_result = QueryResult(*query_result)
|
|
||||||
|
|
||||||
return query_result
|
|
||||||
|
|
||||||
def add_job(self, query, priority):
|
|
||||||
query_hash = gen_query_hash(query)
|
|
||||||
logging.info("[Manager][%s] Inserting job with priority=%s", query_hash, priority)
|
|
||||||
try_count = 0
|
|
||||||
job = None
|
|
||||||
|
|
||||||
while try_count < self.max_retries:
|
|
||||||
try_count += 1
|
|
||||||
|
|
||||||
pipe = self.redis_connection.pipeline()
|
|
||||||
try:
|
|
||||||
pipe.watch('query_hash_job:%s' % query_hash)
|
|
||||||
job_id = pipe.get('query_hash_job:%s' % query_hash)
|
|
||||||
if job_id:
|
|
||||||
logging.info("[Manager][%s] Found existing job: %s", query_hash, job_id)
|
|
||||||
job = worker.Job.load(self, job_id)
|
|
||||||
else:
|
|
||||||
job = worker.Job(self, query, priority)
|
|
||||||
pipe.multi()
|
|
||||||
job.save(pipe)
|
|
||||||
logging.info("[Manager][%s] Created new job: %s", query_hash, job.id)
|
|
||||||
self.queue.push(job.id, job.priority)
|
|
||||||
break
|
|
||||||
|
|
||||||
except redis.WatchError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not job:
|
|
||||||
logging.error("[Manager][%s] Failed adding job for query.", query_hash)
|
|
||||||
|
|
||||||
return job
|
|
||||||
|
|
||||||
def refresh_queries(self):
|
|
||||||
sql = """SELECT queries.query, queries.ttl, retrieved_at
|
|
||||||
FROM (SELECT query, min(ttl) as ttl FROM queries WHERE ttl > 0 GROUP by query) queries
|
|
||||||
JOIN (SELECT query, max(retrieved_at) as retrieved_at
|
|
||||||
FROM query_results
|
|
||||||
GROUP BY query) query_results on query_results.query=queries.query
|
|
||||||
WHERE queries.ttl > 0
|
|
||||||
AND query_results.retrieved_at + ttl * interval '1 second' < now() at time zone 'utc';"""
|
|
||||||
|
|
||||||
queries = self.run_query(sql)
|
|
||||||
for query, ttl, retrieved_at in queries:
|
|
||||||
self.add_job(query, worker.Job.LOW_PRIORITY)
|
|
||||||
|
|
||||||
def store_query_result(self, query, data, run_time, retrieved_at):
|
|
||||||
query_result_id = None
|
|
||||||
query_hash = gen_query_hash(query)
|
|
||||||
sql = "INSERT INTO query_results (query_hash, query, data, runtime, retrieved_at) " \
|
|
||||||
"VALUES (%s, %s, %s, %s, %s) RETURNING id"
|
|
||||||
with self.db_transaction() as cursor:
|
|
||||||
cursor.execute(sql, (query_hash, query, data, run_time, retrieved_at))
|
|
||||||
if cursor.rowcount == 1:
|
|
||||||
query_result_id = cursor.fetchone()[0]
|
|
||||||
logging.info("[Manager][%s] Inserted query data; id=%s", query_hash, query_result_id)
|
|
||||||
|
|
||||||
sql = "UPDATE queries SET latest_query_data_id=%s WHERE query_hash=%s"
|
|
||||||
cursor.execute(sql, (query_result_id, query_hash))
|
|
||||||
|
|
||||||
logging.info("[Manager][%s] Updated %s queries.", query_hash, cursor.rowcount)
|
|
||||||
else:
|
|
||||||
logging.error("[Manager][%s] Failed inserting query data.", query_hash)
|
|
||||||
return query_result_id
|
|
||||||
|
|
||||||
def run_query(self, *args):
|
|
||||||
sql = args[0]
|
|
||||||
logging.debug("running query: %s %s", sql, args[1:])
|
|
||||||
|
|
||||||
with self.db_transaction() as cursor:
|
|
||||||
cursor.execute(sql, args[1:])
|
|
||||||
if cursor.description:
|
|
||||||
data = list(cursor)
|
|
||||||
else:
|
|
||||||
data = cursor.rowcount
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def start_workers(self, workers_count, connection_string, max_connections):
|
|
||||||
if self.workers:
|
|
||||||
return self.workers
|
|
||||||
|
|
||||||
# TODO: who closes the connection pool?
|
|
||||||
pg_connection_pool = psycopg2.pool.ThreadedConnectionPool(1, max_connections, connection_string)
|
|
||||||
runner = query_runner.redshift(pg_connection_pool)
|
|
||||||
|
|
||||||
self.workers = [worker.Worker(self, runner) for i in range(workers_count)]
|
|
||||||
for w in self.workers:
|
|
||||||
w.start()
|
|
||||||
|
|
||||||
return self.workers
|
|
||||||
|
|
||||||
def stop_workers(self):
|
|
||||||
for w in self.workers:
|
|
||||||
w.continue_working = False
|
|
||||||
w.join()
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def db_transaction(self):
|
|
||||||
connection = self.db_connection_pool.getconn()
|
|
||||||
cursor = connection.cursor()
|
|
||||||
try:
|
|
||||||
yield cursor
|
|
||||||
except:
|
|
||||||
connection.rollback()
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
connection.commit()
|
|
||||||
finally:
|
|
||||||
self.db_connection_pool.putconn(connection)
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
"""
|
|
||||||
Django ORM based models to describe the data model of re:dash.
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
from django.db import models
|
|
||||||
from django.template.defaultfilters import slugify
|
|
||||||
import utils
|
|
||||||
|
|
||||||
|
|
||||||
class QueryResult(models.Model):
|
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
query_hash = models.CharField(max_length=32)
|
|
||||||
query = models.TextField()
|
|
||||||
data = models.TextField()
|
|
||||||
runtime = models.FloatField()
|
|
||||||
retrieved_at = models.DateTimeField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
app_label = 'redash'
|
|
||||||
db_table = 'query_results'
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {
|
|
||||||
'id': self.id,
|
|
||||||
'query_hash': self.query_hash,
|
|
||||||
'query': self.query,
|
|
||||||
'data': json.loads(self.data),
|
|
||||||
'runtime': self.runtime,
|
|
||||||
'retrieved_at': self.retrieved_at
|
|
||||||
}
|
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
return u"%d | %s | %s" % (self.id, self.query_hash, self.retrieved_at)
|
|
||||||
|
|
||||||
|
|
||||||
class Query(models.Model):
|
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
latest_query_data = models.ForeignKey(QueryResult)
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
description = models.CharField(max_length=4096)
|
|
||||||
query = models.TextField()
|
|
||||||
query_hash = models.CharField(max_length=32)
|
|
||||||
ttl = models.IntegerField()
|
|
||||||
user = models.CharField(max_length=360)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
app_label = 'redash'
|
|
||||||
db_table = 'queries'
|
|
||||||
|
|
||||||
def to_dict(self, with_result=True):
|
|
||||||
d = {
|
|
||||||
'id': self.id,
|
|
||||||
'latest_query_data_id': self.latest_query_data_id,
|
|
||||||
'name': self.name,
|
|
||||||
'description': self.description,
|
|
||||||
'query': self.query,
|
|
||||||
'query_hash': self.query_hash,
|
|
||||||
'ttl': self.ttl,
|
|
||||||
'user': self.user,
|
|
||||||
'created_at': self.created_at,
|
|
||||||
}
|
|
||||||
|
|
||||||
if with_result and self.latest_query_data_id:
|
|
||||||
d['latest_query_data'] = self.latest_query_data.to_dict()
|
|
||||||
|
|
||||||
return d
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
self.query_hash = utils.gen_query_hash(self.query)
|
|
||||||
super(Query, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
return unicode(self.id)
|
|
||||||
|
|
||||||
|
|
||||||
class Dashboard(models.Model):
|
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
slug = models.CharField(max_length=140)
|
|
||||||
name = models.CharField(max_length=100)
|
|
||||||
user = models.CharField(max_length=360)
|
|
||||||
layout = models.TextField()
|
|
||||||
is_archived = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
app_label = 'redash'
|
|
||||||
db_table = 'dashboards'
|
|
||||||
|
|
||||||
def to_dict(self, with_widgets=False):
|
|
||||||
layout = json.loads(self.layout)
|
|
||||||
|
|
||||||
if with_widgets:
|
|
||||||
widgets = {w.id: w.to_dict() for w in self.widgets.all()}
|
|
||||||
widgets_layout = map(lambda row: map(lambda widget_id: widgets.get(widget_id, None), row), layout)
|
|
||||||
else:
|
|
||||||
widgets_layout = None
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': self.id,
|
|
||||||
'slug': self.slug,
|
|
||||||
'name': self.name,
|
|
||||||
'user': self.user,
|
|
||||||
'layout': layout,
|
|
||||||
'widgets': widgets_layout
|
|
||||||
}
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
# TODO: make sure slug is unique
|
|
||||||
if not self.slug:
|
|
||||||
self.slug = slugify(self.name)
|
|
||||||
super(Dashboard, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
return u"%s=%s" % (self.id, self.name)
|
|
||||||
|
|
||||||
|
|
||||||
class Widget(models.Model):
|
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
query = models.ForeignKey(Query)
|
|
||||||
type = models.CharField(max_length=100)
|
|
||||||
width = models.IntegerField()
|
|
||||||
options = models.TextField()
|
|
||||||
dashboard = models.ForeignKey(Dashboard, related_name='widgets')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
app_label = 'redash'
|
|
||||||
db_table = 'widgets'
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {
|
|
||||||
'id': self.id,
|
|
||||||
'query': self.query.to_dict(),
|
|
||||||
'type': self.type,
|
|
||||||
'width': self.width,
|
|
||||||
'options': json.loads(self.options),
|
|
||||||
'dashboard_id': self.dashboard_id
|
|
||||||
}
|
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
return u"%s=>%s" % (self.id, self.dashboard_id)
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
"""
|
|
||||||
QueryRunner is the function that the workers use, to execute queries. This is the Redshift
|
|
||||||
(PostgreSQL in fact) version, but easily we can write another to support additional databases
|
|
||||||
(MySQL and others).
|
|
||||||
|
|
||||||
Because the worker just pass the query, this can be used with any data store that has some sort of
|
|
||||||
query language (for example: HiveQL).
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import psycopg2
|
|
||||||
import sys
|
|
||||||
from .utils import JSONEncoder
|
|
||||||
|
|
||||||
|
|
||||||
def redshift(connection_pool):
|
|
||||||
def column_friendly_name(column_name):
|
|
||||||
return column_name
|
|
||||||
|
|
||||||
def query_runner(query):
|
|
||||||
connection = connection_pool.getconn()
|
|
||||||
cursor = connection.cursor()
|
|
||||||
|
|
||||||
try:
|
|
||||||
cursor.execute(query)
|
|
||||||
|
|
||||||
column_names = [col.name for col in cursor.description]
|
|
||||||
|
|
||||||
rows = [dict(zip(column_names, row)) for row in cursor]
|
|
||||||
columns = [{'name': col.name,
|
|
||||||
'friendly_name': column_friendly_name(col.name),
|
|
||||||
'type': None} for col in cursor.description]
|
|
||||||
|
|
||||||
data = {'columns': columns, 'rows': rows}
|
|
||||||
json_data = json.dumps(data, cls=JSONEncoder)
|
|
||||||
error = None
|
|
||||||
cursor.close()
|
|
||||||
except psycopg2.DatabaseError as e:
|
|
||||||
connection.rollback()
|
|
||||||
json_data = None
|
|
||||||
error = e.message
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
connection.rollback()
|
|
||||||
connection_pool.putconn(connection)
|
|
||||||
raise sys.exc_info()[1], None, sys.exc_info()[2]
|
|
||||||
|
|
||||||
connection_pool.putconn(connection)
|
|
||||||
|
|
||||||
return json_data, error
|
|
||||||
|
|
||||||
return query_runner
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
BEGIN;
|
|
||||||
CREATE TABLE "query_results" (
|
|
||||||
"id" serial NOT NULL PRIMARY KEY,
|
|
||||||
"query_hash" varchar(32) NOT NULL,
|
|
||||||
"query" text NOT NULL,
|
|
||||||
"data" text NOT NULL,
|
|
||||||
"runtime" double precision NOT NULL,
|
|
||||||
"retrieved_at" timestamp with time zone NOT NULL
|
|
||||||
)
|
|
||||||
;
|
|
||||||
CREATE TABLE "queries" (
|
|
||||||
"id" serial NOT NULL PRIMARY KEY,
|
|
||||||
"latest_query_data_id" integer REFERENCES "query_results" ("id") DEFERRABLE INITIALLY DEFERRED,
|
|
||||||
"name" varchar(255) NOT NULL,
|
|
||||||
"description" varchar(4096),
|
|
||||||
"query" text NOT NULL,
|
|
||||||
"query_hash" varchar(32) NOT NULL,
|
|
||||||
"ttl" integer NOT NULL,
|
|
||||||
"user" varchar(360) NOT NULL,
|
|
||||||
"created_at" timestamp with time zone NOT NULL
|
|
||||||
)
|
|
||||||
;
|
|
||||||
CREATE TABLE "dashboards" (
|
|
||||||
"id" serial NOT NULL PRIMARY KEY,
|
|
||||||
"slug" varchar(140) NOT NULL,
|
|
||||||
"name" varchar(100) NOT NULL,
|
|
||||||
"user" varchar(360) NOT NULL,
|
|
||||||
"layout" text NOT NULL,
|
|
||||||
"is_archived" boolean NOT NULL
|
|
||||||
)
|
|
||||||
;
|
|
||||||
CREATE TABLE "widgets" (
|
|
||||||
"id" serial NOT NULL PRIMARY KEY,
|
|
||||||
"query_id" integer NOT NULL REFERENCES "queries" ("id") DEFERRABLE INITIALLY DEFERRED,
|
|
||||||
"type" varchar(100) NOT NULL,
|
|
||||||
"width" integer NOT NULL,
|
|
||||||
"options" text NOT NULL,
|
|
||||||
"dashboard_id" integer NOT NULL REFERENCES "dashboards" ("id") DEFERRABLE INITIALLY DEFERRED
|
|
||||||
)
|
|
||||||
;
|
|
||||||
CREATE INDEX "queries_latest_query_data_id" ON "queries" ("latest_query_data_id");
|
|
||||||
CREATE INDEX "widgets_query_id" ON "widgets" ("query_id");
|
|
||||||
CREATE INDEX "widgets_dashboard_id" ON "widgets" ("dashboard_id");
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import cStringIO
|
|
||||||
import csv
|
|
||||||
import codecs
|
|
||||||
import decimal
|
|
||||||
import datetime
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
COMMENTS_REGEX = re.compile("/\*.*?\*/")
|
|
||||||
|
|
||||||
|
|
||||||
def gen_query_hash(sql):
|
|
||||||
"""Returns hash of the given query after stripping all comments, line breaks and multiple
|
|
||||||
spaces, and lower casing all text.
|
|
||||||
|
|
||||||
TODO: possible issue - the following queries will get the same id:
|
|
||||||
1. SELECT 1 FROM table WHERE column='Value';
|
|
||||||
2. SELECT 1 FROM table where column='value';
|
|
||||||
"""
|
|
||||||
sql = COMMENTS_REGEX.sub("", sql)
|
|
||||||
sql = "".join(sql.split()).lower()
|
|
||||||
return hashlib.md5(sql.encode('utf-8')).hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
class JSONEncoder(json.JSONEncoder):
|
|
||||||
"""Custom JSON encoding class, to handle Decimal and datetime.date instances.
|
|
||||||
"""
|
|
||||||
def default(self, o):
|
|
||||||
if isinstance(o, decimal.Decimal):
|
|
||||||
return float(o)
|
|
||||||
|
|
||||||
if isinstance(o, datetime.date):
|
|
||||||
return o.isoformat()
|
|
||||||
|
|
||||||
super(JSONEncoder, self).default(o)
|
|
||||||
|
|
||||||
|
|
||||||
class UnicodeWriter:
|
|
||||||
"""
|
|
||||||
A CSV writer which will write rows to CSV file "f",
|
|
||||||
which is encoded in the given encoding.
|
|
||||||
"""
|
|
||||||
def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
|
|
||||||
# Redirect output to a queue
|
|
||||||
self.queue = cStringIO.StringIO()
|
|
||||||
self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
|
|
||||||
self.stream = f
|
|
||||||
self.encoder = codecs.getincrementalencoder(encoding)()
|
|
||||||
|
|
||||||
def _encode_utf8(self, val):
|
|
||||||
if isinstance(val, (unicode, str)):
|
|
||||||
return val.encode('utf-8')
|
|
||||||
|
|
||||||
return val
|
|
||||||
|
|
||||||
def writerow(self, row):
|
|
||||||
self.writer.writerow([self._encode_utf8(s) for s in row])
|
|
||||||
# Fetch UTF-8 output from the queue ...
|
|
||||||
data = self.queue.getvalue()
|
|
||||||
data = data.decode("utf-8")
|
|
||||||
# ... and reencode it into the target encoding
|
|
||||||
data = self.encoder.encode(data)
|
|
||||||
# write to the target stream
|
|
||||||
self.stream.write(data)
|
|
||||||
# empty queue
|
|
||||||
self.queue.truncate(0)
|
|
||||||
|
|
||||||
def writerows(self, rows):
|
|
||||||
for row in rows:
|
|
||||||
self.writerow(row)
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
"""
|
|
||||||
Worker implementation to execute incoming queries.
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import threading
|
|
||||||
import uuid
|
|
||||||
import datetime
|
|
||||||
import time
|
|
||||||
from utils import gen_query_hash
|
|
||||||
|
|
||||||
|
|
||||||
class Job(object):
|
|
||||||
HIGH_PRIORITY = 1
|
|
||||||
LOW_PRIORITY = 2
|
|
||||||
|
|
||||||
WAITING = 1
|
|
||||||
PROCESSING = 2
|
|
||||||
DONE = 3
|
|
||||||
FAILED = 4
|
|
||||||
|
|
||||||
def __init__(self, data_manager, query, priority,
|
|
||||||
job_id=None,
|
|
||||||
wait_time=None, query_time=None,
|
|
||||||
updated_at=None, status=None, error=None, query_result_id=None):
|
|
||||||
self.data_manager = data_manager
|
|
||||||
self.query = query
|
|
||||||
self.priority = priority
|
|
||||||
self.query_hash = gen_query_hash(self.query)
|
|
||||||
self.query_result_id = query_result_id
|
|
||||||
|
|
||||||
if job_id is None:
|
|
||||||
self.id = str(uuid.uuid1())
|
|
||||||
self.new_job = True
|
|
||||||
self.wait_time = 0
|
|
||||||
self.query_time = 0
|
|
||||||
self.error = None
|
|
||||||
self.updated_at = time.time() # job_dict.get('updated_at', time.time())
|
|
||||||
self.status = self.WAITING # int(job_dict.get('status', self.WAITING))
|
|
||||||
else:
|
|
||||||
self.id = job_id
|
|
||||||
self.new_job = False
|
|
||||||
self.error = error
|
|
||||||
self.wait_time = wait_time
|
|
||||||
self.query_time = query_time
|
|
||||||
self.updated_at = updated_at
|
|
||||||
self.status = status
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {
|
|
||||||
'query': self.query,
|
|
||||||
'priority': self.priority,
|
|
||||||
'id': self.id,
|
|
||||||
'wait_time': self.wait_time,
|
|
||||||
'query_time': self.query_time,
|
|
||||||
'updated_at': self.updated_at,
|
|
||||||
'status': self.status,
|
|
||||||
'error': self.error,
|
|
||||||
'query_result_id': self.query_result_id
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _redis_key(job_id):
|
|
||||||
return 'job:%s' % job_id
|
|
||||||
|
|
||||||
def save(self, pipe=None):
|
|
||||||
if not pipe:
|
|
||||||
pipe = self.data_manager.redis_connection.pipeline()
|
|
||||||
|
|
||||||
if self.new_job:
|
|
||||||
pipe.set('query_hash_job:%s' % self.query_hash, self.id)
|
|
||||||
|
|
||||||
if self.is_finished():
|
|
||||||
pipe.delete('query_hash_job:%s' % self.query_hash)
|
|
||||||
|
|
||||||
pipe.sadd('jobs_set', self.id)
|
|
||||||
pipe.hmset(self._redis_key(self.id), self.to_dict())
|
|
||||||
pipe.publish(self._redis_key(self.id), json.dumps(self.to_dict()))
|
|
||||||
pipe.execute()
|
|
||||||
|
|
||||||
def processing(self):
|
|
||||||
self.status = self.PROCESSING
|
|
||||||
self.wait_time = time.time() - self.updated_at
|
|
||||||
self.updated_at = time.time()
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
def is_finished(self):
|
|
||||||
return self.status in (self.FAILED, self.DONE)
|
|
||||||
|
|
||||||
def done(self, query_result_id, error):
|
|
||||||
if error:
|
|
||||||
self.status = self.FAILED
|
|
||||||
else:
|
|
||||||
self.status = self.DONE
|
|
||||||
|
|
||||||
self.query_result_id = query_result_id
|
|
||||||
self.error = error
|
|
||||||
self.query_time = time.time() - self.updated_at
|
|
||||||
self.updated_at = time.time()
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "<Job:%s,priority:%d,status:%d>" % (self.id, self.priority, self.status)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _load(cls, data_manager, job_id):
|
|
||||||
return data_manager.redis_connection.hgetall(cls._redis_key(job_id))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def load(cls, data_manager, job_id):
|
|
||||||
job_dict = cls._load(data_manager, job_id)
|
|
||||||
job = None
|
|
||||||
if job_dict:
|
|
||||||
job = Job(data_manager, job_id=job_dict['id'], query=job_dict['query'].decode('utf-8'),
|
|
||||||
priority=int(job_dict['priority']), updated_at=float(job_dict['updated_at']),
|
|
||||||
status=int(job_dict['status']), wait_time=float(job_dict['wait_time']),
|
|
||||||
query_time=float(job_dict['query_time']), error=job_dict['error'],
|
|
||||||
query_result_id=job_dict['query_result_id'])
|
|
||||||
|
|
||||||
return job
|
|
||||||
|
|
||||||
|
|
||||||
class Worker(threading.Thread):
|
|
||||||
def __init__(self, manager, query_runner, sleep_time=0.1):
|
|
||||||
self.manager = manager
|
|
||||||
self.continue_working = True
|
|
||||||
self.query_runner = query_runner
|
|
||||||
self.sleep_time = sleep_time
|
|
||||||
|
|
||||||
super(Worker, self).__init__(name="Worker-%s" % uuid.uuid1())
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
logging.info("[%s] started.", self.name)
|
|
||||||
while self.continue_working:
|
|
||||||
job_id = self.manager.queue.pop()
|
|
||||||
if job_id:
|
|
||||||
logging.info("[%s] Processing %s", self.name, job_id)
|
|
||||||
self._process(job_id)
|
|
||||||
logging.info("[%s] Finished Processing %s", self.name, job_id)
|
|
||||||
else:
|
|
||||||
time.sleep(self.sleep_time)
|
|
||||||
|
|
||||||
def _process(self, job_id):
|
|
||||||
job = Job.load(self.manager, job_id)
|
|
||||||
if job.is_finished():
|
|
||||||
logging.warning("[%s][%s] tried to process finished job.", self.name, job)
|
|
||||||
return
|
|
||||||
|
|
||||||
job.processing()
|
|
||||||
|
|
||||||
logging.info("[%s][%s] running query...", self.name, job.id)
|
|
||||||
start_time = time.time()
|
|
||||||
data, error = self.query_runner(job.query)
|
|
||||||
run_time = time.time() - start_time
|
|
||||||
logging.info("[%s][%s] query finished... data length=%s, error=%s",
|
|
||||||
self.name, job.id, data and len(data), error)
|
|
||||||
|
|
||||||
# TODO: it is possible that storing the data will fail, and we will need to retry
|
|
||||||
# while we already marked the job as done
|
|
||||||
query_result_id = None
|
|
||||||
if not error:
|
|
||||||
query_result_id = self.manager.store_query_result(job.query, data, run_time,
|
|
||||||
datetime.datetime.utcnow())
|
|
||||||
|
|
||||||
job.done(query_result_id, error)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
psycopg2==2.5.1
|
|
||||||
redis==2.7.5
|
|
||||||
tornado==3.0.2
|
|
||||||
sqlparse==0.1.8
|
|
||||||
Django==1.5.4
|
|
||||||
django-db-pool==0.0.10
|
|
||||||
qr==0.6.0
|
|
||||||
python-dateutil==2.1
|
|
||||||
@@ -1,328 +0,0 @@
|
|||||||
"""
|
|
||||||
Tornado based API implementation for re:dash.
|
|
||||||
|
|
||||||
Also at the moment the Tornado server is used to serve the static assets (and the Angular.js app),
|
|
||||||
but this is only due to configuration issues and temporary.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python server.py [--port=8888] [--debug] [--static=..]
|
|
||||||
|
|
||||||
port - port to listen to
|
|
||||||
debug - enable debug mode (extensive logging, restart on code change)
|
|
||||||
static - static assets path
|
|
||||||
|
|
||||||
If static option isn't specified it will be taken from settings.py.
|
|
||||||
"""
|
|
||||||
import csv
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import numbers
|
|
||||||
import os
|
|
||||||
import urlparse
|
|
||||||
import logging
|
|
||||||
import cStringIO
|
|
||||||
import datetime
|
|
||||||
import dateutil.parser
|
|
||||||
import redis
|
|
||||||
import sqlparse
|
|
||||||
import tornado.ioloop
|
|
||||||
import tornado.web
|
|
||||||
import tornado.auth
|
|
||||||
import tornado.options
|
|
||||||
import settings
|
|
||||||
from data import utils
|
|
||||||
import data
|
|
||||||
|
|
||||||
|
|
||||||
class BaseHandler(tornado.web.RequestHandler):
|
|
||||||
def initialize(self):
|
|
||||||
self.data_manager = self.application.settings.get('data_manager', None)
|
|
||||||
self.redis_connection = self.application.settings['redis_connection']
|
|
||||||
|
|
||||||
def get_current_user(self):
|
|
||||||
user = self.get_secure_cookie("user")
|
|
||||||
return user
|
|
||||||
|
|
||||||
@tornado.web.authenticated
|
|
||||||
def prepare(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def write_json(self, response, encode=True):
|
|
||||||
if encode:
|
|
||||||
response = json.dumps(response, cls=utils.JSONEncoder)
|
|
||||||
self.set_header("Content-Type", "application/json; charset=UTF-8")
|
|
||||||
self.write(response)
|
|
||||||
|
|
||||||
|
|
||||||
class PingHandler(tornado.web.RequestHandler):
|
|
||||||
def get(self):
|
|
||||||
self.write("PONG")
|
|
||||||
|
|
||||||
|
|
||||||
class GoogleLoginHandler(tornado.web.RequestHandler,
|
|
||||||
tornado.auth.GoogleMixin):
|
|
||||||
@tornado.web.asynchronous
|
|
||||||
@tornado.gen.coroutine
|
|
||||||
def get(self):
|
|
||||||
if self.get_argument("openid.mode", None):
|
|
||||||
user = yield self.get_authenticated_user()
|
|
||||||
|
|
||||||
if user['email'] in settings.ALLOWED_USERS or user['email'].endswith("@%s" % settings.GOOGLE_APPS_DOMAIN):
|
|
||||||
logging.info("Authenticated: %s", user['email'])
|
|
||||||
self.set_secure_cookie("user", user['email'])
|
|
||||||
self.redirect("/")
|
|
||||||
else:
|
|
||||||
logging.error("Failed logging in with: %s", user)
|
|
||||||
self.authenticate_redirect()
|
|
||||||
else:
|
|
||||||
self.authenticate_redirect()
|
|
||||||
|
|
||||||
|
|
||||||
class MainHandler(BaseHandler):
|
|
||||||
def get(self, *args):
|
|
||||||
email_md5 = hashlib.md5(self.current_user.lower()).hexdigest()
|
|
||||||
gravatar_url = "https://www.gravatar.com/avatar/%s?s=40" % email_md5
|
|
||||||
|
|
||||||
user = {
|
|
||||||
'gravatar_url': gravatar_url,
|
|
||||||
'is_admin': self.current_user in settings.ADMINS,
|
|
||||||
'name': self.current_user
|
|
||||||
}
|
|
||||||
|
|
||||||
self.render("index.html", user=json.dumps(user))
|
|
||||||
|
|
||||||
|
|
||||||
class QueryFormatHandler(BaseHandler):
|
|
||||||
def post(self):
|
|
||||||
arguments = json.loads(self.request.body)
|
|
||||||
query = arguments.get("query", "")
|
|
||||||
|
|
||||||
self.write(sqlparse.format(query, reindent=True, keyword_case='upper'))
|
|
||||||
|
|
||||||
|
|
||||||
class StatusHandler(BaseHandler):
|
|
||||||
def get(self):
|
|
||||||
status = {}
|
|
||||||
info = self.redis_connection.info()
|
|
||||||
status['redis_used_memory'] = info['used_memory_human']
|
|
||||||
status['queries_in_queue'] = self.redis_connection.zcard('jobs')
|
|
||||||
status['queries_count'] = data.models.Query.objects.count()
|
|
||||||
status['query_results_count'] = data.models.QueryResult.objects.count()
|
|
||||||
status['dashboards_count'] = data.models.Dashboard.objects.count()
|
|
||||||
status['widgets_count'] = data.models.Widget.objects.count()
|
|
||||||
|
|
||||||
self.write_json(status)
|
|
||||||
|
|
||||||
|
|
||||||
class WidgetsHandler(BaseHandler):
|
|
||||||
def post(self, widget_id=None):
|
|
||||||
widget_properties = json.loads(self.request.body)
|
|
||||||
widget_properties['options'] = json.dumps(widget_properties['options'])
|
|
||||||
widget = data.models.Widget(**widget_properties)
|
|
||||||
widget.save()
|
|
||||||
|
|
||||||
layout = json.loads(widget.dashboard.layout)
|
|
||||||
new_row = True
|
|
||||||
|
|
||||||
if len(layout) == 0 or widget.width == 2:
|
|
||||||
layout.append([widget.id])
|
|
||||||
elif len(layout[-1]) == 1:
|
|
||||||
neighbour_widget = data.models.Widget.objects.get(pk=layout[-1][0])
|
|
||||||
if neighbour_widget.width == 1:
|
|
||||||
layout[-1].append(widget.id)
|
|
||||||
new_row = False
|
|
||||||
else:
|
|
||||||
layout.append([widget.id])
|
|
||||||
else:
|
|
||||||
layout.append([widget.id])
|
|
||||||
|
|
||||||
widget.dashboard.layout = json.dumps(layout)
|
|
||||||
widget.dashboard.save()
|
|
||||||
|
|
||||||
self.write_json({'widget': widget.to_dict(), 'layout': layout, 'new_row': new_row})
|
|
||||||
|
|
||||||
def delete(self, widget_id):
|
|
||||||
widget_id = int(widget_id)
|
|
||||||
widget = data.models.Widget.objects.get(pk=widget_id)
|
|
||||||
# TODO: reposition existing ones
|
|
||||||
layout = json.loads(widget.dashboard.layout)
|
|
||||||
layout = map(lambda row: filter(lambda w: w != widget_id, row), layout)
|
|
||||||
layout = filter(lambda row: len(row) > 0, layout)
|
|
||||||
widget.dashboard.layout = json.dumps(layout)
|
|
||||||
widget.dashboard.save()
|
|
||||||
|
|
||||||
widget.delete()
|
|
||||||
|
|
||||||
|
|
||||||
class DashboardHandler(BaseHandler):
|
|
||||||
def get(self, dashboard_slug=None):
|
|
||||||
if dashboard_slug:
|
|
||||||
dashboard = data.models.Dashboard.objects.prefetch_related('widgets__query__latest_query_data').get(slug=dashboard_slug)
|
|
||||||
self.write_json(dashboard.to_dict(with_widgets=True))
|
|
||||||
else:
|
|
||||||
dashboards = [d.to_dict() for d in
|
|
||||||
data.models.Dashboard.objects.filter(is_archived=False)]
|
|
||||||
self.write_json(dashboards)
|
|
||||||
|
|
||||||
def post(self, dashboard_id):
|
|
||||||
if dashboard_id:
|
|
||||||
dashboard_properties = json.loads(self.request.body)
|
|
||||||
dashboard = data.models.Dashboard.objects.get(pk=dashboard_id)
|
|
||||||
dashboard.layout = dashboard_properties['layout']
|
|
||||||
dashboard.name = dashboard_properties['name']
|
|
||||||
dashboard.save()
|
|
||||||
|
|
||||||
self.write_json(dashboard.to_dict(with_widgets=True))
|
|
||||||
else:
|
|
||||||
dashboard_properties = json.loads(self.request.body)
|
|
||||||
dashboard = data.models.Dashboard(name=dashboard_properties['name'],
|
|
||||||
user=self.current_user,
|
|
||||||
layout='[]')
|
|
||||||
dashboard.save()
|
|
||||||
self.write_json(dashboard.to_dict())
|
|
||||||
|
|
||||||
def delete(self, dashboard_slug):
|
|
||||||
dashboard = data.models.Dashboard.objects.get(slug=dashboard_slug)
|
|
||||||
dashboard.is_archived = True
|
|
||||||
dashboard.save()
|
|
||||||
|
|
||||||
|
|
||||||
class QueriesHandler(BaseHandler):
|
|
||||||
def post(self, id=None):
|
|
||||||
query_def = json.loads(self.request.body)
|
|
||||||
if 'created_at' in query_def:
|
|
||||||
query_def['created_at'] = dateutil.parser.parse(query_def['created_at'])
|
|
||||||
|
|
||||||
query_def.pop('latest_query_data', None)
|
|
||||||
|
|
||||||
if id:
|
|
||||||
query = data.models.Query(**query_def)
|
|
||||||
fields = query_def.keys()
|
|
||||||
fields.remove('id')
|
|
||||||
query.save(update_fields=fields)
|
|
||||||
else:
|
|
||||||
query_def['user'] = self.current_user
|
|
||||||
query = data.models.Query(**query_def)
|
|
||||||
query.save()
|
|
||||||
|
|
||||||
self.write_json(query.to_dict(with_result=False))
|
|
||||||
|
|
||||||
def get(self, id=None):
|
|
||||||
if id:
|
|
||||||
q = data.models.Query.objects.get(pk=id)
|
|
||||||
if q:
|
|
||||||
self.write_json(q.to_dict())
|
|
||||||
else:
|
|
||||||
self.send_error(404)
|
|
||||||
else:
|
|
||||||
self.write_json([q.to_dict(with_result=False) for q in data.models.Query.objects.all()])
|
|
||||||
|
|
||||||
|
|
||||||
class QueryResultsHandler(BaseHandler):
|
|
||||||
def get(self, query_result_id):
|
|
||||||
query_result = self.data_manager.get_query_result_by_id(query_result_id)
|
|
||||||
if query_result:
|
|
||||||
self.write_json({'query_result': query_result.to_dict(parse_data=True)})
|
|
||||||
else:
|
|
||||||
self.send_error(404)
|
|
||||||
|
|
||||||
def post(self, _):
|
|
||||||
params = json.loads(self.request.body)
|
|
||||||
|
|
||||||
if params['ttl'] == 0:
|
|
||||||
query_result = None
|
|
||||||
else:
|
|
||||||
query_result = self.data_manager.get_query_result(params['query'], int(params['ttl']))
|
|
||||||
|
|
||||||
if query_result:
|
|
||||||
self.write_json({'query_result': query_result.to_dict(parse_data=True)})
|
|
||||||
else:
|
|
||||||
job = self.data_manager.add_job(params['query'], data.Job.HIGH_PRIORITY)
|
|
||||||
self.write({'job': job.to_dict()})
|
|
||||||
|
|
||||||
|
|
||||||
class JobsHandler(BaseHandler):
|
|
||||||
def get(self, job_id=None):
|
|
||||||
if job_id:
|
|
||||||
# TODO: if finished, include the query result
|
|
||||||
job = data.Job.load(self.data_manager, job_id)
|
|
||||||
self.write({'job': job.to_dict()})
|
|
||||||
else:
|
|
||||||
raise NotImplemented
|
|
||||||
|
|
||||||
def delete(self, job_id):
|
|
||||||
raise NotImplemented
|
|
||||||
|
|
||||||
|
|
||||||
class CsvQueryResultsHandler(BaseHandler):
|
|
||||||
def get(self, query_result_id):
|
|
||||||
query_result = self.data_manager.get_query_result_by_id(query_result_id)
|
|
||||||
if query_result:
|
|
||||||
self.set_header("Content-Type", "text/csv; charset=UTF-8")
|
|
||||||
s = cStringIO.StringIO()
|
|
||||||
|
|
||||||
query_data = json.loads(query_result.data)
|
|
||||||
writer = csv.DictWriter(s, fieldnames=[col['name'] for col in query_data['columns']])
|
|
||||||
writer.writer = utils.UnicodeWriter(s)
|
|
||||||
writer.writeheader()
|
|
||||||
for row in query_data['rows']:
|
|
||||||
for k, v in row.iteritems():
|
|
||||||
if isinstance(v, numbers.Number) and (v > 1000 * 1000 * 1000 * 100):
|
|
||||||
row[k] = datetime.datetime.fromtimestamp(v/1000.0)
|
|
||||||
|
|
||||||
writer.writerow(row)
|
|
||||||
|
|
||||||
self.write(s.getvalue())
|
|
||||||
else:
|
|
||||||
self.send_error(404)
|
|
||||||
|
|
||||||
|
|
||||||
def get_application(static_path, is_debug, redis_connection, data_manager):
|
|
||||||
return tornado.web.Application([(r"/", MainHandler),
|
|
||||||
(r"/ping", PingHandler),
|
|
||||||
(r"/api/queries/format", QueryFormatHandler),
|
|
||||||
(r"/api/queries(?:/([0-9]*))?", QueriesHandler),
|
|
||||||
(r"/api/query_results(?:/([0-9]*))?", QueryResultsHandler),
|
|
||||||
(r"/api/query_results/(.*?).csv", CsvQueryResultsHandler),
|
|
||||||
(r"/api/jobs/(.*)", JobsHandler),
|
|
||||||
(r"/api/widgets(?:/([0-9]*))?", WidgetsHandler),
|
|
||||||
(r"/api/dashboards(?:/(.*))?", DashboardHandler),
|
|
||||||
(r"/admin/(.*)", MainHandler),
|
|
||||||
(r"/dashboard/(.*)", MainHandler),
|
|
||||||
(r"/queries(.*)", MainHandler),
|
|
||||||
(r"/login", GoogleLoginHandler),
|
|
||||||
(r"/status.json", StatusHandler),
|
|
||||||
(r"/(.*)", tornado.web.StaticFileHandler,
|
|
||||||
{"path": static_path})],
|
|
||||||
template_path=static_path,
|
|
||||||
static_path=static_path,
|
|
||||||
debug=is_debug,
|
|
||||||
login_url="/login",
|
|
||||||
cookie_secret=settings.COOKIE_SECRET,
|
|
||||||
redis_connection=redis_connection,
|
|
||||||
data_manager=data_manager)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
tornado.options.define("port", default=8888, type=int)
|
|
||||||
tornado.options.define("debug", default=False, type=bool)
|
|
||||||
tornado.options.define("static", default=settings.STATIC_ASSETS_PATH, type=str)
|
|
||||||
|
|
||||||
tornado.options.parse_command_line()
|
|
||||||
|
|
||||||
root_path = os.path.dirname(__file__)
|
|
||||||
static_path = os.path.abspath(os.path.join(root_path, tornado.options.options.static))
|
|
||||||
|
|
||||||
url = urlparse.urlparse(settings.REDIS_URL)
|
|
||||||
redis_connection = redis.StrictRedis(host=url.hostname, port=url.port, db=0, password=url.password)
|
|
||||||
data_manager = data.Manager(redis_connection, settings.INTERNAL_DB_CONNECTION_STRING,
|
|
||||||
settings.MAX_CONNECTIONS)
|
|
||||||
|
|
||||||
logging.info("re:dash web server stating on port: %d...", tornado.options.options.port)
|
|
||||||
logging.info("UI assets path: %s...", static_path)
|
|
||||||
|
|
||||||
application = get_application(static_path, tornado.options.options.debug,
|
|
||||||
redis_connection, data_manager)
|
|
||||||
|
|
||||||
application.listen(tornado.options.options.port)
|
|
||||||
tornado.ioloop.IOLoop.instance().start()
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
"""
|
|
||||||
Example settings module. You should make your own copy as settings.py and enter the real settings.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import django.conf
|
|
||||||
|
|
||||||
REDIS_URL = "redis://localhost:6379"
|
|
||||||
# Connection string for the database that is used to run queries against
|
|
||||||
CONNECTION_STRING = "user= password= host= port=5439 dbname="
|
|
||||||
# Connection string for the operational databases (where we store the queries, results, etc)
|
|
||||||
INTERNAL_DB_CONNECTION_STRING = "dbname=postgres"
|
|
||||||
# Google Apps domain to allow access from; any user with email in this Google Apps will be allowed
|
|
||||||
# access
|
|
||||||
GOOGLE_APPS_DOMAIN = ""
|
|
||||||
# Email addresses of specific users not from the above set Google Apps Domain, that you want to
|
|
||||||
# allow access to re:dash
|
|
||||||
ALLOWED_USERS = []
|
|
||||||
# Email addresses of admin users
|
|
||||||
ADMINS = []
|
|
||||||
STATIC_ASSETS_PATH = "../rd_ui/dist/"
|
|
||||||
WORKERS_COUNT = 2
|
|
||||||
MAX_CONNECTIONS = 3
|
|
||||||
COOKIE_SECRET = "c292a0a3aa32397cdb050e233733900f"
|
|
||||||
|
|
||||||
# Configuration of the operational database for the Django models
|
|
||||||
django.conf.settings.configure(DATABASES = { 'default': {
|
|
||||||
'ENGINE': 'dbpool.db.backends.postgresql_psycopg2',
|
|
||||||
'OPTIONS': {'MAX_CONNS': 10, 'MIN_CONNS': 1},
|
|
||||||
'NAME': 'postgres',
|
|
||||||
'USER': '',
|
|
||||||
'PASSWORD': '',
|
|
||||||
'HOST': '',
|
|
||||||
'PORT': '',
|
|
||||||
},}, TIME_ZONE = 'UTC')
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- '0.8'
|
|
||||||
- '0.10'
|
- '0.10'
|
||||||
before_script:
|
before_script:
|
||||||
- 'npm install -g bower grunt-cli'
|
- 'npm install -g bower grunt-cli'
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
// Generated on 2013-08-25 using generator-angular 0.4.0
|
// Generated on 2014-07-30 using generator-angular 0.9.2
|
||||||
'use strict';
|
'use strict';
|
||||||
var LIVERELOAD_PORT = 35729;
|
|
||||||
var lrSnippet = require('connect-livereload')({ port: LIVERELOAD_PORT });
|
|
||||||
var mountFolder = function (connect, dir) {
|
|
||||||
return connect.static(require('path').resolve(dir));
|
|
||||||
};
|
|
||||||
|
|
||||||
// # Globbing
|
// # Globbing
|
||||||
// for performance reasons we're only matching one level down:
|
// for performance reasons we're only matching one level down:
|
||||||
@@ -13,48 +8,148 @@ var mountFolder = function (connect, dir) {
|
|||||||
// 'test/spec/**/*.js'
|
// 'test/spec/**/*.js'
|
||||||
|
|
||||||
module.exports = function (grunt) {
|
module.exports = function (grunt) {
|
||||||
|
|
||||||
|
// Load grunt tasks automatically
|
||||||
require('load-grunt-tasks')(grunt);
|
require('load-grunt-tasks')(grunt);
|
||||||
|
|
||||||
|
// Time how long tasks take. Can help when optimizing build times
|
||||||
require('time-grunt')(grunt);
|
require('time-grunt')(grunt);
|
||||||
|
|
||||||
// configurable paths
|
// Configurable paths for the application
|
||||||
var yeomanConfig = {
|
var appConfig = {
|
||||||
app: 'app',
|
app: require('./bower.json').appPath || 'app',
|
||||||
dist: 'dist'
|
dist: 'dist'
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
// Define the configuration for all the tasks
|
||||||
yeomanConfig.app = require('./bower.json').appPath || yeomanConfig.app;
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
grunt.initConfig({
|
grunt.initConfig({
|
||||||
yeoman: yeomanConfig,
|
|
||||||
|
// Project settings
|
||||||
|
yeoman: appConfig,
|
||||||
|
|
||||||
|
// Watches files for changes and runs tasks based on the changed files
|
||||||
watch: {
|
watch: {
|
||||||
coffee: {
|
bower: {
|
||||||
files: ['<%= yeoman.app %>/scripts/{,*/}*.coffee'],
|
files: ['bower.json'],
|
||||||
tasks: ['coffee:dist']
|
tasks: ['wiredep']
|
||||||
},
|
},
|
||||||
coffeeTest: {
|
js: {
|
||||||
files: ['test/spec/{,*/}*.coffee'],
|
files: ['<%= yeoman.app %>/scripts/{,*/}*.js'],
|
||||||
tasks: ['coffee:test']
|
tasks: ['newer:jshint:all'],
|
||||||
|
options: {
|
||||||
|
livereload: '<%= connect.options.livereload %>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
jsTest: {
|
||||||
|
files: ['test/spec/{,*/}*.js'],
|
||||||
|
tasks: ['newer:jshint:test', 'karma']
|
||||||
},
|
},
|
||||||
styles: {
|
styles: {
|
||||||
files: ['<%= yeoman.app %>/styles/{,*/}*.css'],
|
files: ['<%= yeoman.app %>/styles/{,*/}*.css'],
|
||||||
tasks: ['copy:styles', 'autoprefixer']
|
tasks: ['newer:copy:styles', 'autoprefixer']
|
||||||
|
},
|
||||||
|
gruntfile: {
|
||||||
|
files: ['Gruntfile.js']
|
||||||
},
|
},
|
||||||
livereload: {
|
livereload: {
|
||||||
options: {
|
options: {
|
||||||
livereload: LIVERELOAD_PORT
|
livereload: '<%= connect.options.livereload %>'
|
||||||
},
|
},
|
||||||
files: [
|
files: [
|
||||||
'<%= yeoman.app %>/{,*/}*.html',
|
'<%= yeoman.app %>/{,*/}*.html',
|
||||||
'.tmp/styles/{,*/}*.css',
|
'.tmp/styles/{,*/}*.css',
|
||||||
'{.tmp,<%= yeoman.app %>}/scripts/{,*/}*.js',
|
|
||||||
'<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
|
'<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// The actual grunt server settings
|
||||||
|
connect: {
|
||||||
|
options: {
|
||||||
|
port: 9000,
|
||||||
|
// Change this to '0.0.0.0' to access the server from outside.
|
||||||
|
hostname: 'localhost',
|
||||||
|
livereload: 35729
|
||||||
|
},
|
||||||
|
livereload: {
|
||||||
|
options: {
|
||||||
|
open: true,
|
||||||
|
middleware: function (connect) {
|
||||||
|
return [
|
||||||
|
connect.static('.tmp'),
|
||||||
|
connect().use(
|
||||||
|
'/bower_components',
|
||||||
|
connect.static('./bower_components')
|
||||||
|
),
|
||||||
|
connect.static(appConfig.app)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
options: {
|
||||||
|
port: 9001,
|
||||||
|
middleware: function (connect) {
|
||||||
|
return [
|
||||||
|
connect.static('.tmp'),
|
||||||
|
connect.static('test'),
|
||||||
|
connect().use(
|
||||||
|
'/bower_components',
|
||||||
|
connect.static('./bower_components')
|
||||||
|
),
|
||||||
|
connect.static(appConfig.app)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dist: {
|
||||||
|
options: {
|
||||||
|
open: true,
|
||||||
|
base: '<%= yeoman.dist %>'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Make sure code styles are up to par and there are no obvious mistakes
|
||||||
|
jshint: {
|
||||||
|
options: {
|
||||||
|
jshintrc: '.jshintrc',
|
||||||
|
reporter: require('jshint-stylish')
|
||||||
|
},
|
||||||
|
all: {
|
||||||
|
src: [
|
||||||
|
'Gruntfile.js',
|
||||||
|
'<%= yeoman.app %>/scripts/{,*/}*.js'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
options: {
|
||||||
|
jshintrc: 'test/.jshintrc'
|
||||||
|
},
|
||||||
|
src: ['test/spec/{,*/}*.js']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Empties folders to start fresh
|
||||||
|
clean: {
|
||||||
|
dist: {
|
||||||
|
files: [{
|
||||||
|
dot: true,
|
||||||
|
src: [
|
||||||
|
'.tmp',
|
||||||
|
'<%= yeoman.dist %>/{,*/}*',
|
||||||
|
'!<%= yeoman.dist %>/.git*'
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
server: '.tmp'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add vendor prefixed styles
|
||||||
autoprefixer: {
|
autoprefixer: {
|
||||||
options: ['last 1 version'],
|
options: {
|
||||||
|
browsers: ['last 1 version']
|
||||||
|
},
|
||||||
dist: {
|
dist: {
|
||||||
files: [{
|
files: [{
|
||||||
expand: true,
|
expand: true,
|
||||||
@@ -64,134 +159,94 @@ module.exports = function (grunt) {
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
connect: {
|
|
||||||
|
// Automatically inject Bower components into the app
|
||||||
|
wiredep: {
|
||||||
options: {
|
options: {
|
||||||
port: 9000,
|
|
||||||
// Change this to '0.0.0.0' to access the server from outside.
|
|
||||||
hostname: 'localhost'
|
|
||||||
},
|
},
|
||||||
livereload: {
|
app: {
|
||||||
options: {
|
src: ['<%= yeoman.app %>/index.html'],
|
||||||
middleware: function (connect) {
|
ignorePath: /\.\.\//
|
||||||
return [
|
|
||||||
lrSnippet,
|
|
||||||
mountFolder(connect, '.tmp'),
|
|
||||||
mountFolder(connect, yeomanConfig.app)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
test: {
|
|
||||||
options: {
|
|
||||||
middleware: function (connect) {
|
|
||||||
return [
|
|
||||||
mountFolder(connect, '.tmp'),
|
|
||||||
mountFolder(connect, 'test')
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dist: {
|
|
||||||
options: {
|
|
||||||
middleware: function (connect) {
|
|
||||||
return [
|
|
||||||
mountFolder(connect, yeomanConfig.dist)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
open: {
|
|
||||||
server: {
|
// Renames files for browser caching purposes
|
||||||
url: 'http://localhost:<%= connect.options.port %>'
|
filerev: {
|
||||||
}
|
dist: {
|
||||||
},
|
src: [
|
||||||
clean: {
|
'<%= yeoman.dist %>/scripts/{,*/}*.js',
|
||||||
dist: {
|
'<%= yeoman.dist %>/styles/{,*/}*.css',
|
||||||
files: [{
|
'<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}',
|
||||||
dot: true,
|
'<%= yeoman.dist %>/styles/fonts/*'
|
||||||
src: [
|
]
|
||||||
'.tmp',
|
|
||||||
'<%= yeoman.dist %>/*',
|
|
||||||
'!<%= yeoman.dist %>/.git*'
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
server: '.tmp'
|
|
||||||
},
|
|
||||||
jshint: {
|
|
||||||
options: {
|
|
||||||
jshintrc: '.jshintrc'
|
|
||||||
},
|
|
||||||
all: [
|
|
||||||
'Gruntfile.js',
|
|
||||||
'<%= yeoman.app %>/scripts/{,*/}*.js'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
coffee: {
|
|
||||||
options: {
|
|
||||||
sourceMap: true,
|
|
||||||
sourceRoot: ''
|
|
||||||
},
|
|
||||||
dist: {
|
|
||||||
files: [{
|
|
||||||
expand: true,
|
|
||||||
cwd: '<%= yeoman.app %>/scripts',
|
|
||||||
src: '{,*/}*.coffee',
|
|
||||||
dest: '.tmp/scripts',
|
|
||||||
ext: '.js'
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
test: {
|
|
||||||
files: [{
|
|
||||||
expand: true,
|
|
||||||
cwd: 'test/spec',
|
|
||||||
src: '{,*/}*.coffee',
|
|
||||||
dest: '.tmp/spec',
|
|
||||||
ext: '.js'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// not used since Uglify task does concat,
|
|
||||||
// but still available if needed
|
|
||||||
/*concat: {
|
|
||||||
dist: {}
|
|
||||||
},*/
|
|
||||||
rev: {
|
|
||||||
dist: {
|
|
||||||
files: {
|
|
||||||
src: [
|
|
||||||
'<%= yeoman.dist %>/scripts/{,*/}*.js',
|
|
||||||
'<%= yeoman.dist %>/styles/{,*/}*.css',
|
|
||||||
'<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}',
|
|
||||||
'<%= yeoman.dist %>/styles/fonts/*'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Reads HTML for usemin blocks to enable smart builds that automatically
|
||||||
|
// concat, minify and revision files. Creates configurations in memory so
|
||||||
|
// additional tasks can operate on them
|
||||||
useminPrepare: {
|
useminPrepare: {
|
||||||
html: '<%= yeoman.app %>/index.html',
|
html: ['<%= yeoman.app %>/index.html', '<%= yeoman.app %>/login.html'],
|
||||||
options: {
|
options: {
|
||||||
dest: '<%= yeoman.dist %>'
|
dest: '<%= yeoman.dist %>',
|
||||||
|
flow: {
|
||||||
|
html: {
|
||||||
|
steps: {
|
||||||
|
js: ['concat', 'uglifyjs'],
|
||||||
|
css: ['cssmin']
|
||||||
|
},
|
||||||
|
post: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Performs rewrites based on filerev and the useminPrepare configuration
|
||||||
usemin: {
|
usemin: {
|
||||||
html: ['<%= yeoman.dist %>/{,*/}*.html'],
|
html: ['<%= yeoman.dist %>/{,*/}*.html'],
|
||||||
css: ['<%= yeoman.dist %>/styles/{,*/}*.css'],
|
css: ['<%= yeoman.dist %>/styles/{,*/}*.css'],
|
||||||
options: {
|
options: {
|
||||||
dirs: ['<%= yeoman.dist %>']
|
assetsDirs: ['<%= yeoman.dist %>','<%= yeoman.dist %>/images']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// The following *-min tasks will produce minified files in the dist folder
|
||||||
|
// By default, your `index.html`'s <!-- Usemin block --> will take care of
|
||||||
|
// minification. These next options are pre-configured if you do not wish
|
||||||
|
// to use the Usemin blocks.
|
||||||
|
// cssmin: {
|
||||||
|
// dist: {
|
||||||
|
// files: {
|
||||||
|
// '<%= yeoman.dist %>/styles/main.css': [
|
||||||
|
// '.tmp/styles/{,*/}*.css'
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// uglify: {
|
||||||
|
// dist: {
|
||||||
|
// files: {
|
||||||
|
// '<%= yeoman.dist %>/scripts/scripts.js': [
|
||||||
|
// '<%= yeoman.dist %>/scripts/scripts.js'
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// concat: {
|
||||||
|
// dist: {}
|
||||||
|
// },
|
||||||
|
|
||||||
imagemin: {
|
imagemin: {
|
||||||
dist: {
|
dist: {
|
||||||
files: [{
|
files: [{
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: '<%= yeoman.app %>/images',
|
cwd: '<%= yeoman.app %>/images',
|
||||||
src: '{,*/}*.{png,jpg,jpeg}',
|
src: '{,*/}*.{png,jpg,jpeg,gif}',
|
||||||
dest: '<%= yeoman.dist %>/images'
|
dest: '<%= yeoman.dist %>/images'
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
svgmin: {
|
svgmin: {
|
||||||
dist: {
|
dist: {
|
||||||
files: [{
|
files: [{
|
||||||
@@ -202,41 +257,47 @@ module.exports = function (grunt) {
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cssmin: {
|
|
||||||
// By default, your `index.html` <!-- Usemin Block --> will take care of
|
|
||||||
// minification. This option is pre-configured if you do not wish to use
|
|
||||||
// Usemin blocks.
|
|
||||||
// dist: {
|
|
||||||
// files: {
|
|
||||||
// '<%= yeoman.dist %>/styles/main.css': [
|
|
||||||
// '.tmp/styles/{,*/}*.css',
|
|
||||||
// '<%= yeoman.app %>/styles/{,*/}*.css'
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
},
|
|
||||||
htmlmin: {
|
htmlmin: {
|
||||||
dist: {
|
dist: {
|
||||||
options: {
|
options: {
|
||||||
/*removeCommentsFromCDATA: true,
|
collapseWhitespace: true,
|
||||||
// https://github.com/yeoman/grunt-usemin/issues/44
|
conservativeCollapse: true,
|
||||||
//collapseWhitespace: true,
|
|
||||||
collapseBooleanAttributes: true,
|
collapseBooleanAttributes: true,
|
||||||
removeAttributeQuotes: true,
|
removeCommentsFromCDATA: true,
|
||||||
removeRedundantAttributes: true,
|
removeOptionalTags: true
|
||||||
useShortDoctype: true,
|
|
||||||
removeEmptyAttributes: true,
|
|
||||||
removeOptionalTags: true*/
|
|
||||||
},
|
},
|
||||||
files: [{
|
files: [{
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: '<%= yeoman.app %>',
|
cwd: '<%= yeoman.dist %>',
|
||||||
src: ['*.html', 'views/*.html'],
|
src: ['*.html', 'views/{,*/}*.html'],
|
||||||
dest: '<%= yeoman.dist %>'
|
dest: '<%= yeoman.dist %>'
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Put files not handled in other tasks here
|
|
||||||
|
// ngmin tries to make the code safe for minification automatically by
|
||||||
|
// using the Angular long form for dependency injection. It doesn't work on
|
||||||
|
// things like resolve or inject so those have to be done manually.
|
||||||
|
ngmin: {
|
||||||
|
dist: {
|
||||||
|
files: [{
|
||||||
|
expand: true,
|
||||||
|
cwd: '.tmp/concat/scripts',
|
||||||
|
src: '*.js',
|
||||||
|
dest: '.tmp/concat/scripts'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Replace Google CDN references
|
||||||
|
cdnify: {
|
||||||
|
dist: {
|
||||||
|
html: ['<%= yeoman.dist %>/*.html']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Copies remaining files to places other tasks can use
|
||||||
copy: {
|
copy: {
|
||||||
dist: {
|
dist: {
|
||||||
files: [{
|
files: [{
|
||||||
@@ -247,17 +308,26 @@ module.exports = function (grunt) {
|
|||||||
src: [
|
src: [
|
||||||
'*.{ico,png,txt}',
|
'*.{ico,png,txt}',
|
||||||
'.htaccess',
|
'.htaccess',
|
||||||
'bower_components/**/*',
|
'*.html',
|
||||||
'images/{,*/}*.{gif,webp}',
|
'views/{,*/}*.html',
|
||||||
|
'images/{,*/}*.{webp}',
|
||||||
'fonts/*'
|
'fonts/*'
|
||||||
]
|
]
|
||||||
}, {
|
}, {
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: '.tmp/images',
|
cwd: '.tmp/images',
|
||||||
dest: '<%= yeoman.dist %>/images',
|
dest: '<%= yeoman.dist %>/images',
|
||||||
src: [
|
src: ['generated/*']
|
||||||
'generated/*'
|
}, {
|
||||||
]
|
expand: true,
|
||||||
|
cwd: '<%= yeoman.app %>/bower_components/bootstrap/dist',
|
||||||
|
src: 'fonts/*',
|
||||||
|
dest: '<%= yeoman.dist %>'
|
||||||
|
}, {
|
||||||
|
expand: true,
|
||||||
|
cwd: '<%= yeoman.app %>/bower_components/font-awesome',
|
||||||
|
src: 'fonts/*',
|
||||||
|
dest: '<%= yeoman.dist %>'
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
styles: {
|
styles: {
|
||||||
@@ -267,70 +337,52 @@ module.exports = function (grunt) {
|
|||||||
src: '{,*/}*.css'
|
src: '{,*/}*.css'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Run some tasks in parallel to speed up the build process
|
||||||
concurrent: {
|
concurrent: {
|
||||||
server: [
|
server: [
|
||||||
'coffee:dist',
|
|
||||||
'copy:styles'
|
'copy:styles'
|
||||||
],
|
],
|
||||||
test: [
|
test: [
|
||||||
'coffee',
|
|
||||||
'copy:styles'
|
'copy:styles'
|
||||||
],
|
],
|
||||||
dist: [
|
dist: [
|
||||||
'coffee',
|
|
||||||
'copy:styles',
|
'copy:styles',
|
||||||
'imagemin',
|
'imagemin',
|
||||||
'svgmin',
|
'svgmin'
|
||||||
'htmlmin'
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Test settings
|
||||||
karma: {
|
karma: {
|
||||||
unit: {
|
unit: {
|
||||||
configFile: 'karma.conf.js',
|
configFile: 'test/karma.conf.js',
|
||||||
singleRun: true
|
singleRun: true
|
||||||
}
|
}
|
||||||
},
|
|
||||||
cdnify: {
|
|
||||||
dist: {
|
|
||||||
html: ['<%= yeoman.dist %>/*.html']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ngmin: {
|
|
||||||
dist: {
|
|
||||||
files: [{
|
|
||||||
expand: true,
|
|
||||||
cwd: '<%= yeoman.dist %>/scripts',
|
|
||||||
src: '*.js',
|
|
||||||
dest: '<%= yeoman.dist %>/scripts'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
uglify: {
|
|
||||||
dist: {
|
|
||||||
files: {
|
|
||||||
'<%= yeoman.dist %>/scripts/scripts.js': [
|
|
||||||
'<%= yeoman.dist %>/scripts/scripts.js'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
grunt.registerTask('server', function (target) {
|
|
||||||
|
grunt.registerTask('serve', 'Compile then start a connect web server', function (target) {
|
||||||
if (target === 'dist') {
|
if (target === 'dist') {
|
||||||
return grunt.task.run(['build', 'open', 'connect:dist:keepalive']);
|
return grunt.task.run(['build', 'connect:dist:keepalive']);
|
||||||
}
|
}
|
||||||
|
|
||||||
grunt.task.run([
|
grunt.task.run([
|
||||||
'clean:server',
|
'clean:server',
|
||||||
|
'wiredep',
|
||||||
'concurrent:server',
|
'concurrent:server',
|
||||||
'autoprefixer',
|
'autoprefixer',
|
||||||
'connect:livereload',
|
'connect:livereload',
|
||||||
'open',
|
|
||||||
'watch'
|
'watch'
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) {
|
||||||
|
grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
|
||||||
|
grunt.task.run(['serve:' + target]);
|
||||||
|
});
|
||||||
|
|
||||||
grunt.registerTask('test', [
|
grunt.registerTask('test', [
|
||||||
'clean:server',
|
'clean:server',
|
||||||
'concurrent:test',
|
'concurrent:test',
|
||||||
@@ -341,21 +393,23 @@ module.exports = function (grunt) {
|
|||||||
|
|
||||||
grunt.registerTask('build', [
|
grunt.registerTask('build', [
|
||||||
'clean:dist',
|
'clean:dist',
|
||||||
|
'wiredep',
|
||||||
'useminPrepare',
|
'useminPrepare',
|
||||||
'concurrent:dist',
|
'concurrent:dist',
|
||||||
'autoprefixer',
|
'autoprefixer',
|
||||||
'concat',
|
'concat',
|
||||||
|
'ngmin',
|
||||||
'copy:dist',
|
'copy:dist',
|
||||||
'cdnify',
|
'cdnify',
|
||||||
'ngmin',
|
|
||||||
'cssmin',
|
'cssmin',
|
||||||
'uglify',
|
'uglify',
|
||||||
'rev',
|
'filerev',
|
||||||
'usemin'
|
'usemin',
|
||||||
|
'htmlmin'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
grunt.registerTask('default', [
|
grunt.registerTask('default', [
|
||||||
'jshint',
|
'newer:jshint',
|
||||||
'test',
|
'test',
|
||||||
'build'
|
'build'
|
||||||
]);
|
]);
|
||||||
|
|||||||
BIN
rd_ui/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,228 +0,0 @@
|
|||||||
<?xml version="1.0" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<metadata></metadata>
|
|
||||||
<defs>
|
|
||||||
<font id="glyphicons_halflingsregular" horiz-adv-x="1200" >
|
|
||||||
<font-face units-per-em="1200" ascent="960" descent="-240" />
|
|
||||||
<missing-glyph horiz-adv-x="500" />
|
|
||||||
<glyph />
|
|
||||||
<glyph />
|
|
||||||
<glyph unicode=" " />
|
|
||||||
<glyph unicode="*" d="M1100 500h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200z" />
|
|
||||||
<glyph unicode="+" d="M1100 400h-400v-400h-300v400h-400v300h400v400h300v-400h400v-300z" />
|
|
||||||
<glyph unicode=" " />
|
|
||||||
<glyph unicode=" " horiz-adv-x="652" />
|
|
||||||
<glyph unicode=" " horiz-adv-x="1304" />
|
|
||||||
<glyph unicode=" " horiz-adv-x="652" />
|
|
||||||
<glyph unicode=" " horiz-adv-x="1304" />
|
|
||||||
<glyph unicode=" " horiz-adv-x="434" />
|
|
||||||
<glyph unicode=" " horiz-adv-x="326" />
|
|
||||||
<glyph unicode=" " horiz-adv-x="217" />
|
|
||||||
<glyph unicode=" " horiz-adv-x="217" />
|
|
||||||
<glyph unicode=" " horiz-adv-x="163" />
|
|
||||||
<glyph unicode=" " horiz-adv-x="260" />
|
|
||||||
<glyph unicode=" " horiz-adv-x="72" />
|
|
||||||
<glyph unicode=" " horiz-adv-x="260" />
|
|
||||||
<glyph unicode=" " horiz-adv-x="326" />
|
|
||||||
<glyph unicode="€" d="M800 500h-300q9 -74 33 -132t52.5 -91t62 -54.5t59 -29t46.5 -7.5q29 0 66 13t75 37t63.5 67.5t25.5 96.5h174q-31 -172 -128 -278q-107 -117 -274 -117q-205 0 -324 158q-36 46 -69 131.5t-45 205.5h-217l100 100h113q0 47 5 100h-218l100 100h135q37 167 112 257 q117 141 297 141q242 0 354 -189q60 -103 66 -209h-181q0 55 -25.5 99t-63.5 68t-75 36.5t-67 12.5q-24 0 -52.5 -10t-62.5 -32t-65.5 -67t-50.5 -107h379l-100 -100h-300q-6 -46 -6 -100h406z" />
|
|
||||||
<glyph unicode="−" d="M1100 700h-900v-300h900v300z" />
|
|
||||||
<glyph unicode="☁" d="M178 300h750q120 0 205 86t85 208q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5q0 -80 56.5 -137t135.5 -57z" />
|
|
||||||
<glyph unicode="✉" d="M1200 1100h-1200l600 -603zM300 600l-300 -300v600zM1200 900v-600l-300 300zM800 500l400 -400h-1200l400 400l200 -200z" />
|
|
||||||
<glyph unicode="✏" d="M1101 889l99 92q13 13 13 32.5t-13 33.5l-153 153q-15 13 -33 13t-33 -13l-94 -97zM401 189l614 614l-214 214l-614 -614zM-13 -13l333 112l-223 223z" />
|
|
||||||
<glyph unicode="" horiz-adv-x="500" d="M0 0z" />
|
|
||||||
<glyph unicode="" d="M700 100h300v-100h-800v100h300v550l-500 550h1200l-500 -550v-550z" />
|
|
||||||
<glyph unicode="" d="M1000 934v-521q-64 16 -138 -7q-79 -26 -122.5 -83t-25.5 -111q17 -55 85.5 -75.5t147.5 4.5q70 23 111.5 63.5t41.5 95.5v881q0 10 -7 15.5t-17 2.5l-752 -193q-10 -3 -17 -12.5t-7 -19.5v-689q-64 17 -138 -7q-79 -25 -122.5 -82t-25.5 -112t86 -75.5t147 5.5 q65 21 109 69t44 90v606z" />
|
|
||||||
<glyph unicode="" d="M913 432l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342t142 342t342 142t342 -142t142 -342q0 -142 -78 -261zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233z" />
|
|
||||||
<glyph unicode="" d="M649 949q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-149.5 152.5t-126.5 127.5t-94 124.5t-33.5 117.5q0 64 28 123t73 100.5t104.5 64t119 20.5 t120 -38.5t104.5 -104.5z" />
|
|
||||||
<glyph unicode="" d="M791 522l145 -449l-384 275l-382 -275l146 447l-388 280h479l146 400h2l146 -400h472zM168 71l2 1z" />
|
|
||||||
<glyph unicode="" d="M791 522l145 -449l-384 275l-382 -275l146 447l-388 280h479l146 400h2l146 -400h472zM747 331l-74 229l193 140h-235l-77 211l-78 -211h-239l196 -142l-73 -226l192 140zM168 71l2 1z" />
|
|
||||||
<glyph unicode="" d="M1200 143v-143h-1200v143l400 257v100q-37 0 -68.5 74.5t-31.5 125.5v200q0 124 88 212t212 88t212 -88t88 -212v-200q0 -51 -31.5 -125.5t-68.5 -74.5v-100z" />
|
|
||||||
<glyph unicode="" d="M1200 1100v-1100h-1200v1100h1200zM200 1000h-100v-100h100v100zM900 1000h-600v-400h600v400zM1100 1000h-100v-100h100v100zM200 800h-100v-100h100v100zM1100 800h-100v-100h100v100zM200 600h-100v-100h100v100zM1100 600h-100v-100h100v100zM900 500h-600v-400h600 v400zM200 400h-100v-100h100v100zM1100 400h-100v-100h100v100zM200 200h-100v-100h100v100zM1100 200h-100v-100h100v100z" />
|
|
||||||
<glyph unicode="" d="M500 1050v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5zM1100 1050v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5h400 q21 0 35.5 -14.5t14.5 -35.5zM500 450v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5zM1100 450v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400 q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5z" />
|
|
||||||
<glyph unicode="" d="M300 1050v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM700 1050v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200 q21 0 35.5 -14.5t14.5 -35.5zM1100 1050v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM300 650v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200 q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM700 650v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM1100 650v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM300 250v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM700 250v-200 q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM1100 250v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5 t14.5 -35.5z" />
|
|
||||||
<glyph unicode="" d="M300 1050v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM1200 1050v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h700 q21 0 35.5 -14.5t14.5 -35.5zM300 450v200q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-200q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5zM1200 650v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v200 q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5zM300 250v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM1200 250v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700 q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5z" />
|
|
||||||
<glyph unicode="" d="M448 34l818 820l-212 212l-607 -607l-206 207l-212 -212z" />
|
|
||||||
<glyph unicode="" d="M882 106l-282 282l-282 -282l-212 212l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282z" />
|
|
||||||
<glyph unicode="" d="M913 432l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342t142 342t342 142t342 -142t142 -342q0 -142 -78 -261zM507 363q137 0 233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5t-234 -97t-97 -233 t97 -233t234 -97zM600 800h100v-200h-100v-100h-200v100h-100v200h100v100h200v-100z" />
|
|
||||||
<glyph unicode="" d="M913 432l300 -299q7 -7 7 -18t-7 -18l-109 -109q-8 -8 -18 -8t-18 8l-300 299q-120 -77 -261 -77q-200 0 -342 142t-142 342t142 342t342 142t342 -142t142 -342q0 -141 -78 -262zM176 694q0 -136 97 -233t234 -97t233.5 97t96.5 233t-96.5 233t-233.5 97t-234 -97 t-97 -233zM300 801v-200h400v200h-400z" />
|
|
||||||
<glyph unicode="" d="M700 750v400q0 21 -14.5 35.5t-35.5 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-400q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5zM800 975v166q167 -62 272 -210t105 -331q0 -118 -45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123 t-123 184t-45.5 224.5q0 183 105 331t272 210v-166q-103 -55 -165 -155t-62 -220q0 -177 125 -302t302 -125t302 125t125 302q0 120 -62 220t-165 155z" />
|
|
||||||
<glyph unicode="" d="M1200 1h-200v1200h200v-1200zM900 1h-200v800h200v-800zM600 1h-200v500h200v-500zM300 301h-200v-300h200v300z" />
|
|
||||||
<glyph unicode="" d="M488 183l38 -151q40 -5 74 -5q27 0 74 5l38 151l6 2q46 13 93 39l5 3l134 -81q56 44 104 105l-80 134l3 5q24 44 39 93l1 6l152 38q5 40 5 74q0 28 -5 73l-152 38l-1 6q-16 51 -39 93l-3 5l80 134q-44 58 -104 105l-134 -81l-5 3q-45 25 -93 39l-6 1l-38 152q-40 5 -74 5 q-27 0 -74 -5l-38 -152l-5 -1q-50 -14 -94 -39l-5 -3l-133 81q-59 -47 -105 -105l80 -134l-3 -5q-25 -47 -38 -93l-2 -6l-151 -38q-6 -48 -6 -73q0 -33 6 -74l151 -38l2 -6q14 -49 38 -93l3 -5l-80 -134q45 -59 105 -105l133 81l5 -3q45 -26 94 -39zM600 815q89 0 152 -63 t63 -151q0 -89 -63 -152t-152 -63t-152 63t-63 152q0 88 63 151t152 63z" />
|
|
||||||
<glyph unicode="" d="M900 1100h275q10 0 17.5 -7.5t7.5 -17.5v-50q0 -11 -7 -18t-18 -7h-1050q-11 0 -18 7t-7 18v50q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5v-100zM800 1100v100h-300v-100h300zM200 900h900v-800q0 -41 -29.5 -71 t-70.5 -30h-700q-41 0 -70.5 30t-29.5 71v800zM300 100h100v700h-100v-700zM500 100h100v700h-100v-700zM700 100h100v700h-100v-700zM900 100h100v700h-100v-700z" />
|
|
||||||
<glyph unicode="" d="M1301 601h-200v-600h-300v400h-300v-400h-300v600h-200l656 644z" />
|
|
||||||
<glyph unicode="" d="M600 700h400v-675q0 -11 -7 -18t-18 -7h-850q-11 0 -18 7t-7 18v1150q0 11 7 18t18 7h475v-500zM1000 800h-300v300z" />
|
|
||||||
<glyph unicode="" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM600 600h200 v-100h-300v400h100v-300z" />
|
|
||||||
<glyph unicode="" d="M721 400h-242l-40 -400h-539l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538zM712 500l-27 300h-170l-27 -300h224z" />
|
|
||||||
<glyph unicode="" d="M1100 400v-400h-1100v400h490l-290 300h200v500h300v-500h200l-290 -300h490zM988 300h-175v-100h175v100z" />
|
|
||||||
<glyph unicode="" d="M600 1199q122 0 233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233t47.5 233t127.5 191t191 127.5t233 47.5zM600 1012q-170 0 -291 -121t-121 -291t121 -291t291 -121t291 121 t121 291t-121 291t-291 121zM700 600h150l-250 -300l-250 300h150v300h200v-300z" />
|
|
||||||
<glyph unicode="" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM850 600h-150 v-300h-200v300h-150l250 300z" />
|
|
||||||
<glyph unicode="" d="M0 500l200 700h800q199 -700 200 -700v-475q0 -11 -7 -18t-18 -7h-1150q-11 0 -18 7t-7 18v475zM903 1000h-606l-97 -500h200l50 -200h300l50 200h200z" />
|
|
||||||
<glyph unicode="" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5zM797 598 l-297 -201v401z" />
|
|
||||||
<glyph unicode="" d="M1177 600h-150q0 -177 -125 -302t-302 -125t-302 125t-125 302t125 302t302 125q136 0 246 -81l-146 -146h400v400l-145 -145q-157 122 -355 122q-118 0 -224.5 -45.5t-184 -123t-123 -184t-45.5 -224.5t45.5 -224.5t123 -184t184 -123t224.5 -45.5t224.5 45.5t184 123 t123 184t45.5 224.5z" />
|
|
||||||
<glyph unicode="" d="M700 800l147 147q-112 80 -247 80q-177 0 -302 -125t-125 -302h-150q0 118 45.5 224.5t123 184t184 123t224.5 45.5q198 0 355 -122l145 145v-400h-400zM500 400l-147 -147q112 -80 247 -80q177 0 302 125t125 302h150q0 -118 -45.5 -224.5t-123 -184t-184 -123 t-224.5 -45.5q-198 0 -355 122l-145 -145v400h400z" />
|
|
||||||
<glyph unicode="" d="M100 1200v-1200h1100v1200h-1100zM1100 100h-900v900h900v-900zM400 800h-100v100h100v-100zM1000 800h-500v100h500v-100zM400 600h-100v100h100v-100zM1000 600h-500v100h500v-100zM400 400h-100v100h100v-100zM1000 400h-500v100h500v-100zM400 200h-100v100h100v-100 zM1000 300h-500v-100h500v100z" />
|
|
||||||
<glyph unicode="" d="M200 0h-100v1100h100v-1100zM1100 600v500q-40 -81 -101.5 -115.5t-127.5 -29.5t-138 25t-139.5 40t-125.5 25t-103 -29.5t-65 -115.5v-500q60 60 127.5 84t127.5 17.5t122 -23t119 -30t110 -11t103 42t91 120.5z" />
|
|
||||||
<glyph unicode="" d="M1200 275v300q0 116 -49.5 227t-131 192.5t-192.5 131t-227 49.5t-227 -49.5t-192.5 -131t-131 -192.5t-49.5 -227v-300q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 127 70.5 231.5t184.5 161.5t245 57t245 -57t184.5 -161.5t70.5 -231.5v-300q0 -11 7 -18t18 -7h50 q11 0 18 7t7 18zM400 480v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14zM1000 480v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14z" />
|
|
||||||
<glyph unicode="" d="M0 800v-400h300l300 -200v800l-300 -200h-300zM971 600l141 -141l-71 -71l-141 141l-141 -141l-71 71l141 141l-141 141l71 71l141 -141l141 141l71 -71z" />
|
|
||||||
<glyph unicode="" d="M0 800v-400h300l300 -200v800l-300 -200h-300zM700 857l69 53q111 -135 111 -310q0 -169 -106 -302l-67 54q86 110 86 248q0 146 -93 257z" />
|
|
||||||
<glyph unicode="" d="M974 186l6 8q142 178 142 405q0 230 -144 408l-6 8l-83 -64l7 -8q123 -151 123 -344q0 -189 -119 -339l-7 -8zM300 801l300 200v-800l-300 200h-300v400h300zM702 858l69 53q111 -135 111 -310q0 -170 -106 -303l-67 55q86 110 86 248q0 145 -93 257z" />
|
|
||||||
<glyph unicode="" d="M100 700h400v100h100v100h-100v300h-500v-600h100v100zM1200 700v500h-600v-200h100v-300h200v-300h300v200h-200v100h200zM100 1100h300v-300h-300v300zM800 800v300h300v-300h-300zM200 900h100v100h-100v-100zM900 1000h100v-100h-100v100zM300 600h-100v-100h-200 v-500h500v500h-200v100zM900 200v-100h-200v100h-100v100h100v200h-200v100h300v-300h200v-100h-100zM400 400v-300h-300v300h300zM300 200h-100v100h100v-100zM1100 300h100v-100h-100v100zM600 100h100v-100h-100v100zM1200 100v-100h-300v100h300z" />
|
|
||||||
<glyph unicode="" d="M100 1200h-100v-1000h100v1000zM300 200h-100v1000h100v-1000zM700 200h-200v1000h200v-1000zM900 200h-100v1000h100v-1000zM1200 1200v-1000h-200v1000h200zM400 100v-100h-300v100h300zM500 91h100v-91h-100v91zM700 91h100v-91h-100v91zM1100 91v-91h-200v91h200z " />
|
|
||||||
<glyph unicode="" d="M1200 500l-500 -500l-699 700v475q0 10 7.5 17.5t17.5 7.5h474zM320 882q29 29 29 71t-29 71q-30 30 -71.5 30t-71.5 -30q-29 -29 -29 -71t29 -71q30 -30 71.5 -30t71.5 30z" />
|
|
||||||
<glyph unicode="" d="M1201 500l-500 -500l-699 700v475q0 11 7 18t18 7h474zM1501 500l-500 -500l-50 50l450 450l-700 700h100zM320 882q30 29 30 71t-30 71q-29 30 -71 30t-71 -30q-30 -29 -30 -71t30 -71q29 -30 71 -30t71 30z" />
|
|
||||||
<glyph unicode="" d="M1200 1200v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900v1025l175 175h925z" />
|
|
||||||
<glyph unicode="" d="M947 829l-94 346q-2 11 -10 18t-18 7h-450q-10 0 -18 -7t-10 -18l-94 -346l40 -124h592zM1200 800v-700h-200v200h-800v-200h-200v700h200l100 -200h600l100 200h200zM881 176l38 -152q2 -10 -3.5 -17t-15.5 -7h-600q-10 0 -15.5 7t-3.5 17l38 152q2 10 11.5 17t19.5 7 h500q10 0 19.5 -7t11.5 -17z" />
|
|
||||||
<glyph unicode="" d="M1200 0v66q-34 1 -74 43q-18 19 -33 42t-21 37l-6 13l-385 998h-93l-399 -1006q-24 -48 -52 -75q-12 -12 -33 -25t-36 -20l-15 -7v-66h365v66q-41 0 -72 11t-49 38t1 71l92 234h391l82 -222q16 -45 -5.5 -88.5t-74.5 -43.5v-66h417zM416 521l178 457l46 -140l116 -317 h-340z" />
|
|
||||||
<glyph unicode="" d="M100 1199h471q120 0 213 -88t93 -228q0 -55 -11.5 -101.5t-28 -74t-33.5 -47.5t-28 -28l-12 -7q8 -3 21.5 -9t48 -31.5t60.5 -58t47.5 -91.5t21.5 -129q0 -84 -59 -156.5t-142 -111t-162 -38.5h-500v89q41 7 70.5 32.5t29.5 65.5v827q0 28 -1 39.5t-5.5 26t-15.5 21 t-29 14t-49 14.5v70zM400 1079v-379h139q76 0 130 61.5t54 138.5q0 82 -84 130.5t-239 48.5zM400 200h161q89 0 153 48.5t64 132.5q0 90 -62.5 154.5t-156.5 64.5h-159v-400z" />
|
|
||||||
<glyph unicode="" d="M877 1200l2 -57q-33 -8 -62 -25.5t-46 -37t-29.5 -38t-17.5 -30.5l-5 -12l-128 -825q-10 -52 14 -82t95 -36v-57h-500v57q77 7 134.5 40.5t65.5 80.5l173 849q10 56 -10 74t-91 37q-6 1 -10.5 2.5t-9.5 2.5v57h425z" />
|
|
||||||
<glyph unicode="" d="M1150 1200h150v-300h-50q0 29 -8 48.5t-18.5 30t-33.5 15t-39.5 5.5t-50.5 1h-200v-850l100 -50v-100h-400v100l100 50v850h-200q-34 0 -50.5 -1t-40 -5.5t-33.5 -15t-18.5 -30t-8.5 -48.5h-49v300h150h700zM100 1000v-800h75l-125 -167l-125 167h75v800h-75l125 167 l125 -167h-75z" />
|
|
||||||
<glyph unicode="" d="M950 1201h150v-300h-50q0 29 -8 48.5t-18 30t-33.5 15t-40 5.5t-50.5 1h-200v-650l100 -50v-100h-400v100l100 50v650h-200q-34 0 -50.5 -1t-39.5 -5.5t-33.5 -15t-18.5 -30t-8 -48.5h-50v300h150h700zM200 101h800v75l167 -125l-167 -125v75h-800v-75l-167 125l167 125 v-75z" />
|
|
||||||
<glyph unicode="" d="M700 950v100q0 21 -14.5 35.5t-35.5 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h600q21 0 35.5 15t14.5 35zM1100 650v100q0 21 -14.5 35.5t-35.5 14.5h-1000q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h1000 q21 0 35.5 15t14.5 35zM900 350v100q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35zM1200 50v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35 t35.5 -15h1100q21 0 35.5 15t14.5 35z" />
|
|
||||||
<glyph unicode="" d="M1000 950v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35zM1200 650v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h1100 q21 0 35.5 15t14.5 35zM1000 350v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35zM1200 50v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35 t35.5 -15h1100q21 0 35.5 15t14.5 35z" />
|
|
||||||
<glyph unicode="" d="M500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-600q-21 0 -35.5 15t-14.5 35zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1000q-21 0 -35.5 15 t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" />
|
|
||||||
<glyph unicode="" d="M0 950v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15 t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" />
|
|
||||||
<glyph unicode="" d="M0 950v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM300 950v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM300 650v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800 q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35zM0 50v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM300 50v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35z" />
|
|
||||||
<glyph unicode="" d="M400 1100h-100v-1100h100v1100zM700 950v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35zM1100 650v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15 h500q20 0 35 15t15 35zM100 425v75h-201v100h201v75l166 -125zM900 350v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35zM1200 50v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5 v-100q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35z" />
|
|
||||||
<glyph unicode="" d="M201 950v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35zM801 1100h100v-1100h-100v1100zM601 650v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15 h500q20 0 35 15t15 35zM1101 425v75h200v100h-200v75l-167 -125zM401 350v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35zM701 50v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5 v-100q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35z" />
|
|
||||||
<glyph unicode="" d="M900 925v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53v650q0 31 22 53t53 22h750q31 0 53 -22t22 -53zM1200 300l-300 300l300 300v-600z" />
|
|
||||||
<glyph unicode="" d="M1200 1056v-1012q0 -18 -12.5 -31t-31.5 -13h-1112q-18 0 -31 13t-13 31v1012q0 18 13 31t31 13h1112q19 0 31.5 -13t12.5 -31zM1100 1000h-1000v-737l247 182l298 -131l-74 156l293 318l236 -288v500zM476 750q0 -56 -39 -95t-95 -39t-95 39t-39 95t39 95t95 39t95 -39 t39 -95z" />
|
|
||||||
<glyph unicode="" d="M600 1213q123 0 227 -63t164.5 -169.5t60.5 -229.5t-73 -272q-73 -114 -166.5 -237t-150.5 -189l-57 -66q-10 9 -27 26t-66.5 70.5t-96 109t-104 135.5t-100.5 155q-63 139 -63 262q0 124 60.5 231.5t165 172t226.5 64.5zM599 514q107 0 182.5 75.5t75.5 182.5t-75.5 182 t-182.5 75t-182 -75.5t-75 -181.5q0 -107 75.5 -182.5t181.5 -75.5z" />
|
|
||||||
<glyph unicode="" d="M600 1199q122 0 233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233t47.5 233t127.5 191t191 127.5t233 47.5zM600 173v854q-176 0 -301.5 -125t-125.5 -302t125.5 -302t301.5 -125z " />
|
|
||||||
<glyph unicode="" d="M554 1295q21 -71 57.5 -142.5t76 -130.5t83 -118.5t82 -117t70 -116t50 -125.5t18.5 -136q0 -89 -39 -165.5t-102 -126.5t-140 -79.5t-156 -33.5q-114 6 -211.5 53t-161.5 138.5t-64 210.5q0 94 34 186t88.5 172.5t112 159t115 177t87.5 194.5zM455 296q-7 6 -18 17 t-34 48t-33 77q-15 73 -14 143.5t10 122.5l9 51q-92 -110 -119.5 -185t-12.5 -156q14 -82 59.5 -136t136.5 -80z" />
|
|
||||||
<glyph unicode="" d="M1108 902l113 113l-21 85l-92 28l-113 -113zM1100 625v-225q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5v300q0 165 117.5 282.5t282.5 117.5q366 -6 397 -14l-186 -186h-311q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5 t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v125zM436 341l161 50l412 412l-114 113l-405 -405z" />
|
|
||||||
<glyph unicode="" d="M1100 453v-53q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5v300q0 165 117.5 282.5t282.5 117.5h261l2 -80q-133 -32 -218 -120h-145q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5z M813 431l360 324l-359 318v-216q-7 0 -19 -1t-48 -8t-69.5 -18.5t-76.5 -37t-76.5 -59t-62 -88t-39.5 -121.5q30 38 81.5 64t103 35.5t99 14t77.5 3.5l29 -1v-209z" />
|
|
||||||
<glyph unicode="" d="M1100 569v-169q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5v300q0 165 117.5 282.5t282.5 117.5h300q60 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69z M625 348l566 567l-136 137l-430 -431l-147 147l-136 -136z" />
|
|
||||||
<glyph unicode="" d="M900 303v198h-200v-200h195l-295 -300l-300 300h200v200h-200v-198l-300 300l300 296v-198h200v200h-200l300 300l295 -300h-195v-200h200v198l300 -296z" />
|
|
||||||
<glyph unicode="" d="M900 0l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-1100z" />
|
|
||||||
<glyph unicode="" d="M1200 0l-500 488v-488l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-487l500 487v-1100z" />
|
|
||||||
<glyph unicode="" d="M1200 0l-500 488v-488l-564 550l564 550v-487l500 487v-1100z" />
|
|
||||||
<glyph unicode="" d="M1100 550l-900 550v-1100z" />
|
|
||||||
<glyph unicode="" d="M500 150v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5zM900 150v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800q0 -21 14.5 -35.5t35.5 -14.5h200 q21 0 35.5 14.5t14.5 35.5z" />
|
|
||||||
<glyph unicode="" d="M1100 150v800q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-800q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35z" />
|
|
||||||
<glyph unicode="" d="M500 0v488l-500 -488v1100l500 -487v487l564 -550z" />
|
|
||||||
<glyph unicode="" d="M1050 1100h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v488l-500 -488v1100l500 -487v487l500 -487v437q0 21 14.5 35.5t35.5 14.5z" />
|
|
||||||
<glyph unicode="" d="M850 1100h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v1100l500 -487v437q0 21 14.5 35.5t35.5 14.5z" />
|
|
||||||
<glyph unicode="" d="M650 1064l-550 -564h1100zM1200 350v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5z" />
|
|
||||||
<glyph unicode="" d="M777 7l240 240l-353 353l353 353l-240 240l-592 -594z" />
|
|
||||||
<glyph unicode="" d="M513 -46l-241 240l353 353l-353 353l241 240l572 -571l21 -22l-1 -1v-1z" />
|
|
||||||
<glyph unicode="" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM500 900v-200h-200v-200h200v-200h200v200h200v200h-200v200h-200z" />
|
|
||||||
<glyph unicode="" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM300 700v-200h600v200h-600z" />
|
|
||||||
<glyph unicode="" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM247 741l141 -141l-142 -141l213 -213l141 142l141 -142l213 213l-142 141l142 141l-213 212l-141 -141 l-141 142z" />
|
|
||||||
<glyph unicode="" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM546 623l-102 102l-174 -174l276 -277l411 411l-175 174z" />
|
|
||||||
<glyph unicode="" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM500 500h200q5 3 14 8t31.5 25.5t39.5 45.5t31 69t14 94q0 51 -17.5 89t-42 58t-58.5 32t-58.5 15t-51.5 3 q-105 0 -172 -56t-67 -183h144q4 0 11.5 -1t11 -1t6.5 3t3 9t1 11t3.5 8.5t3.5 6t5.5 4t6.5 2.5t9 1.5t9 0.5h11.5h12.5q19 0 30 -10t11 -26q0 -22 -4 -28t-27 -22q-5 -1 -12.5 -3t-27 -13.5t-34 -27t-26.5 -46t-11 -68.5zM500 400v-100h200v100h-200z" />
|
|
||||||
<glyph unicode="" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM500 900v-100h200v100h-200zM400 700v-100h100v-200h-100v-100h400v100h-100v300h-300z" />
|
|
||||||
<glyph unicode="" d="M1200 700v-200h-203q-25 -102 -116.5 -186t-180.5 -117v-197h-200v197q-140 27 -208 102.5t-98 200.5h-194v200h194q15 60 36 104.5t55.5 86t88 69t126.5 40.5v200h200v-200q54 -20 113 -60t112.5 -105.5t71.5 -134.5h203zM700 500v-206q149 48 201 206h-201v200h200 q-25 74 -76 127.5t-124 76.5v-204h-200v203q-75 -24 -130 -77.5t-79 -125.5h209v-200h-210q24 -73 79.5 -127.5t130.5 -78.5v206h200z" />
|
|
||||||
<glyph unicode="" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM844 735 l-135 -135l135 -135l-109 -109l-135 135l-135 -135l-109 109l135 135l-135 135l109 109l135 -135l135 135z" />
|
|
||||||
<glyph unicode="" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM896 654 l-346 -345l-228 228l141 141l87 -87l204 205z" />
|
|
||||||
<glyph unicode="" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM248 385l568 567q-100 62 -216 62q-171 0 -292.5 -121.5t-121.5 -292.5q0 -115 62 -215zM955 809l-564 -564q97 -59 209 -59q171 0 292.5 121.5 t121.5 292.5q0 112 -59 209z" />
|
|
||||||
<glyph unicode="" d="M1200 400h-600v-301l-600 448l600 453v-300h600v-300z" />
|
|
||||||
<glyph unicode="" d="M600 400h-600v300h600v300l600 -453l-600 -448v301z" />
|
|
||||||
<glyph unicode="" d="M1098 600h-298v-600h-300v600h-296l450 600z" />
|
|
||||||
<glyph unicode="" d="M998 600l-449 -600l-445 600h296v600h300v-600h298z" />
|
|
||||||
<glyph unicode="" d="M600 199v301q-95 -2 -183 -20t-170 -52t-147 -92.5t-100 -135.5q6 132 41 238.5t103.5 193t184 138t271.5 59.5v271l600 -453z" />
|
|
||||||
<glyph unicode="" d="M1200 1200h-400l129 -129l-294 -294l142 -142l294 294l129 -129v400zM565 423l-294 -294l129 -129h-400v400l129 -129l294 294z" />
|
|
||||||
<glyph unicode="" d="M871 730l129 -130h-400v400l129 -129l295 295l142 -141zM200 600h400v-400l-129 130l-295 -295l-142 141l295 295z" />
|
|
||||||
<glyph unicode="" d="M600 1177q118 0 224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5t45.5 224.5t123 184t184 123t224.5 45.5zM686 549l58 302q4 20 -8 34.5t-33 14.5h-207q-20 0 -32 -14.5t-8 -34.5 l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5t21.5 34.5zM700 400h-200v-100h200v100z" />
|
|
||||||
<glyph unicode="" d="M1200 900h-111v6t-1 15t-3 18l-34 172q-11 39 -41.5 63t-69.5 24q-32 0 -61 -17l-239 -144q-22 -13 -40 -35q-19 24 -40 36l-238 144q-33 18 -62 18q-39 0 -69.5 -23t-40.5 -61l-35 -177q-2 -8 -3 -18t-1 -15v-6h-111v-100h100v-200h400v300h200v-300h400v200h100v100z M731 900l202 197q5 -12 12 -32.5t23 -64t25 -72t7 -28.5h-269zM481 900h-281q-3 0 14 48t35 96l18 47zM100 0h400v400h-400v-400zM700 400h400v-400h-400v400z" />
|
|
||||||
<glyph unicode="" d="M0 121l216 193q-9 53 -13 83t-5.5 94t9 113t38.5 114t74 124q47 60 99.5 102.5t103 68t127.5 48t145.5 37.5t184.5 43.5t220 58.5q0 -189 -22 -343t-59 -258t-89 -181.5t-108.5 -120t-122 -68t-125.5 -30t-121.5 -1.5t-107.5 12.5t-87.5 17t-56.5 7.5l-99 -55l-201 -202 v143zM692 611q70 38 118.5 69.5t102 79t99 111.5t86.5 148q22 50 24 60t-6 19q-7 5 -17 5t-26.5 -14.5t-33.5 -39.5q-35 -51 -113.5 -108.5t-139.5 -89.5l-61 -32q-369 -197 -458 -401q-48 -111 -28.5 -117.5t86.5 76.5q55 66 367 234z" />
|
|
||||||
<glyph unicode="" d="M1261 600l-26 -40q-6 -10 -20 -30t-49 -63.5t-74.5 -85.5t-97 -90t-116.5 -83.5t-132.5 -59t-145.5 -23.5t-145.5 23.5t-132.5 59t-116.5 83.5t-97 90t-74.5 85.5t-49 63.5t-20 30l-26 40l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5 t145.5 -23.5t132.5 -59t116.5 -83.5t97 -90t74.5 -85.5t49 -63.5t20 -30zM600 240q64 0 123.5 20t100.5 45.5t85.5 71.5t66.5 75.5t58 81.5t47 66q-1 1 -28.5 37.5t-42 55t-43.5 53t-57.5 63.5t-58.5 54q49 -74 49 -163q0 -124 -88 -212t-212 -88t-212 88t-88 212 q0 85 46 158q-102 -87 -226 -258q7 -10 40.5 -58t56 -78.5t68 -77.5t87.5 -75t103 -49.5t125 -21.5zM484 762l-107 -106q49 -124 154 -191l105 105q-37 24 -75 72t-57 84z" />
|
|
||||||
<glyph unicode="" d="M906 1200l-314 -1200h-148l37 143q-82 21 -165 71.5t-140 102t-109.5 112t-72 88.5t-29.5 43l-26 40l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5q61 0 121 -17l37 142h148zM1261 600l-26 -40q-7 -12 -25.5 -38t-63.5 -79.5t-95.5 -102.5 t-124 -100t-146.5 -79l38 145q22 15 44.5 34t46 44t40.5 44t41 50.5t33.5 43.5t33 44t24.5 34q-97 127 -140 175l39 146q67 -54 131.5 -125.5t87.5 -103.5t36 -52zM513 264l37 141q-107 18 -178.5 101.5t-71.5 193.5q0 85 46 158q-102 -87 -226 -258q210 -282 393 -336z M484 762l-107 -106q49 -124 154 -191l47 47l23 87q-30 28 -59 69t-44 68z" />
|
|
||||||
<glyph unicode="" d="M-47 0h1294q37 0 50.5 35.5t-7.5 67.5l-642 1056q-20 33 -48 36t-48 -29l-642 -1066q-21 -32 -7.5 -66t50.5 -34zM700 200v100h-200v-100h-345l445 723l445 -723h-345zM700 700h-200v-100l100 -300l100 300v100z" />
|
|
||||||
<glyph unicode="" d="M800 711l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -91 100 -113v-64q0 -21 -13 -29t-32 1l-94 78h-222l-94 -78q-19 -9 -32 -1t-13 29v64q0 22 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5v41q0 20 11 44.5t26 38.5 l363 325v339q0 62 44 106t106 44t106 -44t44 -106v-339z" />
|
|
||||||
<glyph unicode="" d="M941 800l-600 -600h-341v200h259l600 600h241v198l300 -295l-300 -300v197h-159zM381 678l141 142l-181 180h-341v-200h259zM1100 598l300 -295l-300 -300v197h-241l-181 181l141 142l122 -123h159v198z" />
|
|
||||||
<glyph unicode="" d="M100 1100h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5v600q0 41 29.5 70.5t70.5 29.5z" />
|
|
||||||
<glyph unicode="" d="M400 900h-300v300h300v-300zM1100 900h-300v300h300v-300zM1100 800v-200q0 -42 -3 -83t-15 -104t-31.5 -116t-58 -109.5t-89 -96.5t-129 -65.5t-174.5 -25.5t-174.5 25.5t-129 65.5t-89 96.5t-58 109.5t-31.5 116t-15 104t-3 83v200h300v-250q0 -113 6 -145 q17 -92 102 -117q39 -11 92 -11q37 0 66.5 5.5t50 15.5t36 24t24 31.5t14 37.5t7 42t2.5 45t0 47v25v250h300z" />
|
|
||||||
<glyph unicode="" d="M902 184l226 227l-578 579l-580 -579l227 -227l352 353z" />
|
|
||||||
<glyph unicode="" d="M650 218l578 579l-226 227l-353 -353l-352 353l-227 -227z" />
|
|
||||||
<glyph unicode="" d="M1198 400v600h-796l215 -200h381v-400h-198l299 -283l299 283h-200zM-198 700l299 283l300 -283h-203v-400h385l215 -200h-800v600h-196z" />
|
|
||||||
<glyph unicode="" d="M1050 1200h94q20 0 35 -14.5t15 -35.5t-15 -35.5t-35 -14.5h-54l-201 -961q-2 -4 -6 -10.5t-19 -17.5t-33 -11h-31v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-300v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-50q-21 0 -35.5 15t-14.5 35 q0 21 14.5 35.5t35.5 14.5h535l48 200h-633q-32 0 -54.5 21t-27.5 43l-100 475q-5 24 10 42q14 19 39 19h896l38 162q5 17 18.5 27.5t30.5 10.5z" />
|
|
||||||
<glyph unicode="" d="M1200 1000v-100h-1200v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500zM0 800h1200v-800h-1200v800z" />
|
|
||||||
<glyph unicode="" d="M201 800l-200 -400v600h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-200h-1000zM1501 700l-300 -700h-1200l300 700h1200z" />
|
|
||||||
<glyph unicode="" d="M302 300h198v600h-198l298 300l298 -300h-198v-600h198l-298 -300z" />
|
|
||||||
<glyph unicode="" d="M900 303v197h-600v-197l-300 297l300 298v-198h600v198l300 -298z" />
|
|
||||||
<glyph unicode="" d="M31 400l172 739q5 22 23 41.5t38 19.5h672q19 0 37.5 -22.5t23.5 -45.5l172 -732h-1138zM100 300h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM900 200h-100v-100h100v100z M1100 200h-100v-100h100v100z" />
|
|
||||||
<glyph unicode="" d="M1100 200v850q0 21 14.5 35.5t35.5 14.5q20 0 35 -14.5t15 -35.5v-850q0 -20 -15 -35t-35 -15q-21 0 -35.5 15t-14.5 35zM325 800l675 250v-850l-675 200h-38l47 -276q2 -12 -3 -17.5t-11 -6t-21 -0.5h-8h-83q-20 0 -34.5 14t-18.5 35q-56 337 -56 351v250v5 q0 13 0.5 18.5t2.5 13t8 10.5t15 3h200zM-101 600v50q0 24 25 49t50 38l25 13v-250l-11 5.5t-24 14t-30 21.5t-24 27.5t-11 31.5z" />
|
|
||||||
<glyph unicode="" d="M445 1180l-45 -233l-224 78l78 -225l-233 -44l179 -156l-179 -155l233 -45l-78 -224l224 78l45 -233l155 179l155 -179l45 233l224 -78l-78 224l234 45l-180 155l180 156l-234 44l78 225l-224 -78l-45 233l-155 -180z" />
|
|
||||||
<glyph unicode="" d="M700 1200h-50q-27 0 -51 -20t-38 -48l-96 -198l-145 -196q-20 -26 -20 -63v-400q0 -75 100 -75h61q123 -100 139 -100h250q46 0 83 57l238 344q29 31 29 74v100q0 44 -30.5 84.5t-69.5 40.5h-328q28 118 28 125v150q0 44 -30.5 84.5t-69.5 40.5zM700 925l-50 -225h450 v-125l-250 -375h-214l-136 100h-100v375l150 212l100 213h50v-175zM0 800v-600h200v600h-200z" />
|
|
||||||
<glyph unicode="" d="M700 0h-50q-27 0 -51 20t-38 48l-96 198l-145 196q-20 26 -20 63v400q0 75 100 75h61q123 100 139 100h250q46 0 83 -57l238 -344q29 -31 29 -74v-100q0 -44 -30.5 -84.5t-69.5 -40.5h-328q28 -118 28 -125v-150q0 -44 -30.5 -84.5t-69.5 -40.5zM200 400h-200v600h200 v-600zM700 275l-50 225h450v125l-250 375h-214l-136 -100h-100v-375l150 -212l100 -213h50v175z" />
|
|
||||||
<glyph unicode="" d="M364 873l362 230q14 6 25 6q17 0 29 -12l109 -112q14 -14 14 -34q0 -18 -11 -32l-85 -121h302q85 0 138.5 -38t53.5 -110t-54.5 -111t-138.5 -39h-107l-130 -339q-7 -22 -20.5 -41.5t-28.5 -19.5h-341q-7 0 -90 81t-83 94v525q0 17 14 35.5t28 28.5zM408 792v-503 l100 -89h293l131 339q6 21 19.5 41t28.5 20h203q16 0 25 15t9 36q0 20 -9 34.5t-25 14.5h-457h-6.5h-7.5t-6.5 0.5t-6 1t-5 1.5t-5.5 2.5t-4 4t-4 5.5q-5 12 -5 20q0 14 10 27l147 183l-86 83zM208 200h-200v600h200v-600z" />
|
|
||||||
<glyph unicode="" d="M475 1104l365 -230q7 -4 16.5 -10.5t26 -26t16.5 -36.5v-526q0 -13 -85.5 -93.5t-93.5 -80.5h-342q-15 0 -28.5 20t-19.5 41l-131 339h-106q-84 0 -139 39t-55 111t54 110t139 37h302l-85 121q-11 16 -11 32q0 21 14 34l109 113q13 12 29 12q11 0 25 -6zM370 946 l145 -184q10 -11 10 -26q0 -11 -5 -20q-1 -3 -3.5 -5.5l-4 -4t-5 -2.5t-5.5 -1.5t-6.5 -1t-6.5 -0.5h-7.5h-6.5h-476v-100h222q15 0 28.5 -20.5t19.5 -40.5l131 -339h293l106 89v502l-342 237zM1199 201h-200v600h200v-600z" />
|
|
||||||
<glyph unicode="" d="M1100 473v342q0 15 -20 28.5t-41 19.5l-339 131v106q0 84 -39 139t-111 55t-110 -53.5t-38 -138.5v-302l-121 84q-15 12 -33.5 11.5t-32.5 -13.5l-112 -110q-22 -22 -6 -53l230 -363q4 -6 10.5 -15.5t26 -25t36.5 -15.5h525q13 0 94 83t81 90zM911 400h-503l-236 339 l83 86l183 -146q22 -18 47 -5q3 1 5.5 3.5l4 4t2.5 5t1.5 5.5t1 6.5t0.5 6v7.5v7v456q0 22 25 31t50 -0.5t25 -30.5v-202q0 -16 20 -29.5t41 -19.5l339 -130v-294zM1000 200v-200h-600v200h600z" />
|
|
||||||
<glyph unicode="" d="M305 1104v200h600v-200h-600zM605 310l339 131q20 6 40.5 19.5t20.5 28.5v342q0 7 -81 90t-94 83h-525q-17 0 -35.5 -14t-28.5 -28l-10 -15l-230 -362q-15 -31 7 -53l112 -110q13 -13 32 -13.5t34 10.5l121 85l-1 -302q0 -84 38.5 -138t110.5 -54t111 55t39 139v106z M905 804v-294l-340 -130q-20 -6 -40 -20t-20 -29v-202q0 -22 -25 -31t-50 0t-25 31v456v14.5t-1.5 11.5t-5 12t-9.5 7q-24 13 -46 -5l-184 -146l-83 86l237 339h503z" />
|
|
||||||
<glyph unicode="" d="M603 1195q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5zM598 701h-298v-201h300l-2 -194l402 294l-402 298v-197z" />
|
|
||||||
<glyph unicode="" d="M597 1195q122 0 232.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-218 -217.5t-300 -80t-299.5 80t-217.5 217.5t-80 299.5q0 122 47.5 232.5t127.5 190.5t190.5 127.5t231.5 47.5zM200 600l400 -294v194h302v201h-300v197z" />
|
|
||||||
<glyph unicode="" d="M603 1195q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5zM300 600h200v-300h200v300h200l-300 400z" />
|
|
||||||
<glyph unicode="" d="M603 1195q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5zM500 900v-300h-200l300 -400l300 400h-200v300h-200z" />
|
|
||||||
<glyph unicode="" d="M603 1195q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5zM627 1101q-15 -12 -36.5 -21t-34.5 -12t-44 -8t-39 -6 q-15 -3 -45.5 0.5t-45.5 -2.5q-21 -7 -52 -26.5t-34 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -90.5t-29.5 -79.5q-8 -33 5.5 -92.5t7.5 -87.5q0 -9 17 -44t16 -60q12 0 23 -5.5t23 -15t20 -13.5q24 -12 108 -42q22 -8 53 -31.5t59.5 -38.5t57.5 -11q8 -18 -15 -55 t-20 -57q42 -71 87 -80q0 -6 -3 -15.5t-3.5 -14.5t4.5 -17q102 -2 221 112q30 29 47 47t34.5 49t20.5 62q-14 9 -37 9.5t-36 7.5q-14 7 -49 15t-52 19q-9 0 -39.5 -0.5t-46.5 -1.5t-39 -6.5t-39 -16.5q-50 -35 -66 -12q-4 2 -3.5 25.5t0.5 25.5q-6 13 -26.5 17t-24.5 7 q2 22 -2 41t-16.5 28t-38.5 -20q-23 -25 -42 4q-19 28 -8 58q6 16 22 22q6 -1 26 -1.5t33.5 -4t19.5 -13.5q12 -19 32 -37.5t34 -27.5l14 -8q0 3 9.5 39.5t5.5 57.5q-4 23 14.5 44.5t22.5 31.5q5 14 10 35t8.5 31t15.5 22.5t34 21.5q-6 18 10 37q8 0 23.5 -1.5t24.5 -1.5 t20.5 4.5t20.5 15.5q-10 23 -30.5 42.5t-38 30t-49 26.5t-43.5 23q11 41 1 44q31 -13 58.5 -14.5t39.5 3.5l11 4q6 36 -17 53.5t-64 28.5t-56 23q-19 -3 -37 0zM613 994q0 -18 8 -42.5t16.5 -44t9.5 -23.5q-9 2 -31 5t-36 5t-32 8t-30 14q3 12 16 30t16 25q10 -10 18.5 -10 t14 6t14.5 14.5t16 12.5z" />
|
|
||||||
<glyph unicode="" horiz-adv-x="1220" d="M100 1196h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 1096h-200v-100h200v100zM100 796h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 696h-500v-100h500v100zM100 396h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 296h-300v-100h300v100z " />
|
|
||||||
<glyph unicode="" d="M1100 1200v-100h-1000v100h1000zM150 1000h900l-350 -500v-300l-200 -200v500z" />
|
|
||||||
<glyph unicode="" d="M329 729l142 142l-200 200l129 129h-400v-400l129 129zM1200 1200v-400l-129 129l-200 -200l-142 142l200 200l-129 129h400zM271 129l129 -129h-400v400l129 -129l200 200l142 -142zM1071 271l129 129v-400h-400l129 129l-200 200l142 142z" />
|
|
||||||
<glyph unicode="" d="M596 1192q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM596 1010q-171 0 -292.5 -121.5t-121.5 -292.5q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5zM455 905 q22 0 38 -16t16 -39t-16 -39t-38 -16q-23 0 -39 16.5t-16 38.5t16 38.5t39 16.5zM708 821l1 1q-9 14 -9 28q0 22 16 38.5t39 16.5q22 0 38 -16t16 -39t-16 -39t-38 -16q-14 0 -29 10l-55 -145q17 -22 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5t-61.5 25.5t-25.5 61.5 q0 32 20.5 56.5t51.5 29.5zM855 709q23 0 38.5 -15.5t15.5 -38.5t-16 -39t-38 -16q-23 0 -39 16t-16 39q0 22 16 38t39 16zM345 709q23 0 39 -16t16 -38q0 -23 -16 -39t-39 -16q-22 0 -38 16t-16 39t15.5 38.5t38.5 15.5z" />
|
|
||||||
<glyph unicode="" d="M649 54l-16 22q-90 125 -293 323q-71 70 -104.5 105.5t-77 89.5t-61 99t-17.5 91q0 131 98.5 229.5t230.5 98.5q143 0 241 -129q103 129 246 129q129 0 226 -98.5t97 -229.5q0 -46 -17.5 -91t-61 -99t-77 -89.5t-104.5 -105.5q-203 -198 -293 -323zM844 524l12 12 q64 62 97.5 97t64.5 79t31 72q0 71 -48 119t-105 48q-74 0 -132 -82l-118 -171l-114 174q-51 79 -123 79q-60 0 -109.5 -49t-49.5 -118q0 -27 30.5 -70t61.5 -75.5t95 -94.5l22 -22q93 -90 190 -201q82 92 195 203z" />
|
|
||||||
<glyph unicode="" d="M476 406l19 -17l105 105l-212 212l389 389l247 -247l-95 -96l18 -18q46 -46 77 -99l29 29q35 35 62.5 88t27.5 96q0 93 -66 159l-141 141q-66 66 -159 66q-95 0 -159 -66l-283 -283q-66 -64 -66 -159q0 -93 66 -159zM123 193l141 -141q66 -66 159 -66q95 0 159 66 l283 283q66 66 66 159t-66 159l-141 141q-12 12 -19 17l-105 -105l212 -212l-389 -389l-247 248l95 95l-18 18q-46 45 -75 101l-55 -55q-66 -66 -66 -159q0 -94 66 -160z" />
|
|
||||||
<glyph unicode="" d="M200 100v953q0 21 30 46t81 48t129 38t163 15t162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5zM900 1000h-600v-700h600v700zM600 46q43 0 73.5 30.5t30.5 73.5t-30.5 73.5t-73.5 30.5t-73.5 -30.5t-30.5 -73.5 t30.5 -73.5t73.5 -30.5z" />
|
|
||||||
<glyph unicode="" d="M700 1029v-307l64 -14q34 -7 64 -16.5t70 -31.5t67.5 -52t47.5 -80.5t20 -112.5q0 -139 -89 -224t-244 -96v-77h-100v78q-152 17 -237 104q-40 40 -52.5 93.5t-15.5 139.5h139q5 -77 48.5 -126.5t117.5 -64.5v335l-27 7q-46 14 -79 26.5t-72 36t-62.5 52t-40 72.5 t-16.5 99q0 92 44 159.5t109 101t144 40.5v78h100v-79q38 -4 72.5 -13.5t75.5 -31.5t71 -53.5t51.5 -84t24.5 -118.5h-159q-8 72 -35 109.5t-101 50.5zM600 755v274q-61 -8 -97.5 -37.5t-36.5 -102.5q0 -29 8 -51t16.5 -34t29.5 -22.5t31 -13.5t38 -10q7 -2 11 -3zM700 548 v-311q170 18 170 151q0 64 -44 99.5t-126 60.5z" />
|
|
||||||
<glyph unicode="" d="M866 300l50 -147q-41 -25 -80.5 -36.5t-59 -13t-61.5 -1.5q-23 0 -128 33t-155 29q-39 -4 -82 -17t-66 -25l-24 -11l-55 145l16.5 11t15.5 10t13.5 9.5t14.5 12t14.5 14t17.5 18.5q48 55 54 126.5t-30 142.5h-221v100h166q-24 49 -44 104q-10 26 -14.5 55.5t-3 72.5 t25 90t68.5 87q97 88 263 88q129 0 230 -89t101 -208h-153q0 52 -34 89.5t-74 51.5t-76 14q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -11 2.5 -24.5t5.5 -24t9.5 -26.5t10.5 -25t14 -27.5t14 -25.5t15.5 -27t13.5 -24h242v-100h-197q8 -50 -2.5 -115t-31.5 -94 q-41 -59 -99 -113q35 11 84 18t70 7q32 1 102 -16t104 -17q76 0 136 30z" />
|
|
||||||
<glyph unicode="" d="M300 0l298 300h-198v900h-200v-900h-198zM900 1200l298 -300h-198v-900h-200v900h-198z" />
|
|
||||||
<glyph unicode="" d="M400 300h198l-298 -300l-298 300h198v900h200v-900zM1000 1200v-500h-100v100h-100v-100h-100v500h300zM901 1100h-100v-200h100v200zM700 500h300v-200h-99v-100h-100v100h99v100h-200v100zM800 100h200v-100h-300v200h100v-100z" />
|
|
||||||
<glyph unicode="" d="M400 300h198l-298 -300l-298 300h198v900h200v-900zM1000 1200v-200h-99v-100h-100v100h99v100h-200v100h300zM800 800h200v-100h-300v200h100v-100zM700 500h300v-500h-100v100h-100v-100h-100v500zM801 200h100v200h-100v-200z" />
|
|
||||||
<glyph unicode="" d="M300 0l298 300h-198v900h-200v-900h-198zM900 1100h-100v100h200v-500h-100v400zM1100 500v-500h-100v100h-200v400h300zM1001 400h-100v-200h100v200z" />
|
|
||||||
<glyph unicode="" d="M300 0l298 300h-198v900h-200v-900h-198zM1100 1200v-500h-100v100h-200v400h300zM1001 1100h-100v-200h100v200zM900 400h-100v100h200v-500h-100v400z" />
|
|
||||||
<glyph unicode="" d="M300 0l298 300h-198v900h-200v-900h-198zM900 1000h-200v200h200v-200zM1000 700h-300v200h300v-200zM1100 400h-400v200h400v-200zM1200 100h-500v200h500v-200z" />
|
|
||||||
<glyph unicode="" d="M300 0l298 300h-198v900h-200v-900h-198zM1200 1000h-500v200h500v-200zM1100 700h-400v200h400v-200zM1000 400h-300v200h300v-200zM900 100h-200v200h200v-200z" />
|
|
||||||
<glyph unicode="" d="M400 1100h300q162 0 281 -118.5t119 -281.5v-300q0 -165 -118.5 -282.5t-281.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5v300q0 165 117.5 282.5t282.5 117.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5 t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5z" />
|
|
||||||
<glyph unicode="" d="M700 0h-300q-163 0 -281.5 117.5t-118.5 282.5v300q0 163 119 281.5t281 118.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5 t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5zM400 800v-500l333 250z" />
|
|
||||||
<glyph unicode="" d="M0 400v300q0 163 117.5 281.5t282.5 118.5h300q163 0 281.5 -119t118.5 -281v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM900 300v500q0 41 -29.5 70.5t-70.5 29.5h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5 t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5zM800 700h-500l250 -333z" />
|
|
||||||
<glyph unicode="" d="M1100 700v-300q0 -162 -118.5 -281t-281.5 -119h-300q-165 0 -282.5 118.5t-117.5 281.5v300q0 165 117.5 282.5t282.5 117.5h300q165 0 282.5 -117.5t117.5 -282.5zM900 300v500q0 41 -29.5 70.5t-70.5 29.5h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5 t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5zM550 733l-250 -333h500z" />
|
|
||||||
<glyph unicode="" d="M500 1100h400q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-400v200h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-500v200zM700 550l-400 -350v200h-300v300h300v200z" />
|
|
||||||
<glyph unicode="" d="M403 2l9 -1q13 0 26 16l538 630q15 19 6 36q-8 18 -32 16h-300q1 4 78 219.5t79 227.5q2 17 -6 27l-8 8h-9q-16 0 -25 -15q-4 -5 -98.5 -111.5t-228 -257t-209.5 -238.5q-17 -19 -7 -40q10 -19 32 -19h302q-155 -438 -160 -458q-5 -21 4 -32z" />
|
|
||||||
<glyph unicode="" d="M800 200h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h500v185q-14 4 -114 7.5t-193 5.5l-93 2q-165 0 -282.5 -117.5t-117.5 -282.5v-300q0 -165 117.5 -282.5t282.5 -117.5h300q47 0 100 15v185zM900 200v200h-300v300h300v200l400 -350z" />
|
|
||||||
<glyph unicode="" d="M1200 700l-149 149l-342 -353l-213 213l353 342l-149 149h500v-500zM1022 571l-122 -123v-148q0 -41 -29.5 -70.5t-70.5 -29.5h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h156l118 122l-74 78h-100q-165 0 -282.5 -117.5t-117.5 -282.5v-300 q0 -165 117.5 -282.5t282.5 -117.5h300q163 0 281.5 117.5t118.5 282.5v98z" />
|
|
||||||
<glyph unicode="" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM600 794 q80 0 137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137t57 137t137 57z" />
|
|
||||||
<glyph unicode="" d="M700 800v400h-300v-400h-300l445 -500l450 500h-295zM25 300h1048q11 0 19 -7.5t8 -17.5v-275h-1100v275q0 11 7 18t18 7zM1000 200h-100v-50h100v50z" />
|
|
||||||
<glyph unicode="" d="M400 700v-300h300v300h295l-445 500l-450 -500h300zM25 300h1048q11 0 19 -7.5t8 -17.5v-275h-1100v275q0 11 7 18t18 7zM1000 200h-100v-50h100v50z" />
|
|
||||||
<glyph unicode="" d="M405 400l596 596l-154 155l-442 -442l-150 151l-155 -155zM25 300h1048q11 0 19 -7.5t8 -17.5v-275h-1100v275q0 11 7 18t18 7zM1000 200h-100v-50h100v50z" />
|
|
||||||
<glyph unicode="" d="M409 1103l-97 97l-212 -212l97 -98zM650 861l-149 149l-212 -212l149 -149l-238 -248h700v699zM25 300h1048q11 0 19 -7.5t8 -17.5v-275h-1100v275q0 11 7 18t18 7zM1000 200h-100v-50h100v50z" />
|
|
||||||
<glyph unicode="" d="M539 950l-149 -149l212 -212l149 148l248 -237v700h-699zM297 709l-97 -97l212 -212l98 97zM25 300h1048q11 0 19 -7.5t8 -17.5v-275h-1100v275q0 11 7 18t18 7zM1000 200h-100v-50h100v50z" />
|
|
||||||
<glyph unicode="" d="M1200 1199v-1079l-475 272l-310 -393v416h-392zM1166 1148l-672 -712v-226z" />
|
|
||||||
<glyph unicode="" d="M1100 1000v-850q0 -21 -15 -35.5t-35 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1200h-100v-200h100v200z" />
|
|
||||||
<glyph unicode="" d="M578 500h-378v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-218l-276 -275l-120 120zM700 1200h-100v-200h100v200zM1300 538l-475 -476l-244 244l123 123l120 -120l353 352z" />
|
|
||||||
<glyph unicode="" d="M529 500h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-269l-103 -103l-170 170zM700 1200h-100v-200h100v200zM1167 6l-170 170l-170 -170l-127 127l170 170l-170 170l127 127l170 -170l170 170l127 -128 l-170 -169l170 -170z" />
|
|
||||||
<glyph unicode="" d="M700 500h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-300h-400v-200zM700 1000h-100v200h100v-200zM1000 600h-200v-300h-200l300 -300l300 300h-200v300z" />
|
|
||||||
<glyph unicode="" d="M602 500h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-402l-200 200zM700 1000h-100v200h100v-200zM1000 300h200l-300 300l-300 -300h200v-300h200v300z" />
|
|
||||||
<glyph unicode="" d="M1200 900v150q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-150h1200zM0 800v-550q0 -21 14.5 -35.5t35.5 -14.5h1100q21 0 35.5 14.5t14.5 35.5v550h-1200zM100 500h400v-200h-400v200z" />
|
|
||||||
<glyph unicode="" d="M500 1000h400v198l300 -298l-300 -298v198h-400v200zM100 800v200h100v-200h-100zM400 800h-100v200h100v-200zM700 300h-400v-198l-300 298l300 298v-198h400v-200zM800 500h100v-200h-100v200zM1000 500v-200h100v200h-100z" />
|
|
||||||
<glyph unicode="" d="M1200 50v1106q0 31 -18 40.5t-44 -7.5l-276 -117q-25 -16 -43.5 -50.5t-18.5 -65.5v-359q0 -29 10.5 -55.5t25 -43t29 -28.5t25.5 -18l10 -5v-397q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5zM550 1200l50 -100v-400l-100 -203v-447q0 -21 -14.5 -35.5 t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447l-100 203v400l50 100l50 -100v-300h100v300l50 100l50 -100v-300h100v300z" />
|
|
||||||
<glyph unicode="" d="M1100 106v888q0 22 25 34.5t50 13.5l25 2v56h-400v-56q75 0 87.5 -6t12.5 -44v-394h-500v394q0 38 12.5 44t87.5 6v56h-400v-56q4 0 11 -0.5t24 -3t30 -7t24 -15t11 -24.5v-888q0 -22 -25 -34.5t-50 -13.5l-25 -2v-56h400v56q-75 0 -87.5 6t-12.5 44v394h500v-394 q0 -38 -12.5 -44t-87.5 -6v-56h400v56q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5z" />
|
|
||||||
<glyph unicode="" d="M675 1000l-100 100h-375l-100 -100h400l200 -200v-98l295 98h105v200h-425zM500 300v500q0 41 -29.5 70.5t-70.5 29.5h-300q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h300q41 0 70.5 29.5t29.5 70.5zM100 800h300v-200h-300v200zM700 565l400 133 v-163l-400 -133v163zM100 500h300v-200h-300v200zM805 300l295 98v-298h-425l-100 -100h-375l-100 100h400l200 200h105z" />
|
|
||||||
<glyph unicode="" d="M179 1169l-162 -162q-1 -11 -0.5 -32.5t16 -90t46.5 -140t104 -177.5t175 -208q103 -103 207.5 -176t180 -103.5t137 -47t92.5 -16.5l31 1l163 162q16 17 13 40.5t-22 37.5l-192 136q-19 14 -45 12t-42 -19l-119 -118q-143 103 -267 227q-126 126 -227 268l118 118 q17 17 20 41.5t-11 44.5l-139 194q-14 19 -36.5 22t-40.5 -14z" />
|
|
||||||
<glyph unicode="" d="M1200 712v200q-6 8 -19 20.5t-63 45t-112 57t-171 45t-235 20.5q-92 0 -175 -10.5t-141.5 -27t-108.5 -36.5t-81.5 -40t-53.5 -36.5t-31 -27.5l-9 -10v-200q0 -21 14.5 -33.5t34.5 -8.5l202 33q20 4 34.5 21t14.5 38v146q141 24 300 24t300 -24v-146q0 -21 14.5 -38 t34.5 -21l202 -33q20 -4 34.5 8.5t14.5 33.5zM800 650l365 -303q14 -14 24.5 -39.5t10.5 -45.5v-212q0 -21 -15 -35.5t-35 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v212q0 20 10.5 45.5t24.5 39.5l365 303v50q0 4 1 10.5t12 22.5t30 28.5t60 23t97 10.5t97 -10t60 -23.5 t30 -27.5t12 -24l1 -10v-50z" />
|
|
||||||
<glyph unicode="" d="M175 200h950l-125 150v250l100 100v400h-100v-200h-100v200h-200v-200h-100v200h-200v-200h-100v200h-100v-400l100 -100v-250zM1200 100v-100h-1100v100h1100z" />
|
|
||||||
<glyph unicode="" d="M600 1100h100q41 0 70.5 -29.5t29.5 -70.5v-1000h-300v1000q0 41 29.5 70.5t70.5 29.5zM1000 800h100q41 0 70.5 -29.5t29.5 -70.5v-700h-300v700q0 41 29.5 70.5t70.5 29.5zM400 0v400q0 41 -29.5 70.5t-70.5 29.5h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-400h300z" />
|
|
||||||
<glyph unicode="" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM200 800v-300h200v-100h-200v-100h300v300h-200v100h200v100h-300zM800 800h-200v-500h200v100h100v300h-100 v100zM800 700v-300h-100v300h100z" />
|
|
||||||
<glyph unicode="" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM400 600h-100v200h-100v-500h100v200h100v-200h100v500h-100v-200zM800 800h-200v-500h200v100h100v300h-100 v100zM800 700v-300h-100v300h100z" />
|
|
||||||
<glyph unicode="" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM200 800v-500h300v100h-200v300h200v100h-300zM600 800v-500h300v100h-200v300h200v100h-300z" />
|
|
||||||
<glyph unicode="" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM500 700l-300 -150l300 -150v300zM600 400l300 150l-300 150v-300z" />
|
|
||||||
<glyph unicode="" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM900 800v-500h-700v500h700zM300 400h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130v-300zM800 700h-130 q-38 0 -66.5 -43t-28.5 -108t27 -107t68 -42h130v300z" />
|
|
||||||
<glyph unicode="" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM200 800v-300h200v-100h-200v-100h300v300h-200v100h200v100h-300zM800 300h100v500h-200v-100h100v-400z M601 300h100v100h-100v-100z" />
|
|
||||||
<glyph unicode="" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM300 700v100h-100v-500h300v400h-200zM800 300h100v500h-200v-100h100v-400zM401 400h-100v200h100v-200z M601 300h100v100h-100v-100z" />
|
|
||||||
<glyph unicode="" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM1000 900h-900v-700h900v700zM400 700h-200v100h300v-300h-99v-100h-100v100h99v200zM800 700h-100v100h200v-500h-100v400zM201 400h100v-100 h-100v100zM701 300h-100v100h100v-100z" />
|
|
||||||
<glyph unicode="" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM800 700h-300 v-200h300v-100h-300l-100 100v200l100 100h300v-100z" />
|
|
||||||
<glyph unicode="" d="M596 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM596 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM800 700v-100 h-100v100h-200v-100h200v-100h-200v-100h-100v400h300zM800 400h-100v100h100v-100z" />
|
|
||||||
<glyph unicode="" d="M800 300h128q120 0 205 86t85 208q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5q0 -80 56.5 -137t135.5 -57h222v300h400v-300zM700 200h200l-300 -300 l-300 300h200v300h200v-300z" />
|
|
||||||
<glyph unicode="" d="M600 714l403 -403q94 26 154.5 104t60.5 178q0 121 -85 207.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5q0 -80 56.5 -137t135.5 -57h8zM700 -100h-200v300h-200l300 300 l300 -300h-200v-300z" />
|
|
||||||
<glyph unicode="" d="M700 200h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170l-270 -300h400v-155l-75 -45h350l-75 45v155z" />
|
|
||||||
<glyph unicode="" d="M700 45v306q46 -30 100 -30q74 0 126.5 52.5t52.5 126.5q0 24 -9 55q50 32 79.5 83t29.5 112q0 90 -61.5 155.5t-150.5 71.5q-26 89 -99.5 145.5t-167.5 56.5q-116 0 -197.5 -81.5t-81.5 -197.5q0 -4 1 -12t1 -11q-14 2 -23 2q-74 0 -126.5 -52.5t-52.5 -126.5 q0 -53 28.5 -97t75.5 -65q-4 -16 -4 -38q0 -74 52.5 -126.5t126.5 -52.5q56 0 100 30v-306l-75 -45h350z" />
|
|
||||||
<glyph unicode="💼" d="M800 1000h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5h200q41 0 70.5 -29.5t29.5 -70.5v-100zM500 1000h200v100h-200v-100zM1200 400v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5v200h1200z" />
|
|
||||||
<glyph unicode="📅" d="M1100 900v150q0 21 -14.5 35.5t-35.5 14.5h-150v100h-100v-100h-500v100h-100v-100h-150q-21 0 -35.5 -14.5t-14.5 -35.5v-150h1100zM0 800v-750q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v750h-1100zM100 600h100v-100h-100v100zM300 600h100v-100h-100v100z M500 600h100v-100h-100v100zM700 600h100v-100h-100v100zM900 600h100v-100h-100v100zM100 400h100v-100h-100v100zM300 400h100v-100h-100v100zM500 400h100v-100h-100v100zM700 400h100v-100h-100v100zM900 400h100v-100h-100v100zM100 200h100v-100h-100v100zM300 200 h100v-100h-100v100zM500 200h100v-100h-100v100zM700 200h100v-100h-100v100zM900 200h100v-100h-100v100z" />
|
|
||||||
<glyph unicode="📌" d="M902 1185l283 -282q15 -15 15 -36t-15 -35q-14 -15 -35 -15t-35 15l-36 35l-279 -267v-300l-212 210l-208 -207l-380 -303l303 380l207 208l-210 212h300l267 279l-35 36q-15 14 -15 35t15 35q14 15 35 15t35 -15z" />
|
|
||||||
<glyph unicode="📎" d="M518 119l69 -60l517 511q67 67 95 157t11 183q-16 87 -67 154t-130 103q-69 33 -152 33q-107 0 -197 -55q-40 -24 -111 -95l-512 -512q-68 -68 -81 -163t35 -173q35 -57 94 -89t129 -32q63 0 119 28q33 16 65 40.5t52.5 45.5t59.5 64q40 44 57 61l394 394q35 35 47 84 t-3 96q-27 87 -117 104q-20 2 -29 2q-46 0 -79.5 -17t-67.5 -51l-388 -396l-7 -7l69 -67l377 373q20 22 39 38q23 23 50 23q38 0 53 -36q16 -39 -20 -75l-547 -547q-52 -52 -125 -52q-55 0 -100 33t-54 96q-5 35 2.5 66t31.5 63t42 50t56 54q24 21 44 41l348 348 q52 52 82.5 79.5t84 54t107.5 26.5q25 0 48 -4q95 -17 154 -94.5t51 -175.5q-7 -101 -98 -192l-252 -249l-253 -256z" />
|
|
||||||
<glyph unicode="📷" d="M1200 200v600q0 41 -29.5 70.5t-70.5 29.5h-150q-4 8 -11.5 21.5t-33 48t-53 61t-69 48t-83.5 21.5h-200q-41 0 -82 -20.5t-70 -50t-52 -59t-34 -50.5l-12 -20h-150q-41 0 -70.5 -29.5t-29.5 -70.5v-600q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5z M1000 700h-100v100h100v-100zM844 500q0 -100 -72 -172t-172 -72t-172 72t-72 172t72 172t172 72t172 -72t72 -172zM706 500q0 44 -31 75t-75 31t-75 -31t-31 -75t31 -75t75 -31t75 31t31 75z" />
|
|
||||||
<glyph unicode="🔒" d="M900 800h100q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-900q-41 0 -70.5 29.5t-29.5 70.5v600q0 41 29.5 70.5t70.5 29.5h100v200q0 82 59 141t141 59h300q82 0 141 -59t59 -141v-200zM400 800h300v150q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-150z" />
|
|
||||||
<glyph unicode="🔔" d="M1062 400h17q20 0 33.5 -14.5t13.5 -35.5q0 -20 -13 -40t-31 -27q-22 -9 -63 -23t-167.5 -37t-251.5 -23t-245.5 20.5t-178.5 41.5l-58 20q-18 7 -31 27.5t-13 40.5q0 21 13.5 35.5t33.5 14.5h17l118 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3 32t29 13h94 q20 0 29 -10.5t3 -29.5l-18 -37q83 -19 144 -82.5t76 -140.5l63 -327zM600 104q-54 0 -103 6q12 -49 40 -79.5t63 -30.5t63 30.5t39 79.5q-48 -6 -102 -6z" />
|
|
||||||
<glyph unicode="🔖" d="M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z" />
|
|
||||||
<glyph unicode="🔥" d="M400 755q2 -12 8 -41.5t8 -43t6 -39.5t3.5 -39.5t-1 -33.5t-6 -31.5t-13.5 -24t-21 -20.5t-31 -12q-38 -10 -67 13t-40.5 61.5t-15 81.5t10.5 75q-52 -46 -83.5 -101t-39 -107t-7.5 -85t5 -63q9 -56 44 -119.5t105 -108.5q31 -21 64 -16t62 23.5t57 49.5t48 61.5t35 60.5 q32 66 39 184.5t-13 157.5q79 -80 122 -164t26 -184q-5 -33 -20.5 -69.5t-37.5 -80.5q-10 -19 -14.5 -29t-12 -26t-9 -23.5t-3 -19t2.5 -15.5t11 -9.5t19.5 -5t30.5 2.5t42 8q57 20 91 34t87.5 44.5t87 64t65.5 88.5t47 122q38 172 -44.5 341.5t-246.5 278.5q22 -44 43 -129 q39 -159 -32 -154q-15 2 -33 9q-79 33 -120.5 100t-44 175.5t48.5 257.5q-13 -8 -34 -23.5t-72.5 -66.5t-88.5 -105.5t-60 -138t-8 -166.5z" />
|
|
||||||
<glyph unicode="🔧" d="M948 778l251 126q13 -175 -151 -267q-123 -70 -253 -23l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5t15 37.5l600 599q-33 101 6 201.5t135 154.5q164 92 306 -9l-259 -138z" />
|
|
||||||
</font>
|
|
||||||
</defs></svg>
|
|
||||||
|
Before Width: | Height: | Size: 62 KiB |
BIN
rd_ui/app/google_login.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
rd_ui/app/images/favicon-16x16.png
Executable file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
rd_ui/app/images/favicon-32x32.png
Executable file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
rd_ui/app/images/favicon-96x96.png
Executable file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
rd_ui/app/images/redash_icon_small.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
@@ -4,7 +4,7 @@
|
|||||||
<!--[if IE 8]> <html class="no-js lt-ie9" ng-app="redash" ng-controller='MainCtrl'> <![endif]-->
|
<!--[if IE 8]> <html class="no-js lt-ie9" ng-app="redash" ng-controller='MainCtrl'> <![endif]-->
|
||||||
<!--[if gt IE 8]><!--> <html class="no-js" ng-app="redash" ng-controller='MainCtrl'> <!--<![endif]-->
|
<!--[if gt IE 8]><!--> <html class="no-js" ng-app="redash" ng-controller='MainCtrl'> <!--<![endif]-->
|
||||||
<head>
|
<head>
|
||||||
<title ng-bind="'re:dash: ' + pageTitle"></title>
|
<title ng-bind="'{{name}} | ' + pageTitle"></title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
|
||||||
@@ -12,10 +12,21 @@
|
|||||||
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.css">
|
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.css">
|
||||||
<link rel="stylesheet" href="/bower_components/codemirror/lib/codemirror.css">
|
<link rel="stylesheet" href="/bower_components/codemirror/lib/codemirror.css">
|
||||||
<link rel="stylesheet" href="/bower_components/gridster/dist/jquery.gridster.css">
|
<link rel="stylesheet" href="/bower_components/gridster/dist/jquery.gridster.css">
|
||||||
<link rel="stylesheet" href="/bower_components/pivottable/examples/pivot.css">
|
<link rel="stylesheet" href="/bower_components/pivottable/dist/pivot.css">
|
||||||
<link rel="stylesheet" href="/bower_components/cornelius/src/cornelius.css">
|
<link rel="stylesheet" href="/bower_components/cornelius/src/cornelius.css">
|
||||||
|
<link rel="stylesheet" href="/bower_components/select2/select2.css">
|
||||||
|
<link rel="stylesheet" href="/bower_components/angular-ui-select/dist/select.css">
|
||||||
|
<link rel="stylesheet" href="/bower_components/pace/themes/pace-theme-minimal.css">
|
||||||
|
<link rel="stylesheet" href="/bower_components/font-awesome/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="/bower_components/codemirror/addon/hint/show-hint.css">
|
||||||
|
<link rel="stylesheet" href="/bower_components/leaflet/dist/leaflet.css">
|
||||||
<link rel="stylesheet" href="/styles/redash.css">
|
<link rel="stylesheet" href="/styles/redash.css">
|
||||||
<!-- endbuild -->
|
<!-- endbuild -->
|
||||||
|
|
||||||
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div growl></div>
|
<div growl></div>
|
||||||
@@ -29,38 +40,56 @@
|
|||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
</button>
|
</button>
|
||||||
<a class="navbar-brand" href="/"><strong>re:dash</strong></a>
|
<a class="navbar-brand" href="/"><img src="/images/redash_icon_small.png"/></a>
|
||||||
</div>
|
</div>
|
||||||
|
{% raw %}
|
||||||
<div class="collapse navbar-collapse navbar-ex1-collapse">
|
<div class="collapse navbar-collapse navbar-ex1-collapse">
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
<li class="active" ng-show="pageTitle"><a ng-bind="pageTitle"></a></li>
|
<li class="active" ng-show="pageTitle"><a class="page-title" ng-bind="pageTitle"></a></li>
|
||||||
<li class="dropdown">
|
<li class="dropdown" ng-show="groupedDashboards.length > 0 || otherDashboards.length > 0 || currentUser.hasPermission('create_dashboard')" dropdown>
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="glyphicon glyphicon-th-large"></span> <b class="caret"></b></a>
|
<a href="#" class="dropdown-toggle" dropdown-toggle><span class="glyphicon glyphicon-th-large"></span> <b class="caret"></b></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu" dropdown-menu>
|
||||||
<span ng-repeat="(name, group) in groupedDashboards">
|
<span ng-repeat="(name, group) in groupedDashboards">
|
||||||
<li role="presentation" class="dropdown-header" ng-bind="name"></li>
|
<li class="dropdown-submenu">
|
||||||
<li ng-repeat="dashboard in group" role="presentation">
|
<a href="#" ng-bind="name"></a>
|
||||||
<a role="menu-item" ng-href="/dashboard/{{!dashboard.slug}}" ng-bind="dashboard.name"></a>
|
<ul class="dropdown-menu">
|
||||||
|
<li ng-repeat="dashboard in group" role="presentation">
|
||||||
|
<a role="menu-item" ng-href="/dashboard/{{dashboard.slug}}" ng-bind="dashboard.name"></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="divider"></li>
|
|
||||||
</span>
|
</span>
|
||||||
<li><a data-toggle="modal" href="#new_dashboard_dialog">New Dashboard</a></li>
|
<li ng-repeat="dashboard in otherDashboards">
|
||||||
|
<a role="menu-item" ng-href="/dashboard/{{dashboard.slug}}" ng-bind="dashboard.name"></a>
|
||||||
|
</li>
|
||||||
|
<li class="divider" ng-show="currentUser.hasPermission('create_dashboard') && (groupedDashboards.length > 0 || otherDashboards.length > 0)"></li>
|
||||||
|
<li><a data-toggle="modal" href="#new_dashboard_dialog" ng-show="currentUser.hasPermission('create_dashboard')">New Dashboard</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown">
|
<li class="dropdown" ng-show="currentUser.hasPermission('view_query')" dropdown>
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Queries <b class="caret"></b></a>
|
<a href="#" class="dropdown-toggle" dropdown-toggle>Queries <b class="caret"></b></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu" dropdown-menu>
|
||||||
<li><a href="/queries/new">New Query</a></li>
|
<li ng-show="currentUser.hasPermission('create_query')"><a href="/queries/new">New Query</a></li>
|
||||||
<li><a href="/queries">Queries</a></li>
|
<li><a href="/queries">Queries</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<form class="navbar-form navbar-left" role="search" ng-submit="searchQueries()">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" ng-model="term" class="form-control" placeholder="Search queries...">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search"></span></button>
|
||||||
|
</form>
|
||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
<p class="navbar-text avatar">
|
<p class="navbar-text avatar" ng-show="currentUser.id" ng-cloak>
|
||||||
<img ng-src="{{!currentUser.gravatar_url}}" class="img-circle" alt="{{!currentUser.name}}" width="40" height="40"/>
|
<img ng-src="{{currentUser.gravatar_url}}" class="img-circle" alt="{{currentUser.name}}"/>
|
||||||
|
<a target="_self" href="/logout" id="logout" title="Logout">
|
||||||
|
<span class="glyphicon glyphicon-log-out"></span>
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{% endraw %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -76,41 +105,83 @@
|
|||||||
<script src="/bower_components/bootstrap/js/collapse.js"></script>
|
<script src="/bower_components/bootstrap/js/collapse.js"></script>
|
||||||
<script src="/bower_components/bootstrap/js/modal.js"></script>
|
<script src="/bower_components/bootstrap/js/modal.js"></script>
|
||||||
<script src="/bower_components/angular-resource/angular-resource.js"></script>
|
<script src="/bower_components/angular-resource/angular-resource.js"></script>
|
||||||
|
<script src="/bower_components/angular-route/angular-route.js"></script>
|
||||||
<script src="/bower_components/underscore/underscore.js"></script>
|
<script src="/bower_components/underscore/underscore.js"></script>
|
||||||
<script src="/bower_components/moment/moment.js"></script>
|
<script src="/bower_components/moment/moment.js"></script>
|
||||||
<script src="/bower_components/angular-moment/angular-moment.js"></script>
|
<script src="/bower_components/angular-moment/angular-moment.js"></script>
|
||||||
<script src="/bower_components/codemirror/lib/codemirror.js"></script>
|
<script src="/bower_components/codemirror/lib/codemirror.js"></script>
|
||||||
<script src="/bower_components/codemirror/addon/edit/matchbrackets.js"></script>
|
<script src="/bower_components/codemirror/addon/edit/matchbrackets.js"></script>
|
||||||
<script src="/bower_components/codemirror/addon/edit/closebrackets.js"></script>
|
<script src="/bower_components/codemirror/addon/edit/closebrackets.js"></script>
|
||||||
|
<script src="/bower_components/codemirror/addon/hint/show-hint.js"></script>
|
||||||
|
<script src="/bower_components/codemirror/addon/hint/anyword-hint.js"></script>
|
||||||
<script src="/bower_components/codemirror/mode/sql/sql.js"></script>
|
<script src="/bower_components/codemirror/mode/sql/sql.js"></script>
|
||||||
|
<script src="/bower_components/codemirror/mode/python/python.js"></script>
|
||||||
<script src="/bower_components/codemirror/mode/javascript/javascript.js"></script>
|
<script src="/bower_components/codemirror/mode/javascript/javascript.js"></script>
|
||||||
<script src="/bower_components/angular-ui-codemirror/ui-codemirror.js"></script>
|
|
||||||
<script src="/bower_components/highcharts/highcharts.js"></script>
|
<script src="/bower_components/highcharts/highcharts.js"></script>
|
||||||
<script src="/bower_components/highcharts/modules/exporting.js"></script>
|
<script src="/bower_components/highcharts/modules/exporting.js"></script>
|
||||||
<script src="/scripts/ng-highchart.js"></script>
|
|
||||||
<script src="/scripts/smart-table.js"></script>
|
|
||||||
<script src="/scripts/ui-bootstrap-tpls-0.5.0.min.js"></script>
|
|
||||||
<script src="/bower_components/gridster/dist/jquery.gridster.js"></script>
|
<script src="/bower_components/gridster/dist/jquery.gridster.js"></script>
|
||||||
<script src="/bower_components/angular-growl/build/angular-growl.js"></script>
|
<script src="/bower_components/angular-growl/build/angular-growl.js"></script>
|
||||||
<script src="/bower_components/pivottable/examples/pivot.js"></script>
|
<script src="/bower_components/pivottable/dist/pivot.js"></script>
|
||||||
<script src="/bower_components/cornelius/src/cornelius.js"></script>
|
<script src="/bower_components/cornelius/src/cornelius.js"></script>
|
||||||
|
<script src="/bower_components/mousetrap/mousetrap.js"></script>
|
||||||
|
<script src="/bower_components/mousetrap/plugins/global-bind/mousetrap-global-bind.js"></script>
|
||||||
|
<script src="/bower_components/select2/select2.js"></script>
|
||||||
|
<script src="/bower_components/angular-ui-select2/src/select2.js"></script>
|
||||||
|
<script src="/bower_components/angular-ui-select/dist/select.js"></script>
|
||||||
|
<script src="/bower_components/underscore.string/lib/underscore.string.js"></script>
|
||||||
|
<script src="/bower_components/marked/lib/marked.js"></script>
|
||||||
|
<script src="/scripts/ng_highchart.js"></script>
|
||||||
|
<script src="/scripts/ng_smart_table.js"></script>
|
||||||
|
<script src="/bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.js"></script>
|
||||||
|
<script src="/bower_components/bucky/bucky.js"></script>
|
||||||
|
<script src="/bower_components/pace/pace.js"></script>
|
||||||
|
<script src="/bower_components/mustache/mustache.js"></script>
|
||||||
|
<script src="/bower_components/canvg/rgbcolor.js"></script>
|
||||||
|
<script src="/bower_components/canvg/StackBlur.js"></script>
|
||||||
|
<script src="/bower_components/canvg/canvg.js"></script>
|
||||||
|
<script src="/bower_components/leaflet/dist/leaflet.js"></script>
|
||||||
<!-- endbuild -->
|
<!-- endbuild -->
|
||||||
|
|
||||||
<!-- build:js({.tmp,app}) /scripts/scripts.js -->
|
<!-- build:js({.tmp,app}) /scripts/scripts.js -->
|
||||||
<script src="/scripts/app.js"></script>
|
<script src="/scripts/app.js"></script>
|
||||||
<script src="/scripts/controllers.js"></script>
|
<script src="/scripts/services/services.js"></script>
|
||||||
<script src="/scripts/admin_controllers.js"></script>
|
<script src="/scripts/services/resources.js"></script>
|
||||||
<script src="/scripts/directives.js"></script>
|
|
||||||
<script src="/scripts/services.js"></script>
|
|
||||||
<script src="/scripts/filters.js"></script>
|
|
||||||
<script src="/scripts/services/notifications.js"></script>
|
<script src="/scripts/services/notifications.js"></script>
|
||||||
<script src="/scripts/services/dashboards.js"></script>
|
<script src="/scripts/services/dashboards.js"></script>
|
||||||
<script src="/scripts/query_fiddle/renderers.js"></script>
|
<script src="/scripts/controllers/controllers.js"></script>
|
||||||
|
<script src="/scripts/controllers/dashboard.js"></script>
|
||||||
|
<script src="/scripts/controllers/admin_controllers.js"></script>
|
||||||
|
<script src="/scripts/controllers/query_view.js"></script>
|
||||||
|
<script src="/scripts/controllers/query_source.js"></script>
|
||||||
|
<script src="/scripts/visualizations/base.js"></script>
|
||||||
|
<script src="/scripts/visualizations/chart.js"></script>
|
||||||
|
<script src="/scripts/visualizations/cohort.js"></script>
|
||||||
|
<script src="/scripts/visualizations/map.js"></script>
|
||||||
|
<script src="/scripts/visualizations/counter.js"></script>
|
||||||
|
<script src="/scripts/visualizations/table.js"></script>
|
||||||
|
<script src="/scripts/visualizations/pivot.js"></script>
|
||||||
|
<script src="/scripts/directives/directives.js"></script>
|
||||||
|
<script src="/scripts/directives/query_directives.js"></script>
|
||||||
|
<script src="/scripts/directives/dashboard_directives.js"></script>
|
||||||
|
<script src="/scripts/filters.js"></script>
|
||||||
<!-- endbuild -->
|
<!-- endbuild -->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var currentUser = {% raw user %};
|
// TODO: move currentUser & features to be an Angular service
|
||||||
|
var featureFlags = {{ features|safe }};
|
||||||
|
var currentUser = {{ user|safe }};
|
||||||
|
|
||||||
|
currentUser.canEdit = function(object) {
|
||||||
|
var user_id = object.user_id || (object.user && object.user.id);
|
||||||
|
return user_id && (user_id == currentUser.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
currentUser.hasPermission = function(permission) {
|
||||||
|
return this.permissions.indexOf(permission) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
{{ analytics|safe }}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
98
rd_ui/app/login.html
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
|
||||||
|
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
|
||||||
|
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
|
||||||
|
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
|
||||||
|
<head>
|
||||||
|
<title>{{name}} Login</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
|
||||||
|
<!-- build:css /styles/main_login.css -->
|
||||||
|
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.css">
|
||||||
|
<link rel="stylesheet" href="/styles/redash.css">
|
||||||
|
<link rel="stylesheet" href="/styles/login.css">
|
||||||
|
<!-- endbuild -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
||||||
|
<div class="container">
|
||||||
|
<div class="navbar-header">
|
||||||
|
<button type="button" class="navbar-toggle" data-toggle="collapse"
|
||||||
|
data-target=".navbar-ex1-collapse">
|
||||||
|
<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="/"><strong>{{name}}</strong></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
{% if show_google_openid %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<a href="/oauth/google?next={{next}}"><img src="/google_login.png" class="login-button"/></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-or">
|
||||||
|
<hr class="hr-or">
|
||||||
|
<span class="span-or">or</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if show_saml_login %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<a href="/saml/login">SAML Login</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-or">
|
||||||
|
<hr class="hr-or">
|
||||||
|
<span class="span-or">or</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form role="form" method="post" name="login">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="inputUsernameEmail">Username or email</label>
|
||||||
|
<input type="text" class="form-control" id="inputUsernameEmail" name="username" value="{{username}}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<!--<a class="pull-right" href="#">Forgot password?</a>-->
|
||||||
|
<label for="inputPassword">Password</label>
|
||||||
|
<input type="password" class="form-control" id="inputPassword" name="password">
|
||||||
|
</div>
|
||||||
|
<div class="checkbox pull-right">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="remember">
|
||||||
|
Remember me </label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn btn-primary">
|
||||||
|
Log In
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/bower_components/jquery/jquery.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
{{ analytics|safe }}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
(function () {
|
|
||||||
var AdminStatusCtrl = function ($scope, $http, $timeout) {
|
|
||||||
$scope.$parent.pageTitle = "System Status";
|
|
||||||
|
|
||||||
var refresh = function () {
|
|
||||||
$scope.refresh_time = moment().add('minutes', 1);
|
|
||||||
$http.get('/status.json').success(function (data) {
|
|
||||||
$scope.status = data;
|
|
||||||
});
|
|
||||||
|
|
||||||
$timeout(refresh, 59 * 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
angular.module('redash.admin_controllers', [])
|
|
||||||
.controller('AdminStatusCtrl', ['$scope', '$http', '$timeout', AdminStatusCtrl])
|
|
||||||
})();
|
|
||||||
@@ -1,19 +1,102 @@
|
|||||||
angular.module('redash', ['redash.directives', 'redash.admin_controllers', 'redash.controllers', 'redash.filters', 'redash.services',
|
angular.module('redash', [
|
||||||
'redash.renderers',
|
'redash.directives',
|
||||||
'ui.codemirror', 'highchart', 'angular-growl', 'angularMoment', 'ui.bootstrap', 'smartTable.table', 'ngResource']).
|
'redash.admin_controllers',
|
||||||
config(['$routeProvider', '$locationProvider', '$compileProvider', function ($routeProvider, $locationProvider, $compileProvider) {
|
'redash.controllers',
|
||||||
|
'redash.filters',
|
||||||
|
'redash.services',
|
||||||
|
'redash.renderers',
|
||||||
|
'redash.visualization',
|
||||||
|
'highchart',
|
||||||
|
'ui.select2',
|
||||||
|
'angular-growl',
|
||||||
|
'angularMoment',
|
||||||
|
'ui.bootstrap',
|
||||||
|
'smartTable.table',
|
||||||
|
'ngResource',
|
||||||
|
'ngRoute',
|
||||||
|
'ui.select'
|
||||||
|
]).config(['$routeProvider', '$locationProvider', '$compileProvider', 'growlProvider',
|
||||||
|
function ($routeProvider, $locationProvider, $compileProvider, growlProvider) {
|
||||||
|
if (featureFlags.clientSideMetrics) {
|
||||||
|
Bucky.setOptions({
|
||||||
|
host: '/api/metrics'
|
||||||
|
});
|
||||||
|
|
||||||
$compileProvider.urlSanitizationWhitelist(/^\s*(https?|http|data):/);
|
Bucky.requests.monitor('ajax_requsts');
|
||||||
|
Bucky.requests.transforms.enable('dashboards', /dashboard\/[\w-]+/ig, '/dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
$locationProvider.html5Mode(true);
|
function getQuery(Query, $route) {
|
||||||
$routeProvider.when('/dashboard/:dashboardSlug', {templateUrl: '/views/dashboard.html', controller: 'DashboardCtrl'});
|
var query = Query.get({'id': $route.current.params.queryId });
|
||||||
$routeProvider.when('/queries', {templateUrl: '/views/queries.html', controller: 'QueriesCtrl', reloadOnSearch: false});
|
return query.$promise;
|
||||||
$routeProvider.when('/queries/new', {templateUrl: '/views/queryfiddle.html', controller: 'QueryFiddleCtrl', reloadOnSearch: false});
|
};
|
||||||
$routeProvider.when('/queries/:queryId', {templateUrl: '/views/queryfiddle.html', controller: 'QueryFiddleCtrl', reloadOnSearch: false});
|
|
||||||
$routeProvider.when('/admin/status', {templateUrl: '/views/admin_status.html', controller: 'AdminStatusCtrl'});
|
|
||||||
$routeProvider.when('/', {templateUrl: '/views/index.html', controller: 'IndexCtrl'});
|
|
||||||
$routeProvider.otherwise({redirectTo: '/'});
|
|
||||||
|
|
||||||
Highcharts.setOptions({colors: ["#4572A7", "#AA4643", "#89A54E", "#80699B", "#3D96AE", "#DB843D", "#92A8CD", "#A47D7C", "#B5CA92"]});
|
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|http|data):/);
|
||||||
}]);
|
$locationProvider.html5Mode(true);
|
||||||
|
growlProvider.globalTimeToLive(2000);
|
||||||
|
|
||||||
|
$routeProvider.when('/dashboard/:dashboardSlug', {
|
||||||
|
templateUrl: '/views/dashboard.html',
|
||||||
|
controller: 'DashboardCtrl',
|
||||||
|
reloadOnSearch: false
|
||||||
|
});
|
||||||
|
$routeProvider.when('/queries', {
|
||||||
|
templateUrl: '/views/queries.html',
|
||||||
|
controller: 'QueriesCtrl',
|
||||||
|
reloadOnSearch: false
|
||||||
|
});
|
||||||
|
$routeProvider.when('/queries/new', {
|
||||||
|
templateUrl: '/views/query.html',
|
||||||
|
controller: 'QuerySourceCtrl',
|
||||||
|
reloadOnSearch: false,
|
||||||
|
resolve: {
|
||||||
|
'query': ['Query', function newQuery(Query) {
|
||||||
|
return Query.newQuery();
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$routeProvider.when('/queries/search', {
|
||||||
|
templateUrl: '/views/queries_search_results.html',
|
||||||
|
controller: 'QuerySearchCtrl',
|
||||||
|
reloadOnSearch: true,
|
||||||
|
});
|
||||||
|
$routeProvider.when('/queries/:queryId', {
|
||||||
|
templateUrl: '/views/query.html',
|
||||||
|
controller: 'QueryViewCtrl',
|
||||||
|
reloadOnSearch: false,
|
||||||
|
resolve: {
|
||||||
|
'query': ['Query', '$route', getQuery]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$routeProvider.when('/queries/:queryId/source', {
|
||||||
|
templateUrl: '/views/query.html',
|
||||||
|
controller: 'QuerySourceCtrl',
|
||||||
|
reloadOnSearch: false,
|
||||||
|
resolve: {
|
||||||
|
'query': ['Query', '$route', getQuery]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$routeProvider.when('/admin/status', {
|
||||||
|
templateUrl: '/views/admin_status.html',
|
||||||
|
controller: 'AdminStatusCtrl'
|
||||||
|
});
|
||||||
|
$routeProvider.when('/admin/workers', {
|
||||||
|
templateUrl: '/views/admin_workers.html',
|
||||||
|
controller: 'AdminWorkersCtrl'
|
||||||
|
});
|
||||||
|
|
||||||
|
$routeProvider.when('/', {
|
||||||
|
templateUrl: '/views/index.html',
|
||||||
|
controller: 'IndexCtrl'
|
||||||
|
});
|
||||||
|
$routeProvider.when('/personal', {
|
||||||
|
templateUrl: '/views/personal.html',
|
||||||
|
controller: 'PersonalIndexCtrl'
|
||||||
|
});
|
||||||
|
$routeProvider.otherwise({
|
||||||
|
redirectTo: '/'
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|||||||
@@ -1,235 +0,0 @@
|
|||||||
(function () {
|
|
||||||
var DashboardCtrl = function ($scope, $routeParams, $http, Dashboard) {
|
|
||||||
$scope.dashboard = Dashboard.get({slug: $routeParams.dashboardSlug}, function(dashboard) {
|
|
||||||
$scope.$parent.pageTitle = dashboard.name;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var WidgetCtrl = function ($scope, $http, Query) {
|
|
||||||
$scope.deleteWidget = function() {
|
|
||||||
if (!confirm('Are you sure you want to remove "' + $scope.widget.query.name + '" from the dashboard?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$http.delete('/api/widgets/' + $scope.widget.id).success(function() {
|
|
||||||
$scope.dashboard.widgets = _.map($scope.dashboard.widgets, function(row) {
|
|
||||||
return _.filter(row, function(widget) {
|
|
||||||
return widget.id != $scope.widget.id;
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.query = new Query($scope.widget.query);
|
|
||||||
$scope.queryResult = $scope.query.getQueryResult();
|
|
||||||
|
|
||||||
$scope.updateTime = (new Date($scope.queryResult.getUpdatedAt())).toISOString();
|
|
||||||
$scope.nextUpdateTime = moment(new Date(($scope.query.updated_at + $scope.query.ttl + $scope.query.runtime + 300) * 1000)).fromNow();
|
|
||||||
|
|
||||||
$scope.updateTime = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
var QueryFiddleCtrl = function ($scope, $routeParams, $http, $location, growl, Query) {
|
|
||||||
$scope.$parent.pageTitle = "Query Fiddle";
|
|
||||||
|
|
||||||
$scope.tabs = [{'key': 'table', 'name': 'Table'}, {'key': 'chart', 'name': 'Chart'},
|
|
||||||
{'key': 'pivot', 'name': 'Pivot Table'}, {'key': 'cohort', 'name': 'Cohort'}];
|
|
||||||
|
|
||||||
$scope.lockButton = function (lock) {
|
|
||||||
$scope.queryExecuting = lock;
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.formatQuery = function() {
|
|
||||||
$scope.editorOptions.readOnly = 'nocursor';
|
|
||||||
|
|
||||||
$http.post('/api/queries/format', {'query': $scope.query.query}).success(function(response) {
|
|
||||||
$scope.query.query = response;
|
|
||||||
$scope.editorOptions.readOnly = false;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.saveQuery = function (duplicate, oldId) {
|
|
||||||
if (!oldId) {
|
|
||||||
oldId = $scope.query.id;
|
|
||||||
}
|
|
||||||
delete $scope.query.latest_query_data;
|
|
||||||
$scope.query.$save(function (q) {
|
|
||||||
if (duplicate) {
|
|
||||||
growl.addInfoMessage("Query duplicated.", {ttl: 2000});
|
|
||||||
} else{
|
|
||||||
growl.addSuccessMessage("Query saved.", {ttl: 2000});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldId != q.id) {
|
|
||||||
if (oldId == undefined) {
|
|
||||||
$location.path($location.path().replace('new', q.id)).replace();
|
|
||||||
} else {
|
|
||||||
// TODO: replace this with a safer method
|
|
||||||
$location.path($location.path().replace(oldId, q.id)).replace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.duplicateQuery = function () {
|
|
||||||
var oldId = $scope.query.id;
|
|
||||||
$scope.query.id = null;
|
|
||||||
$scope.query.ttl = -1;
|
|
||||||
|
|
||||||
$scope.saveQuery(true, oldId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Query Editor:
|
|
||||||
$scope.editorOptions = {
|
|
||||||
mode: 'text/x-sql',
|
|
||||||
lineWrapping: true,
|
|
||||||
lineNumbers: true,
|
|
||||||
readOnly: false,
|
|
||||||
matchBrackets: true,
|
|
||||||
autoCloseBrackets: true
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.refreshOptions = [
|
|
||||||
{value: -1, name: 'No Refresh'},
|
|
||||||
]
|
|
||||||
|
|
||||||
_.each(_.range(1, 13), function(i) {
|
|
||||||
$scope.refreshOptions.push({value: i*3600, name: 'Every ' + i + 'h'});
|
|
||||||
})
|
|
||||||
|
|
||||||
$scope.refreshOptions.push({value: 24*3600, name: 'Every 24h'});
|
|
||||||
|
|
||||||
|
|
||||||
$scope.$watch('queryResult && queryResult.getError()', function (newError, oldError) {
|
|
||||||
if (newError == undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldError == undefined && newError != undefined) {
|
|
||||||
$scope.lockButton(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.$watch('queryResult && queryResult.getData()', function (data, oldData) {
|
|
||||||
if (!data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($scope.queryResult.getId() == null) {
|
|
||||||
$scope.dataUri = "";
|
|
||||||
} else {
|
|
||||||
$scope.dataUri = '/api/query_results/' + $scope.queryResult.getId() + '.csv';
|
|
||||||
$scope.dataFilename = $scope.query.name.replace(" ", "_") + moment($scope.queryResult.getUpdatedAt()).format("_YYYY_MM_DD") + ".csv";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.$watch("queryResult && queryResult.getStatus()", function (status) {
|
|
||||||
if (!status) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status == "done") {
|
|
||||||
if ($scope.query.id && $scope.query.latest_query_data_id != $scope.queryResult.getId() &&
|
|
||||||
$scope.query.query_hash == $scope.queryResult.query_result.query_hash) {
|
|
||||||
Query.save({'id': $scope.query.id, 'latest_query_data_id': $scope.queryResult.getId()})
|
|
||||||
}
|
|
||||||
$scope.query.latest_query_data_id = $scope.queryResult.getId();
|
|
||||||
$scope.lockButton(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if ($routeParams.queryId != undefined) {
|
|
||||||
$scope.query = Query.get({id: $routeParams.queryId}, function() {
|
|
||||||
$scope.queryResult = $scope.query.getQueryResult();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$scope.query = new Query({query: "", name: "New Query", ttl: -1, user: currentUser.name});
|
|
||||||
$scope.lockButton(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.$watch('query.name', function() {
|
|
||||||
$scope.$parent.pageTitle = $scope.query.name;
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.executeQuery = function() {
|
|
||||||
$scope.queryResult = $scope.query.getQueryResult(0);
|
|
||||||
$scope.lockButton(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var QueriesCtrl = function($scope, $http, $location, Query) {
|
|
||||||
$scope.$parent.pageTitle = "All Queries";
|
|
||||||
|
|
||||||
$scope.queries = Query.query();
|
|
||||||
$scope.tabs = [{"name": "My Queries", "key": "my"}, {"key": "all", "name": "All Queries"}, {"key": "drafts", "name": "Drafts"}];
|
|
||||||
|
|
||||||
$scope.$watch('selectedTab', function(tab) {
|
|
||||||
if (tab) {
|
|
||||||
$scope.$parent.pageTitle = tab.name;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
$scope.filterQueries = function(query) {
|
|
||||||
if (!$scope.selectedTab) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($scope.selectedTab.key == 'my') {
|
|
||||||
return query.user == currentUser.name && query.name != 'New Query';
|
|
||||||
} else if ($scope.selectedTab.key == 'drafts') {
|
|
||||||
return query.user == currentUser.name && query.name == 'New Query';
|
|
||||||
}
|
|
||||||
|
|
||||||
return query.name != 'New Query';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var MainCtrl = function ($scope, Dashboard, notifications) {
|
|
||||||
$scope.dashboards = [];
|
|
||||||
$scope.reloadDashboards = function() {
|
|
||||||
Dashboard.query(function (dashboards) {
|
|
||||||
$scope.dashboards = _.sortBy(dashboards, "name");
|
|
||||||
$scope.groupedDashboards = _.groupBy($scope.dashboards, function(d) {
|
|
||||||
parts = d.name.split(":");
|
|
||||||
if (parts.length == 1) {
|
|
||||||
return "Other";
|
|
||||||
}
|
|
||||||
return parts[0];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.reloadDashboards();
|
|
||||||
|
|
||||||
$scope.currentUser = currentUser;
|
|
||||||
$scope.newDashboard = {
|
|
||||||
'name': null,
|
|
||||||
'layout': null
|
|
||||||
}
|
|
||||||
|
|
||||||
$(window).click(function () {
|
|
||||||
notifications.getPermissions();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var IndexCtrl = function($scope, Dashboard) {
|
|
||||||
$scope.$parent.pageTitle = "Home";
|
|
||||||
|
|
||||||
|
|
||||||
$scope.archiveDashboard = function(dashboard) {
|
|
||||||
if (confirm('Are you sure you want to delete "' + dashboard.name + '" dashboard?')) {
|
|
||||||
dashboard.$delete(function() {
|
|
||||||
$scope.$parent.reloadDashboards();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
angular.module('redash.controllers', [])
|
|
||||||
.controller('DashboardCtrl', ['$scope', '$routeParams', '$http', 'Dashboard', DashboardCtrl])
|
|
||||||
.controller('WidgetCtrl', ['$scope', '$http', 'Query', WidgetCtrl])
|
|
||||||
.controller('QueriesCtrl', ['$scope', '$http', '$location', 'Query', QueriesCtrl])
|
|
||||||
.controller('QueryFiddleCtrl', ['$scope', '$routeParams', '$http', '$location', 'growl', 'Query', QueryFiddleCtrl])
|
|
||||||
.controller('IndexCtrl', ['$scope', 'Dashboard', IndexCtrl])
|
|
||||||
.controller('MainCtrl', ['$scope', 'Dashboard', 'notifications', MainCtrl]);
|
|
||||||
})();
|
|
||||||
24
rd_ui/app/scripts/controllers/admin_controllers.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
(function () {
|
||||||
|
var AdminStatusCtrl = function ($scope, Events, $http, $timeout) {
|
||||||
|
Events.record(currentUser, "view", "page", "admin/status");
|
||||||
|
$scope.$parent.pageTitle = "System Status";
|
||||||
|
|
||||||
|
var refresh = function () {
|
||||||
|
$scope.refresh_time = moment().add('minutes', 1);
|
||||||
|
$http.get('/status.json').success(function (data) {
|
||||||
|
$scope.workers = data.workers;
|
||||||
|
delete data.workers;
|
||||||
|
$scope.manager = data.manager;
|
||||||
|
delete data.manager;
|
||||||
|
$scope.status = data;
|
||||||
|
});
|
||||||
|
|
||||||
|
$timeout(refresh, 59 * 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('redash.admin_controllers', [])
|
||||||
|
.controller('AdminStatusCtrl', ['$scope', 'Events', '$http', '$timeout', AdminStatusCtrl])
|
||||||
|
})();
|
||||||
233
rd_ui/app/scripts/controllers/controllers.js
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
(function () {
|
||||||
|
var dateFormatter = function (value) {
|
||||||
|
if (!value) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return value.toDate().toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
var QuerySearchCtrl = function($scope, $location, $filter, Events, Query) {
|
||||||
|
$scope.$parent.pageTitle = "Queries Search";
|
||||||
|
|
||||||
|
$scope.gridConfig = {
|
||||||
|
isPaginationEnabled: true,
|
||||||
|
itemsByPage: 50,
|
||||||
|
maxSize: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.gridColumns = [
|
||||||
|
{
|
||||||
|
"label": "Name",
|
||||||
|
"map": "name",
|
||||||
|
"cellTemplateUrl": "/views/queries_query_name_cell.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Created By',
|
||||||
|
'map': 'user_name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Created At',
|
||||||
|
'map': 'created_at',
|
||||||
|
'formatFunction': dateFormatter
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Update Schedule',
|
||||||
|
'map': 'schedule',
|
||||||
|
'formatFunction': function (value) {
|
||||||
|
return $filter('scheduleHumanize')(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
$scope.queries = [];
|
||||||
|
$scope.$parent.term = $location.search().q;
|
||||||
|
|
||||||
|
Query.search({q: $scope.term }, function(results) {
|
||||||
|
$scope.queries = _.map(results, function(query) {
|
||||||
|
query.created_at = moment(query.created_at);
|
||||||
|
query.user_name = query.user.name;
|
||||||
|
return query;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.search = function() {
|
||||||
|
if (!angular.isString($scope.term) || $scope.term.trim() == "") {
|
||||||
|
$scope.queries = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$location.search({q: $scope.term});
|
||||||
|
};
|
||||||
|
|
||||||
|
Events.record(currentUser, "search", "query", "", {"term": $scope.term});
|
||||||
|
};
|
||||||
|
|
||||||
|
var QueriesCtrl = function ($scope, $http, $location, $filter, Query) {
|
||||||
|
$scope.$parent.pageTitle = "All Queries";
|
||||||
|
$scope.gridConfig = {
|
||||||
|
isPaginationEnabled: true,
|
||||||
|
itemsByPage: 50,
|
||||||
|
maxSize: 8,
|
||||||
|
isGlobalSearchActivated: true};
|
||||||
|
|
||||||
|
$scope.allQueries = [];
|
||||||
|
$scope.queries = [];
|
||||||
|
|
||||||
|
var filterQueries = function () {
|
||||||
|
$scope.queries = _.filter($scope.allQueries, function (query) {
|
||||||
|
if (!$scope.selectedTab) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scope.selectedTab.key == 'my') {
|
||||||
|
return query.user.id == currentUser.id && query.name != 'New Query';
|
||||||
|
} else if ($scope.selectedTab.key == 'drafts') {
|
||||||
|
return query.user.id == currentUser.id && query.name == 'New Query';
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.name != 'New Query';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Query.query(function (queries) {
|
||||||
|
$scope.allQueries = _.map(queries, function (query) {
|
||||||
|
query.created_at = moment(query.created_at);
|
||||||
|
query.retrieved_at = moment(query.retrieved_at);
|
||||||
|
query.user_name = query.user.name;
|
||||||
|
return query;
|
||||||
|
});
|
||||||
|
|
||||||
|
filterQueries();
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.gridColumns = [
|
||||||
|
{
|
||||||
|
"label": "Name",
|
||||||
|
"map": "name",
|
||||||
|
"cellTemplateUrl": "/views/queries_query_name_cell.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Created By',
|
||||||
|
'map': 'user_name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Created At',
|
||||||
|
'map': 'created_at',
|
||||||
|
'formatFunction': dateFormatter
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Runtime',
|
||||||
|
'map': 'runtime',
|
||||||
|
'formatFunction': function (value) {
|
||||||
|
return $filter('durationHumanize')(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Last Executed At',
|
||||||
|
'map': 'retrieved_at',
|
||||||
|
'formatFunction': dateFormatter
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Update Schedule',
|
||||||
|
'map': 'schedule',
|
||||||
|
'formatFunction': function (value) {
|
||||||
|
return $filter('scheduleHumanize')(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
$scope.tabs = [
|
||||||
|
{"name": "My Queries", "key": "my"},
|
||||||
|
{"key": "all", "name": "All Queries"},
|
||||||
|
{"key": "drafts", "name": "Drafts"}
|
||||||
|
];
|
||||||
|
|
||||||
|
$scope.$watch('selectedTab', function (tab) {
|
||||||
|
if (tab) {
|
||||||
|
$scope.$parent.pageTitle = tab.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
filterQueries();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var MainCtrl = function ($scope, $location, Dashboard, notifications) {
|
||||||
|
if (featureFlags.clientSideMetrics) {
|
||||||
|
$scope.$on('$locationChangeSuccess', function(event, newLocation, oldLocation) {
|
||||||
|
// This will be called once per actual page load.
|
||||||
|
Bucky.sendPagePerformance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$scope.dashboards = [];
|
||||||
|
$scope.reloadDashboards = function () {
|
||||||
|
Dashboard.query(function (dashboards) {
|
||||||
|
$scope.dashboards = _.sortBy(dashboards, "name");
|
||||||
|
$scope.allDashboards = _.groupBy($scope.dashboards, function (d) {
|
||||||
|
parts = d.name.split(":");
|
||||||
|
if (parts.length == 1) {
|
||||||
|
return "Other";
|
||||||
|
}
|
||||||
|
return parts[0];
|
||||||
|
});
|
||||||
|
$scope.otherDashboards = $scope.allDashboards['Other'] || [];
|
||||||
|
$scope.groupedDashboards = _.omit($scope.allDashboards, 'Other');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.searchQueries = function() {
|
||||||
|
$location.path('/queries/search').search({q: $scope.term});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.reloadDashboards();
|
||||||
|
|
||||||
|
$scope.currentUser = currentUser;
|
||||||
|
$scope.newDashboard = {
|
||||||
|
'name': null,
|
||||||
|
'layout': null
|
||||||
|
}
|
||||||
|
|
||||||
|
$(window).click(function () {
|
||||||
|
notifications.getPermissions();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var IndexCtrl = function ($scope, Events, Dashboard) {
|
||||||
|
Events.record(currentUser, "view", "page", "homepage");
|
||||||
|
$scope.$parent.pageTitle = "Home";
|
||||||
|
|
||||||
|
$scope.archiveDashboard = function (dashboard) {
|
||||||
|
if (confirm('Are you sure you want to delete "' + dashboard.name + '" dashboard?')) {
|
||||||
|
Events.record(currentUser, "archive", "dashboard", dashboard.id);
|
||||||
|
dashboard.$delete(function () {
|
||||||
|
$scope.$parent.reloadDashboards();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var PersonalIndexCtrl = function ($scope, Events, Dashboard, Query) {
|
||||||
|
Events.record(currentUser, "view", "page", "personal_homepage");
|
||||||
|
$scope.$parent.pageTitle = "Home";
|
||||||
|
|
||||||
|
$scope.recentQueries = Query.recent();
|
||||||
|
$scope.recentDashboards = Dashboard.recent();
|
||||||
|
|
||||||
|
$scope.archiveDashboard = function (dashboard) {
|
||||||
|
if (confirm('Are you sure you want to delete "' + dashboard.name + '" dashboard?')) {
|
||||||
|
Events.record(currentUser, "archive", "dashboard", dashboard.id);
|
||||||
|
dashboard.$delete(function () {
|
||||||
|
$scope.$parent.reloadDashboards();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
angular.module('redash.controllers', [])
|
||||||
|
.controller('QueriesCtrl', ['$scope', '$http', '$location', '$filter', 'Query', QueriesCtrl])
|
||||||
|
.controller('IndexCtrl', ['$scope', 'Events', 'Dashboard', IndexCtrl])
|
||||||
|
.controller('PersonalIndexCtrl', ['$scope', 'Events', 'Dashboard', 'Query', PersonalIndexCtrl])
|
||||||
|
.controller('MainCtrl', ['$scope', '$location', 'Dashboard', 'notifications', MainCtrl])
|
||||||
|
.controller('QuerySearchCtrl', ['$scope', '$location', '$filter', 'Events', 'Query', QuerySearchCtrl]);
|
||||||
|
})();
|
||||||
156
rd_ui/app/scripts/controllers/dashboard.js
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
(function() {
|
||||||
|
var DashboardCtrl = function($scope, Events, Widget, $routeParams, $location, $http, $timeout, $q, Dashboard) {
|
||||||
|
$scope.refreshEnabled = false;
|
||||||
|
$scope.refreshRate = 60;
|
||||||
|
|
||||||
|
var loadDashboard = _.throttle(function() {
|
||||||
|
$scope.dashboard = Dashboard.get({ slug: $routeParams.dashboardSlug }, function (dashboard) {
|
||||||
|
Events.record(currentUser, "view", "dashboard", dashboard.id);
|
||||||
|
|
||||||
|
$scope.$parent.pageTitle = dashboard.name;
|
||||||
|
|
||||||
|
var promises = [];
|
||||||
|
|
||||||
|
$scope.dashboard.widgets = _.map($scope.dashboard.widgets, function (row) {
|
||||||
|
return _.map(row, function (widget) {
|
||||||
|
var w = new Widget(widget);
|
||||||
|
|
||||||
|
if (w.visualization) {
|
||||||
|
promises.push(w.getQuery().getQueryResult().toPromise());
|
||||||
|
}
|
||||||
|
|
||||||
|
return w;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$q.all(promises).then(function(queryResults) {
|
||||||
|
var filters = {};
|
||||||
|
_.each(queryResults, function(queryResult) {
|
||||||
|
var queryFilters = queryResult.getFilters();
|
||||||
|
_.each(queryFilters, function (queryFilter) {
|
||||||
|
var hasQueryStringValue = _.has($location.search(), queryFilter.name);
|
||||||
|
|
||||||
|
if (!(hasQueryStringValue || dashboard.dashboard_filters_enabled)) {
|
||||||
|
// If dashboard filters not enabled, or no query string value given, skip filters linking.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.has(filters, queryFilter.name)) {
|
||||||
|
var filter = _.extend({}, queryFilter);
|
||||||
|
filters[filter.name] = filter;
|
||||||
|
filters[filter.name].originFilters = [];
|
||||||
|
if (hasQueryStringValue) {
|
||||||
|
filter.current = $location.search()[filter.name];
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.$watch(function () { return filter.current }, function (value) {
|
||||||
|
_.each(filter.originFilters, function (originFilter) {
|
||||||
|
originFilter.current = value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: merge values.
|
||||||
|
filters[queryFilter.name].originFilters.push(queryFilter);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.filters = _.values(filters);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}, function () {
|
||||||
|
// error...
|
||||||
|
// try again. we wrap loadDashboard with throttle so it doesn't happen too often.\
|
||||||
|
// we might want to consider exponential backoff and also move this as a general solution in $http/$resource for
|
||||||
|
// all AJAX calls.
|
||||||
|
loadDashboard();
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
loadDashboard();
|
||||||
|
|
||||||
|
var autoRefresh = function() {
|
||||||
|
if ($scope.refreshEnabled) {
|
||||||
|
$timeout(function() {
|
||||||
|
Dashboard.get({
|
||||||
|
slug: $routeParams.dashboardSlug
|
||||||
|
}, function(dashboard) {
|
||||||
|
var newWidgets = _.groupBy(_.flatten(dashboard.widgets), 'id');
|
||||||
|
|
||||||
|
_.each($scope.dashboard.widgets, function(row) {
|
||||||
|
_.each(row, function(widget, i) {
|
||||||
|
var newWidget = newWidgets[widget.id];
|
||||||
|
if (newWidget && newWidget[0].visualization.query.latest_query_data_id != widget.visualization.query.latest_query_data_id) {
|
||||||
|
row[i] = new Widget(newWidget[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
autoRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
}, $scope.refreshRate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.triggerRefresh = function() {
|
||||||
|
$scope.refreshEnabled = !$scope.refreshEnabled;
|
||||||
|
|
||||||
|
Events.record(currentUser, "autorefresh", "dashboard", dashboard.id, {'enable': $scope.refreshEnabled});
|
||||||
|
|
||||||
|
if ($scope.refreshEnabled) {
|
||||||
|
var refreshRate = _.min(_.map(_.flatten($scope.dashboard.widgets), function(widget) {
|
||||||
|
var schedule = widget.visualization.query.schedule;
|
||||||
|
if (schedule === null || schedule.match(/\d\d:\d\d/) !== null) {
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
return widget.visualization.query.schedule;
|
||||||
|
}));
|
||||||
|
|
||||||
|
$scope.refreshRate = _.max([120, refreshRate * 2]) * 1000;
|
||||||
|
|
||||||
|
autoRefresh();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var WidgetCtrl = function($scope, $location, Events, Query) {
|
||||||
|
$scope.deleteWidget = function() {
|
||||||
|
if (!confirm('Are you sure you want to remove "' + $scope.widget.getName() + '" from the dashboard?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Events.record(currentUser, "delete", "widget", $scope.widget.id);
|
||||||
|
|
||||||
|
$scope.widget.$delete(function() {
|
||||||
|
$scope.dashboard.widgets = _.map($scope.dashboard.widgets, function(row) {
|
||||||
|
return _.filter(row, function(widget) {
|
||||||
|
return widget.id != undefined;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Events.record(currentUser, "view", "widget", $scope.widget.id);
|
||||||
|
|
||||||
|
if ($scope.widget.visualization) {
|
||||||
|
Events.record(currentUser, "view", "query", $scope.widget.visualization.query.id);
|
||||||
|
Events.record(currentUser, "view", "visualization", $scope.widget.visualization.id);
|
||||||
|
|
||||||
|
$scope.query = $scope.widget.getQuery();
|
||||||
|
var parameters = Query.collectParamsFromQueryString($location, $scope.query);
|
||||||
|
var maxAge = $location.search()['maxAge'];
|
||||||
|
$scope.queryResult = $scope.query.getQueryResult(maxAge, parameters);
|
||||||
|
|
||||||
|
$scope.type = 'visualization';
|
||||||
|
} else {
|
||||||
|
$scope.type = 'textbox';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
angular.module('redash.controllers')
|
||||||
|
.controller('DashboardCtrl', ['$scope', 'Events', 'Widget', '$routeParams', '$location', '$http', '$timeout', '$q', 'Dashboard', DashboardCtrl])
|
||||||
|
.controller('WidgetCtrl', ['$scope', '$location', 'Events', 'Query', WidgetCtrl])
|
||||||
|
|
||||||
|
})();
|
||||||
126
rd_ui/app/scripts/controllers/query_source.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function QuerySourceCtrl(Events, growl, $controller, $scope, $location, Query, Visualization, KeyboardShortcuts) {
|
||||||
|
// extends QueryViewCtrl
|
||||||
|
$controller('QueryViewCtrl', {$scope: $scope});
|
||||||
|
// TODO:
|
||||||
|
// This doesn't get inherited. Setting it on this didn't work either (which is weird).
|
||||||
|
// Obviously it shouldn't be repeated, but we got bigger fish to fry.
|
||||||
|
var DEFAULT_TAB = 'table';
|
||||||
|
|
||||||
|
Events.record(currentUser, 'view_source', 'query', $scope.query.id);
|
||||||
|
|
||||||
|
var isNewQuery = !$scope.query.id,
|
||||||
|
queryText = $scope.query.query,
|
||||||
|
// ref to QueryViewCtrl.saveQuery
|
||||||
|
saveQuery = $scope.saveQuery;
|
||||||
|
|
||||||
|
$scope.sourceMode = true;
|
||||||
|
$scope.canEdit = true;
|
||||||
|
$scope.isDirty = false;
|
||||||
|
|
||||||
|
$scope.newVisualization = undefined;
|
||||||
|
|
||||||
|
// @override
|
||||||
|
Object.defineProperty($scope, 'showDataset', {
|
||||||
|
get: function() {
|
||||||
|
return $scope.queryResult && $scope.queryResult.getStatus() == 'done';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var shortcuts = {
|
||||||
|
'meta+s': function () {
|
||||||
|
if ($scope.canEdit) {
|
||||||
|
$scope.saveQuery();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'ctrl+s': function () {
|
||||||
|
if ($scope.canEdit) {
|
||||||
|
$scope.saveQuery();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Cmd+Enter for Mac
|
||||||
|
'meta+enter': $scope.executeQuery,
|
||||||
|
// Ctrl+Enter for PC
|
||||||
|
'ctrl+enter': $scope.executeQuery
|
||||||
|
};
|
||||||
|
|
||||||
|
KeyboardShortcuts.bind(shortcuts);
|
||||||
|
|
||||||
|
// @override
|
||||||
|
$scope.saveQuery = function(options, data) {
|
||||||
|
var savePromise = saveQuery(options, data);
|
||||||
|
|
||||||
|
savePromise.then(function(savedQuery) {
|
||||||
|
queryText = savedQuery.query;
|
||||||
|
$scope.isDirty = $scope.query.query !== queryText;
|
||||||
|
|
||||||
|
if (isNewQuery) {
|
||||||
|
// redirect to new created query (keep hash)
|
||||||
|
$location.path(savedQuery.getSourceLink()).replace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return savePromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.duplicateQuery = function() {
|
||||||
|
Events.record(currentUser, 'fork', 'query', $scope.query.id);
|
||||||
|
$scope.query.id = null;
|
||||||
|
$scope.query.schedule = null;
|
||||||
|
|
||||||
|
$scope.saveQuery({
|
||||||
|
successMessage: 'Query forked',
|
||||||
|
errorMessage: 'Query could not be forked'
|
||||||
|
}).then(function redirect(savedQuery) {
|
||||||
|
// redirect to forked query (clear hash)
|
||||||
|
$location.url(savedQuery.getSourceLink()).replace()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.deleteVisualization = function($e, vis) {
|
||||||
|
$e.preventDefault();
|
||||||
|
if (confirm('Are you sure you want to delete ' + vis.name + ' ?')) {
|
||||||
|
Events.record(currentUser, 'delete', 'visualization', vis.id);
|
||||||
|
|
||||||
|
Visualization.delete(vis, function() {
|
||||||
|
if ($scope.selectedTab == vis.id) {
|
||||||
|
$scope.selectedTab = DEFAULT_TAB;
|
||||||
|
$location.hash($scope.selectedTab);
|
||||||
|
}
|
||||||
|
$scope.query.visualizations =
|
||||||
|
$scope.query.visualizations.filter(function (v) {
|
||||||
|
return vis.id !== v.id;
|
||||||
|
});
|
||||||
|
}, function () {
|
||||||
|
growl.addErrorMessage("Error deleting visualization. Maybe it's used in a dashboard?");
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('query.query', function(newQueryText) {
|
||||||
|
$scope.isDirty = (newQueryText !== queryText);
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$on('$destroy', function destroy() {
|
||||||
|
KeyboardShortcuts.unbind(shortcuts);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isNewQuery) {
|
||||||
|
// save new query when creating a visualization
|
||||||
|
var unbind = $scope.$watch('selectedTab == "add"', function(triggerSave) {
|
||||||
|
if (triggerSave) {
|
||||||
|
unbind();
|
||||||
|
$scope.saveQuery();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('redash.controllers').controller('QuerySourceCtrl', [
|
||||||
|
'Events', 'growl', '$controller', '$scope', '$location', 'Query',
|
||||||
|
'Visualization', 'KeyboardShortcuts', QuerySourceCtrl
|
||||||
|
]);
|
||||||
|
})();
|
||||||
237
rd_ui/app/scripts/controllers/query_view.js
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function QueryViewCtrl($scope, Events, $route, $location, notifications, growl, $modal, Query, DataSource) {
|
||||||
|
var DEFAULT_TAB = 'table';
|
||||||
|
|
||||||
|
var getQueryResult = function(maxAge) {
|
||||||
|
// Collect params, and getQueryResult with params; getQueryResult merges it into the query
|
||||||
|
var parameters = Query.collectParamsFromQueryString($location, $scope.query);
|
||||||
|
if (maxAge == undefined) {
|
||||||
|
maxAge = $location.search()['maxAge'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxAge == undefined) {
|
||||||
|
maxAge = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.queryResult = $scope.query.getQueryResult(maxAge, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.dataSource = {};
|
||||||
|
$scope.query = $route.current.locals.query;
|
||||||
|
|
||||||
|
var updateSchema = function() {
|
||||||
|
$scope.hasSchema = false;
|
||||||
|
$scope.editorSize = "col-md-12";
|
||||||
|
var dataSourceId = $scope.query.data_source_id || $scope.dataSources[0].id;
|
||||||
|
DataSource.getSchema({id: dataSourceId}, function(data) {
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
$scope.schema = data;
|
||||||
|
_.each(data, function(table) {
|
||||||
|
table.collapsed = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.editorSize = "col-md-9";
|
||||||
|
$scope.hasSchema = true;
|
||||||
|
} else {
|
||||||
|
$scope.hasSchema = false;
|
||||||
|
$scope.editorSize = "col-md-12";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Events.record(currentUser, 'view', 'query', $scope.query.id);
|
||||||
|
getQueryResult();
|
||||||
|
$scope.queryExecuting = false;
|
||||||
|
|
||||||
|
$scope.isQueryOwner = (currentUser.id === $scope.query.user.id) || currentUser.hasPermission('admin');
|
||||||
|
$scope.canViewSource = currentUser.hasPermission('view_source');
|
||||||
|
|
||||||
|
$scope.dataSources = DataSource.get(function(dataSources) {
|
||||||
|
updateSchema();
|
||||||
|
$scope.query.data_source_id = $scope.query.data_source_id || dataSources[0].id;
|
||||||
|
$scope.dataSource = _.find(dataSources, function(ds) { return ds.id == $scope.query.data_source_id; });
|
||||||
|
});
|
||||||
|
|
||||||
|
// in view mode, latest dataset is always visible
|
||||||
|
// source mode changes this behavior
|
||||||
|
$scope.showDataset = true;
|
||||||
|
|
||||||
|
$scope.lockButton = function(lock) {
|
||||||
|
$scope.queryExecuting = lock;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.showApiKey = function() {
|
||||||
|
alert("API Key for this query:\n" + $scope.query.api_key);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.saveQuery = function(options, data) {
|
||||||
|
if (data) {
|
||||||
|
data.id = $scope.query.id;
|
||||||
|
} else {
|
||||||
|
data = _.clone($scope.query);
|
||||||
|
}
|
||||||
|
|
||||||
|
options = _.extend({}, {
|
||||||
|
successMessage: 'Query saved',
|
||||||
|
errorMessage: 'Query could not be saved'
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
delete data.latest_query_data;
|
||||||
|
delete data.queryResult;
|
||||||
|
|
||||||
|
return Query.save(data, function() {
|
||||||
|
growl.addSuccessMessage(options.successMessage);
|
||||||
|
}, function(httpResponse) {
|
||||||
|
growl.addErrorMessage(options.errorMessage);
|
||||||
|
}).$promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.saveDescription = function() {
|
||||||
|
Events.record(currentUser, 'edit_description', 'query', $scope.query.id);
|
||||||
|
$scope.saveQuery(undefined, {'description': $scope.query.description});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.saveName = function() {
|
||||||
|
Events.record(currentUser, 'edit_name', 'query', $scope.query.id);
|
||||||
|
$scope.saveQuery(undefined, {'name': $scope.query.name});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.executeQuery = function() {
|
||||||
|
getQueryResult(0);
|
||||||
|
$scope.lockButton(true);
|
||||||
|
$scope.cancelling = false;
|
||||||
|
Events.record(currentUser, 'execute', 'query', $scope.query.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.cancelExecution = function() {
|
||||||
|
$scope.cancelling = true;
|
||||||
|
$scope.queryResult.cancelExecution();
|
||||||
|
Events.record(currentUser, 'cancel_execute', 'query', $scope.query.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.archiveQuery = function(options, data) {
|
||||||
|
if (data) {
|
||||||
|
data.id = $scope.query.id;
|
||||||
|
} else {
|
||||||
|
data = $scope.query;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.isDirty = false;
|
||||||
|
|
||||||
|
options = _.extend({}, {
|
||||||
|
successMessage: 'Query archived',
|
||||||
|
errorMessage: 'Query could not be archived'
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
return Query.delete({id: data.id}, function() {
|
||||||
|
$scope.query.is_archived = true;
|
||||||
|
$scope.query.schedule = null;
|
||||||
|
growl.addSuccessMessage(options.successMessage);
|
||||||
|
// This feels dirty.
|
||||||
|
$('#archive-confirmation-modal').modal('hide');
|
||||||
|
}, function(httpResponse) {
|
||||||
|
growl.addErrorMessage(options.errorMessage);
|
||||||
|
}).$promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.updateDataSource = function() {
|
||||||
|
Events.record(currentUser, 'update_data_source', 'query', $scope.query.id);
|
||||||
|
|
||||||
|
$scope.query.latest_query_data = null;
|
||||||
|
$scope.query.latest_query_data_id = null;
|
||||||
|
|
||||||
|
if ($scope.query.id) {
|
||||||
|
Query.save({
|
||||||
|
'id': $scope.query.id,
|
||||||
|
'data_source_id': $scope.query.data_source_id,
|
||||||
|
'latest_query_data_id': null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSchema();
|
||||||
|
$scope.dataSource = _.find($scope.dataSources, function(ds) { return ds.id == $scope.query.data_source_id; });
|
||||||
|
$scope.executeQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.setVisualizationTab = function (visualization) {
|
||||||
|
$scope.selectedTab = visualization.id;
|
||||||
|
$location.hash(visualization.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('query.name', function() {
|
||||||
|
$scope.$parent.pageTitle = $scope.query.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch('queryResult && queryResult.getData()', function(data, oldData) {
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.filters = $scope.queryResult.getFilters();
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch("queryResult && queryResult.getStatus()", function(status) {
|
||||||
|
if (!status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status == 'done') {
|
||||||
|
if ($scope.query.id &&
|
||||||
|
$scope.query.latest_query_data_id != $scope.queryResult.getId() &&
|
||||||
|
$scope.query.query_hash == $scope.queryResult.query_result.query_hash) {
|
||||||
|
Query.save({
|
||||||
|
'id': $scope.query.id,
|
||||||
|
'latest_query_data_id': $scope.queryResult.getId()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
$scope.query.latest_query_data_id = $scope.queryResult.getId();
|
||||||
|
$scope.query.queryResult = $scope.queryResult;
|
||||||
|
|
||||||
|
notifications.showNotification("re:dash", $scope.query.name + " updated.");
|
||||||
|
} else if (status == 'failed') {
|
||||||
|
notifications.showNotification("re:dash", $scope.query.name + " failed to run: " + $scope.queryResult.getError());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'done' || status === 'failed') {
|
||||||
|
$scope.lockButton(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.openScheduleForm = function() {
|
||||||
|
if (!$scope.isQueryOwner) {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
$modal.open({
|
||||||
|
templateUrl: '/views/schedule_form.html',
|
||||||
|
size: 'sm',
|
||||||
|
scope: $scope,
|
||||||
|
controller: ['$scope', '$modalInstance', function($scope, $modalInstance) {
|
||||||
|
$scope.close = function() {
|
||||||
|
$modalInstance.close();
|
||||||
|
}
|
||||||
|
if ($scope.query.hasDailySchedule()) {
|
||||||
|
$scope.refreshType = 'daily';
|
||||||
|
} else {
|
||||||
|
$scope.refreshType = 'periodic';
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch(function() {
|
||||||
|
return $location.hash()
|
||||||
|
}, function(hash) {
|
||||||
|
if (hash == 'pivot') {
|
||||||
|
Events.record(currentUser, 'pivot', 'query', $scope.query && $scope.query.id);
|
||||||
|
}
|
||||||
|
$scope.selectedTab = hash || DEFAULT_TAB;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
angular.module('redash.controllers')
|
||||||
|
.controller('QueryViewCtrl',
|
||||||
|
['$scope', 'Events', '$route', '$location', 'notifications', 'growl', '$modal', 'Query', 'DataSource', QueryViewCtrl]);
|
||||||
|
})();
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
var directives = angular.module('redash.directives', []);
|
|
||||||
directives.directive('rdTabs', ['$location', '$rootScope', function($location, $rootScope) {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
scope: {
|
|
||||||
tabsCollection: '=',
|
|
||||||
selectedTab: '='
|
|
||||||
},
|
|
||||||
template: '<ul class="nav nav-tabs"><li ng-class="{active: tab==selectedTab}" ng-repeat="tab in tabsCollection"><a href="#{{tab.key}}">{{tab.name}}</a></li></ul>',
|
|
||||||
replace: true,
|
|
||||||
link: function($scope, element, attrs) {
|
|
||||||
$scope.selectTab = function(tabKey) {
|
|
||||||
$scope.selectedTab = _.find($scope.tabsCollection, function(tab) { return tab.key == tabKey; });
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.$watch(function() { return $location.hash()}, function(hash) {
|
|
||||||
if (hash) {
|
|
||||||
$scope.selectTab($location.hash());
|
|
||||||
} else {
|
|
||||||
$scope.selectTab($scope.tabsCollection[0].key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}])
|
|
||||||
|
|
||||||
directives.directive('editDashboardForm', ['$http', '$location', '$timeout', 'Dashboard', function($http, $location, $timeout, Dashboard) {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
scope: {
|
|
||||||
dashboard: '='
|
|
||||||
},
|
|
||||||
templateUrl: '/views/edit_dashboard.html',
|
|
||||||
replace: true,
|
|
||||||
link: function($scope, element, attrs) {
|
|
||||||
$scope.$watch('dashboard.widgets', function() {
|
|
||||||
if ($scope.dashboard.widgets) {
|
|
||||||
$scope.layout = [];
|
|
||||||
_.each($scope.dashboard.widgets, function(row, rowIndex) {
|
|
||||||
_.each(row, function(widget, colIndex) {
|
|
||||||
$scope.layout.push({
|
|
||||||
id: widget.id,
|
|
||||||
col: colIndex+1,
|
|
||||||
row: rowIndex+1,
|
|
||||||
ySize: 1,
|
|
||||||
xSize: widget.width,
|
|
||||||
name: widget.query.name
|
|
||||||
})
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
$timeout(function () {
|
|
||||||
$(".gridster ul").gridster({
|
|
||||||
widget_margins: [5, 5],
|
|
||||||
widget_base_dimensions: [260, 100],
|
|
||||||
min_cols: 2,
|
|
||||||
max_cols: 2,
|
|
||||||
serialize_params: function ($w, wgd) {
|
|
||||||
return { col: wgd.col, row: wgd.row, id: $w.data('widget-id') }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.saveDashboard = function() {
|
|
||||||
$scope.saveInProgress = true;
|
|
||||||
// TODO: we should use the dashboard service here.
|
|
||||||
if ($scope.dashboard.id) {
|
|
||||||
var positions = $(element).find('.gridster ul').data('gridster').serialize();
|
|
||||||
var layout = [];
|
|
||||||
_.each(_.sortBy(positions, function (pos) {
|
|
||||||
return pos.row * 10 + pos.col;
|
|
||||||
}), function (pos) {
|
|
||||||
var row = pos.row - 1;
|
|
||||||
var col = pos.col - 1;
|
|
||||||
layout[row] = layout[row] || [];
|
|
||||||
if (col > 0 && layout[row][col - 1] == undefined) {
|
|
||||||
layout[row][col - 1] = pos.id;
|
|
||||||
} else {
|
|
||||||
layout[row][col] = pos.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
$scope.dashboard.layout = layout;
|
|
||||||
|
|
||||||
layout = JSON.stringify(layout);
|
|
||||||
$http.post('/api/dashboards/' + $scope.dashboard.id, {'name': $scope.dashboard.name, 'layout': layout}).success(function(response) {
|
|
||||||
$scope.dashboard = new Dashboard(response);
|
|
||||||
$scope.saveInProgress = false;
|
|
||||||
$(element).modal('hide');
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
$http.post('/api/dashboards', {'name': $scope.dashboard.name}).success(function(response) {
|
|
||||||
$(element).modal('hide');
|
|
||||||
$location.path('/dashboard/' + response.slug).replace();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}])
|
|
||||||
|
|
||||||
|
|
||||||
directives.directive('newWidgetForm', ['$http', function($http) {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
scope: {
|
|
||||||
dashboard: '='
|
|
||||||
},
|
|
||||||
templateUrl: '/views/new_widget_form.html',
|
|
||||||
replace: true,
|
|
||||||
link: function($scope, element, attrs) {
|
|
||||||
$scope.widgetTypes = [{name: 'Chart', value: 'chart'}, {name: 'Table', value: 'grid'}];
|
|
||||||
$scope.widgetSizes = [{name: 'Regular Size', value: 1}, {name: 'Double Size', value: 2}];
|
|
||||||
|
|
||||||
var reset = function() {
|
|
||||||
$scope.saveInProgress = false;
|
|
||||||
$scope.widgetType = 'chart';
|
|
||||||
$scope.widgetSize = 1;
|
|
||||||
$scope.queryId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
reset();
|
|
||||||
|
|
||||||
$scope.saveWidget = function() {
|
|
||||||
$scope.saveInProgress = true;
|
|
||||||
|
|
||||||
var widget = {
|
|
||||||
'query_id': $scope.queryId,
|
|
||||||
'dashboard_id': $scope.dashboard.id,
|
|
||||||
'type': $scope.widgetType,
|
|
||||||
'options': {},
|
|
||||||
'width': $scope.widgetSize
|
|
||||||
}
|
|
||||||
|
|
||||||
$http.post('/api/widgets', widget).success(function(response) {
|
|
||||||
// update dashboard layout
|
|
||||||
$scope.dashboard.layout = response['layout'];
|
|
||||||
if (response['new_row']) {
|
|
||||||
$scope.dashboard.widgets.push([response['widget']]);
|
|
||||||
} else {
|
|
||||||
$scope.dashboard.widgets[$scope.dashboard.widgets.length-1].push(response['widget']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// close the dialog
|
|
||||||
$('#add_query_dialog').modal('hide');
|
|
||||||
reset();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}])
|
|
||||||
|
|
||||||
// From: http://jsfiddle.net/joshdmiller/NDFHg/
|
|
||||||
directives.directive('editInPlace', function () {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
scope: { value: '=' },
|
|
||||||
template: '<span ng-click="edit()" ng-bind="value"></span><input ng-model="value"></input>',
|
|
||||||
link: function ($scope, element, attrs) {
|
|
||||||
// Let's get a reference to the input element, as we'll want to reference it.
|
|
||||||
var inputElement = angular.element(element.children()[1]);
|
|
||||||
|
|
||||||
// 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 = function () {
|
|
||||||
$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();
|
|
||||||
};
|
|
||||||
|
|
||||||
$(inputElement).blur(function() {
|
|
||||||
$scope.editing = false;
|
|
||||||
element.removeClass('active');
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
directives.directive('rdTimer', ['$timeout', function ($timeout) {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
scope: { timestamp: '=' },
|
|
||||||
template: '{{currentTime}}',
|
|
||||||
controller: ['$scope' ,function ($scope) {
|
|
||||||
$scope.currentTime = "00:00:00";
|
|
||||||
var currentTimeout = null;
|
|
||||||
|
|
||||||
var updateTime = function() {
|
|
||||||
$scope.currentTime = moment(moment() - moment($scope.timestamp)).utc().format("HH:mm:ss")
|
|
||||||
currentTimeout = $timeout(updateTime, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
var cancelTimer = function() {
|
|
||||||
if (currentTimeout) {
|
|
||||||
$timeout.cancel(currentTimeout);
|
|
||||||
currentTimeout = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTime();
|
|
||||||
|
|
||||||
$scope.$on('$destroy', function () {
|
|
||||||
cancelTimer();
|
|
||||||
});
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
}]);
|
|
||||||
222
rd_ui/app/scripts/directives/dashboard_directives.js
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
(function() {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var directives = angular.module('redash.directives');
|
||||||
|
|
||||||
|
directives.directive('editDashboardForm', ['Events', '$http', '$location', '$timeout', 'Dashboard',
|
||||||
|
function(Events, $http, $location, $timeout, Dashboard) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
dashboard: '='
|
||||||
|
},
|
||||||
|
templateUrl: '/views/edit_dashboard.html',
|
||||||
|
replace: true,
|
||||||
|
link: function($scope, element, attrs) {
|
||||||
|
var gridster = element.find(".gridster ul").gridster({
|
||||||
|
widget_margins: [5, 5],
|
||||||
|
widget_base_dimensions: [260, 100],
|
||||||
|
min_cols: 2,
|
||||||
|
max_cols: 2,
|
||||||
|
serialize_params: function($w, wgd) {
|
||||||
|
return {
|
||||||
|
col: wgd.col,
|
||||||
|
row: wgd.row,
|
||||||
|
id: $w.data('widget-id')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).data('gridster');
|
||||||
|
|
||||||
|
var gsItemTemplate = '<li data-widget-id="{id}" class="widget panel panel-default gs-w">' +
|
||||||
|
'<div class="panel-heading">{name}' +
|
||||||
|
'</div></li>';
|
||||||
|
|
||||||
|
$scope.$watch('dashboard.widgets && dashboard.widgets.length', function(widgets_length) {
|
||||||
|
$timeout(function() {
|
||||||
|
gridster.remove_all_widgets();
|
||||||
|
|
||||||
|
if ($scope.dashboard.widgets && $scope.dashboard.widgets.length) {
|
||||||
|
var layout = [];
|
||||||
|
|
||||||
|
_.each($scope.dashboard.widgets, function(row, rowIndex) {
|
||||||
|
_.each(row, function(widget, colIndex) {
|
||||||
|
layout.push({
|
||||||
|
id: widget.id,
|
||||||
|
col: colIndex + 1,
|
||||||
|
row: rowIndex + 1,
|
||||||
|
ySize: 1,
|
||||||
|
xSize: widget.width,
|
||||||
|
name: widget.getName()//visualization.query.name
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
_.each(layout, function(item) {
|
||||||
|
var el = gsItemTemplate.replace('{id}', item.id).replace('{name}', item.name);
|
||||||
|
gridster.add_widget(el, item.xSize, item.ySize, item.col, item.row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.saveDashboard = function() {
|
||||||
|
$scope.saveInProgress = true;
|
||||||
|
// TODO: we should use the dashboard service here.
|
||||||
|
if ($scope.dashboard.id) {
|
||||||
|
var positions = $(element).find('.gridster ul').data('gridster').serialize();
|
||||||
|
var layout = [];
|
||||||
|
_.each(_.sortBy(positions, function(pos) {
|
||||||
|
return pos.row * 10 + pos.col;
|
||||||
|
}), function(pos) {
|
||||||
|
var row = pos.row - 1;
|
||||||
|
var col = pos.col - 1;
|
||||||
|
layout[row] = layout[row] || [];
|
||||||
|
if (col > 0 && layout[row][col - 1] == undefined) {
|
||||||
|
layout[row][col - 1] = pos.id;
|
||||||
|
} else {
|
||||||
|
layout[row][col] = pos.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
$scope.dashboard.layout = layout;
|
||||||
|
|
||||||
|
layout = JSON.stringify(layout);
|
||||||
|
$http.post('/api/dashboards/' + $scope.dashboard.id, {
|
||||||
|
'name': $scope.dashboard.name,
|
||||||
|
'layout': layout
|
||||||
|
}).success(function(response) {
|
||||||
|
$scope.dashboard = new Dashboard(response);
|
||||||
|
$scope.saveInProgress = false;
|
||||||
|
$(element).modal('hide');
|
||||||
|
});
|
||||||
|
Events.record(currentUser, 'edit', 'dashboard', $scope.dashboard.id);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
$http.post('/api/dashboards', {
|
||||||
|
'name': $scope.dashboard.name
|
||||||
|
}).success(function(response) {
|
||||||
|
$(element).modal('hide');
|
||||||
|
$scope.dashboard = {
|
||||||
|
'name': null,
|
||||||
|
'layout': null
|
||||||
|
};
|
||||||
|
$scope.saveInProgress = false;
|
||||||
|
$location.path('/dashboard/' + response.slug).replace();
|
||||||
|
});
|
||||||
|
Events.record(currentUser, 'create', 'dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
directives.directive('newWidgetForm', ['Query', 'Widget', 'growl',
|
||||||
|
function(Query, Widget, growl) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
dashboard: '='
|
||||||
|
},
|
||||||
|
templateUrl: '/views/new_widget_form.html',
|
||||||
|
replace: true,
|
||||||
|
link: function($scope, element, attrs) {
|
||||||
|
$scope.widgetSizes = [{
|
||||||
|
name: 'Regular',
|
||||||
|
value: 1
|
||||||
|
}, {
|
||||||
|
name: 'Double',
|
||||||
|
value: 2
|
||||||
|
}];
|
||||||
|
|
||||||
|
$scope.type = 'visualization';
|
||||||
|
|
||||||
|
$scope.isVisualization = function () {
|
||||||
|
return $scope.type == 'visualization';
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.isTextBox = function () {
|
||||||
|
return $scope.type == 'textbox';
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.setType = function (type) {
|
||||||
|
$scope.type = type;
|
||||||
|
};
|
||||||
|
|
||||||
|
var reset = function() {
|
||||||
|
$scope.saveInProgress = false;
|
||||||
|
$scope.widgetSize = 1;
|
||||||
|
$scope.selectedVis = null;
|
||||||
|
$scope.query = {};
|
||||||
|
$scope.selected_query = undefined;
|
||||||
|
$scope.text = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
reset();
|
||||||
|
|
||||||
|
$scope.loadVisualizations = function () {
|
||||||
|
if (!$scope.query.selected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Query.get({ id: $scope.query.selected.id }, function(query) {
|
||||||
|
if (query) {
|
||||||
|
$scope.selected_query = query;
|
||||||
|
if (query.visualizations.length) {
|
||||||
|
$scope.selectedVis = query.visualizations[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.searchQueries = function (term) {
|
||||||
|
if (!term || term.length < 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Query.search({q: term}, function(results) {
|
||||||
|
$scope.queries = results;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('query', function () {
|
||||||
|
$scope.loadVisualizations();
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
$scope.saveWidget = function() {
|
||||||
|
$scope.saveInProgress = true;
|
||||||
|
|
||||||
|
var widget = new Widget({
|
||||||
|
'visualization_id': $scope.selectedVis && $scope.selectedVis.id,
|
||||||
|
'dashboard_id': $scope.dashboard.id,
|
||||||
|
'options': {},
|
||||||
|
'width': $scope.widgetSize,
|
||||||
|
'text': $scope.text
|
||||||
|
});
|
||||||
|
|
||||||
|
widget.$save().then(function(response) {
|
||||||
|
// update dashboard layout
|
||||||
|
$scope.dashboard.layout = response['layout'];
|
||||||
|
var newWidget = new Widget(response['widget']);
|
||||||
|
if (response['new_row']) {
|
||||||
|
$scope.dashboard.widgets.push([newWidget]);
|
||||||
|
} else {
|
||||||
|
$scope.dashboard.widgets[$scope.dashboard.widgets.length - 1].push(newWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
// close the dialog
|
||||||
|
$('#add_query_dialog').modal('hide');
|
||||||
|
reset();
|
||||||
|
}).catch(function() {
|
||||||
|
growl.addErrorMessage("Widget can not be added");
|
||||||
|
}).finally(function() {
|
||||||
|
$scope.saveInProgress = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})();
|
||||||
250
rd_ui/app/scripts/directives/directives.js
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var directives = angular.module('redash.directives', []);
|
||||||
|
|
||||||
|
directives.directive('alertUnsavedChanges', ['$window', function ($window) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
replace: true,
|
||||||
|
scope: {
|
||||||
|
'isDirty': '='
|
||||||
|
},
|
||||||
|
link: function ($scope) {
|
||||||
|
var
|
||||||
|
|
||||||
|
unloadMessage = "You will lose your changes if you leave",
|
||||||
|
confirmMessage = unloadMessage + "\n\nAre you sure you want to leave this page?",
|
||||||
|
|
||||||
|
// store original handler (if any)
|
||||||
|
_onbeforeunload = $window.onbeforeunload;
|
||||||
|
|
||||||
|
$window.onbeforeunload = function () {
|
||||||
|
return $scope.isDirty ? unloadMessage : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.$on('$locationChangeStart', function (event, next, current) {
|
||||||
|
if (next.split("#")[0] == current.split("#")[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scope.isDirty && !confirm(confirmMessage)) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$on('$destroy', function () {
|
||||||
|
$window.onbeforeunload = _onbeforeunload;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
|
||||||
|
directives.directive('rdTab', function () {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
'tabId': '@',
|
||||||
|
'name': '@'
|
||||||
|
},
|
||||||
|
transclude: true,
|
||||||
|
template: '<li class="rd-tab" ng-class="{active: tabId==selectedTab}"><a href="#{{tabId}}">{{name}}<span ng-transclude></span></a></li>',
|
||||||
|
replace: true,
|
||||||
|
link: function (scope) {
|
||||||
|
scope.$watch(function () {
|
||||||
|
return scope.$parent.selectedTab
|
||||||
|
}, function (tab) {
|
||||||
|
scope.selectedTab = tab;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
directives.directive('rdTabs', ['$location', function ($location) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
tabsCollection: '=',
|
||||||
|
selectedTab: '='
|
||||||
|
},
|
||||||
|
template: '<ul class="nav nav-tabs"><li ng-class="{active: tab==selectedTab}" ng-repeat="tab in tabsCollection"><a href="#{{tab.key}}">{{tab.name}}</a></li></ul>',
|
||||||
|
replace: true,
|
||||||
|
link: function ($scope, element, attrs) {
|
||||||
|
$scope.selectTab = function (tabKey) {
|
||||||
|
$scope.selectedTab = _.find($scope.tabsCollection, function (tab) {
|
||||||
|
return tab.key == tabKey;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.$watch(function () {
|
||||||
|
return $location.hash()
|
||||||
|
}, function (hash) {
|
||||||
|
if (hash) {
|
||||||
|
$scope.selectTab($location.hash());
|
||||||
|
} else {
|
||||||
|
$scope.selectTab($scope.tabsCollection[0].key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
|
||||||
|
// From: http://jsfiddle.net/joshdmiller/NDFHg/
|
||||||
|
directives.directive('editInPlace', function () {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
value: '=',
|
||||||
|
ignoreBlanks: '=',
|
||||||
|
editable: '=',
|
||||||
|
done: '=',
|
||||||
|
},
|
||||||
|
template: function (tElement, tAttrs) {
|
||||||
|
var elType = tAttrs.editor || 'input';
|
||||||
|
var placeholder = tAttrs.placeholder || 'Click to edit';
|
||||||
|
|
||||||
|
var 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>';
|
||||||
|
}
|
||||||
|
|
||||||
|
var placeholderSpan = '<span ng-click="editable && edit()" ng-show="editable && !value" ng-class="{editable: editable}">' + placeholder + '</span>';
|
||||||
|
var editor = '<{elType} ng-model="value" class="rd-form-control"></{elType}>'.replace('{elType}', elType);
|
||||||
|
|
||||||
|
return viewMode + placeholderSpan + editor;
|
||||||
|
},
|
||||||
|
link: function ($scope, element, attrs) {
|
||||||
|
// Let's get a reference to the input element, as we'll want to reference it.
|
||||||
|
var inputElement = angular.element(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 = function () {
|
||||||
|
$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) {
|
||||||
|
$scope.done && $scope.done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(inputElement).keydown(function (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(function () {
|
||||||
|
$(inputElement[0]).blur();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).blur(function () {
|
||||||
|
save();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// http://stackoverflow.com/a/17904092/1559840
|
||||||
|
directives.directive('jsonText', function () {
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
require: 'ngModel',
|
||||||
|
link: function (scope, element, attr, ngModel) {
|
||||||
|
function into(input) {
|
||||||
|
return JSON.parse(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
function out(data) {
|
||||||
|
return JSON.stringify(data, undefined, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngModel.$parsers.push(into);
|
||||||
|
ngModel.$formatters.push(out);
|
||||||
|
|
||||||
|
scope.$watch(attr.ngModel, function (newValue) {
|
||||||
|
element[0].value = out(newValue);
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
directives.directive('rdTimer', [function () {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: { timestamp: '=' },
|
||||||
|
template: '{{currentTime}}',
|
||||||
|
controller: ['$scope' , function ($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.
|
||||||
|
var currentTimer = setInterval(function () {
|
||||||
|
$scope.currentTime = moment(moment() - moment($scope.timestamp)).utc().format("HH:mm:ss");
|
||||||
|
$scope.$digest();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
$scope.$on('$destroy', function () {
|
||||||
|
if (currentTimer) {
|
||||||
|
clearInterval(currentTimer);
|
||||||
|
currentTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}]);
|
||||||
|
|
||||||
|
directives.directive('rdTimeAgo', function () {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
value: '='
|
||||||
|
},
|
||||||
|
template: '<span>' +
|
||||||
|
'<span ng-show="value" am-time-ago="value"></span>' +
|
||||||
|
'<span ng-hide="value">-</span>' +
|
||||||
|
'</span>'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Used instead of autofocus attribute, which doesn't work in Angular as there is no real page load.
|
||||||
|
directives.directive('autofocus',
|
||||||
|
['$timeout', function ($timeout) {
|
||||||
|
return {
|
||||||
|
link: function (scope, element) {
|
||||||
|
$timeout(function () {
|
||||||
|
element[0].focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
})();
|
||||||
290
rd_ui/app/scripts/directives/query_directives.js
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
(function() {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
function queryLink() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
'query': '=',
|
||||||
|
'visualization': '=?'
|
||||||
|
},
|
||||||
|
template: '<small><span class="glyphicon glyphicon-link"></span></small> <a ng-href="{{link}}" class="query-link">{{query.name}}</a>',
|
||||||
|
link: function(scope, element) {
|
||||||
|
scope.link = '/queries/' + scope.query.id;
|
||||||
|
if (scope.visualization) {
|
||||||
|
if (scope.visualization.type === 'TABLE') {
|
||||||
|
// link to hard-coded table tab instead of the (hidden) visualization tab
|
||||||
|
scope.link += '#table';
|
||||||
|
} else {
|
||||||
|
scope.link += '#' + scope.visualization.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// element.find('a').attr('href', link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function querySourceLink() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
template: '<span ng-show="query.id && canViewSource">\
|
||||||
|
<a ng-show="!sourceMode"\
|
||||||
|
ng-href="/queries/{{query.id}}/source#{{selectedTab}}">Show Source\
|
||||||
|
</a>\
|
||||||
|
<a ng-show="sourceMode"\
|
||||||
|
ng-href="/queries/{{query.id}}#{{selectedTab}}">Hide Source\
|
||||||
|
</a>\
|
||||||
|
</span>'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryResultCSVLink() {
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
link: function (scope, element) {
|
||||||
|
scope.$watch('queryResult && queryResult.getData()', function(data) {
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope.queryResult.getId() == null) {
|
||||||
|
element.attr('href', '');
|
||||||
|
} else {
|
||||||
|
element.attr('href', '/api/queries/' + scope.query.id + '/results/' + scope.queryResult.getId() + '.csv');
|
||||||
|
element.attr('download', scope.query.name.replace(" ", "_") + moment(scope.queryResult.getUpdatedAt()).format("_YYYY_MM_DD") + ".csv");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryEditor() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
'query': '=',
|
||||||
|
'lock': '=',
|
||||||
|
'schema': '=',
|
||||||
|
'syntax': '='
|
||||||
|
},
|
||||||
|
template: '<textarea></textarea>',
|
||||||
|
link: {
|
||||||
|
pre: function ($scope, element) {
|
||||||
|
$scope.syntax = $scope.syntax || 'sql';
|
||||||
|
|
||||||
|
var modes = {
|
||||||
|
'sql': 'text/x-sql',
|
||||||
|
'python': 'text/x-python',
|
||||||
|
'json': 'application/json'
|
||||||
|
};
|
||||||
|
|
||||||
|
var textarea = element.children()[0];
|
||||||
|
var editorOptions = {
|
||||||
|
mode: modes[$scope.syntax],
|
||||||
|
lineWrapping: true,
|
||||||
|
lineNumbers: true,
|
||||||
|
readOnly: false,
|
||||||
|
matchBrackets: true,
|
||||||
|
autoCloseBrackets: true,
|
||||||
|
extraKeys: {"Ctrl-Space": "autocomplete"}
|
||||||
|
};
|
||||||
|
|
||||||
|
var additionalHints = [];
|
||||||
|
|
||||||
|
CodeMirror.commands.autocomplete = function(cm) {
|
||||||
|
var hinter = function(editor, options) {
|
||||||
|
var hints = CodeMirror.hint.anyword(editor, options);
|
||||||
|
var cur = editor.getCursor(), token = editor.getTokenAt(cur).string;
|
||||||
|
|
||||||
|
hints.list = _.union(hints.list, _.filter(additionalHints, function (h) {
|
||||||
|
return h.search(token) === 0;
|
||||||
|
}));
|
||||||
|
|
||||||
|
return hints;
|
||||||
|
};
|
||||||
|
|
||||||
|
// CodeMirror.showHint(cm, CodeMirror.hint.anyword);
|
||||||
|
CodeMirror.showHint(cm, hinter);
|
||||||
|
};
|
||||||
|
|
||||||
|
var codemirror = CodeMirror.fromTextArea(textarea, editorOptions);
|
||||||
|
|
||||||
|
codemirror.on('change', function(instance) {
|
||||||
|
var newValue = instance.getValue();
|
||||||
|
|
||||||
|
if (newValue !== $scope.query.query) {
|
||||||
|
$scope.$evalAsync(function() {
|
||||||
|
$scope.query.query = newValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.schema-container').css('height', $('.CodeMirror').css('height'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch('query.query', function () {
|
||||||
|
if ($scope.query.query !== codemirror.getValue()) {
|
||||||
|
codemirror.setValue($scope.query.query);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch('schema', function (schema) {
|
||||||
|
if (schema) {
|
||||||
|
var keywords = [];
|
||||||
|
_.each(schema, function (table) {
|
||||||
|
keywords.push(table.name);
|
||||||
|
_.each(table.columns, function (c) {
|
||||||
|
keywords.push(c);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
additionalHints = _.unique(keywords);
|
||||||
|
}
|
||||||
|
|
||||||
|
codemirror.refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch('syntax', function(syntax) {
|
||||||
|
codemirror.setOption('mode', modes[syntax]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch('lock', function (locked) {
|
||||||
|
var readOnly = locked ? 'nocursor' : false;
|
||||||
|
codemirror.setOption('readOnly', readOnly);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryFormatter($http) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
// don't create new scope to avoid ui-codemirror bug
|
||||||
|
// seehttps://github.com/angular-ui/ui-codemirror/pull/37
|
||||||
|
scope: false,
|
||||||
|
template: '<button type="button" class="btn btn-default btn-xs"\
|
||||||
|
ng-click="formatQuery()">\
|
||||||
|
<span class="glyphicon glyphicon-indent-left"></span>\
|
||||||
|
Format SQL\
|
||||||
|
</button>',
|
||||||
|
link: function($scope) {
|
||||||
|
$scope.formatQuery = function formatQuery() {
|
||||||
|
$scope.queryFormatting = true;
|
||||||
|
$http.post('/api/queries/format', {
|
||||||
|
'query': $scope.query.query
|
||||||
|
}).success(function (response) {
|
||||||
|
$scope.query.query = response;
|
||||||
|
}).finally(function () {
|
||||||
|
$scope.queryFormatting = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryTimePicker() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
template: '<select ng-disabled="refreshType != \'daily\'" ng-model="hour" ng-change="updateSchedule()" ng-options="c as c for c in hourOptions"></select> :\
|
||||||
|
<select ng-disabled="refreshType != \'daily\'" ng-model="minute" ng-change="updateSchedule()" ng-options="c as c for c in minuteOptions"></select>',
|
||||||
|
link: function($scope) {
|
||||||
|
var padWithZeros = function(size, v) {
|
||||||
|
v = String(v);
|
||||||
|
if (v.length < size) {
|
||||||
|
v = "0" + v;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.hourOptions = _.map(_.range(0, 24), _.partial(padWithZeros, 2));
|
||||||
|
$scope.minuteOptions = _.map(_.range(0, 60, 5), _.partial(padWithZeros, 2));
|
||||||
|
|
||||||
|
if ($scope.query.hasDailySchedule()) {
|
||||||
|
var parts = $scope.query.scheduleInLocalTime().split(':');
|
||||||
|
$scope.minute = parts[1];
|
||||||
|
$scope.hour = parts[0];
|
||||||
|
} else {
|
||||||
|
$scope.minute = "15";
|
||||||
|
$scope.hour = "00";
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.updateSchedule = function() {
|
||||||
|
var newSchedule = moment().hour($scope.hour).minute($scope.minute).utc().format('HH:mm');
|
||||||
|
if (newSchedule != $scope.query.schedule) {
|
||||||
|
$scope.query.schedule = newSchedule;
|
||||||
|
$scope.saveQuery();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('refreshType', function() {
|
||||||
|
if ($scope.refreshType == 'daily') {
|
||||||
|
$scope.updateSchedule();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryRefreshSelect() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
template: '<select\
|
||||||
|
ng-disabled="refreshType != \'periodic\'"\
|
||||||
|
ng-model="query.schedule"\
|
||||||
|
ng-change="saveQuery()"\
|
||||||
|
ng-options="c.value as c.name for c in refreshOptions">\
|
||||||
|
<option value="">No Refresh</option>\
|
||||||
|
</select>',
|
||||||
|
link: function($scope) {
|
||||||
|
$scope.refreshOptions = [
|
||||||
|
{
|
||||||
|
value: "60",
|
||||||
|
name: 'Every minute'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
_.each([5, 10, 15, 30], function(i) {
|
||||||
|
$scope.refreshOptions.push({
|
||||||
|
value: String(i*60),
|
||||||
|
name: "Every " + i + " minutes"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
_.each(_.range(1, 13), function (i) {
|
||||||
|
$scope.refreshOptions.push({
|
||||||
|
value: String(i * 3600),
|
||||||
|
name: 'Every ' + i + 'h'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
$scope.refreshOptions.push({
|
||||||
|
value: String(24 * 3600),
|
||||||
|
name: 'Every 24h'
|
||||||
|
});
|
||||||
|
$scope.refreshOptions.push({
|
||||||
|
value: String(7 * 24 * 3600),
|
||||||
|
name: 'Once a week'
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch('refreshType', function() {
|
||||||
|
if ($scope.refreshType == 'periodic') {
|
||||||
|
if ($scope.query.hasDailySchedule()) {
|
||||||
|
$scope.query.schedule = null;
|
||||||
|
$scope.saveQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('redash.directives')
|
||||||
|
.directive('queryLink', queryLink)
|
||||||
|
.directive('querySourceLink', querySourceLink)
|
||||||
|
.directive('queryResultLink', queryResultCSVLink)
|
||||||
|
.directive('queryEditor', queryEditor)
|
||||||
|
.directive('queryRefreshSelect', queryRefreshSelect)
|
||||||
|
.directive('queryTimePicker', queryTimePicker)
|
||||||
|
.directive('queryFormatter', ['$http', queryFormatter]);
|
||||||
|
})();
|
||||||
@@ -1,35 +1,91 @@
|
|||||||
|
var durationHumanize = function (duration) {
|
||||||
|
var humanized = "";
|
||||||
|
if (duration == undefined) {
|
||||||
|
humanized = "-";
|
||||||
|
} else if (duration < 60) {
|
||||||
|
humanized = Math.round(duration) + "s";
|
||||||
|
} else if (duration > 3600 * 24) {
|
||||||
|
var days = Math.round(parseFloat(duration) / 60.0 / 60.0 / 24.0);
|
||||||
|
humanized = days + "days";
|
||||||
|
} else if (duration >= 3600) {
|
||||||
|
var hours = Math.round(parseFloat(duration) / 60.0 / 60.0);
|
||||||
|
humanized = hours + "h";
|
||||||
|
} else {
|
||||||
|
var minutes = Math.round(parseFloat(duration) / 60.0);
|
||||||
|
humanized = minutes + "m";
|
||||||
|
}
|
||||||
|
return humanized;
|
||||||
|
};
|
||||||
|
|
||||||
|
var urlPattern = /(^|[\s\n]|<br\/?>)((?:https?|ftp):\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|])/gi;
|
||||||
|
|
||||||
angular.module('redash.filters', []).
|
angular.module('redash.filters', []).
|
||||||
filter('durationHumanize', function () {
|
filter('durationHumanize', function () {
|
||||||
return function (duration) {
|
return durationHumanize;
|
||||||
var humanized = "";
|
})
|
||||||
if (duration == undefined) {
|
|
||||||
humanized = "-";
|
|
||||||
} else if (duration < 60) {
|
|
||||||
humanized = Math.round(duration) + "s";
|
|
||||||
} else if (duration >= 3600) {
|
|
||||||
var hours = Math.round(parseFloat(duration) / 60.0 / 60.0)
|
|
||||||
humanized = hours + "h";
|
|
||||||
} else {
|
|
||||||
var minutes = Math.round(parseFloat(duration) / 60.0);
|
|
||||||
humanized = minutes + "m";
|
|
||||||
}
|
|
||||||
return humanized;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
.filter('toHuman', function() {
|
.filter('scheduleHumanize', function() {
|
||||||
return function(text) {
|
return function (schedule) {
|
||||||
return text.replace(/_/g, ' ').replace(/(?:^|\s)\S/g, function (a) {
|
if (schedule === null) {
|
||||||
return a.toUpperCase();
|
return "Never";
|
||||||
});
|
} else if (schedule.match(/\d\d:\d\d/) !== null) {
|
||||||
}
|
var parts = schedule.split(':');
|
||||||
})
|
var localTime = moment.utc().hour(parts[0]).minute(parts[1]).local().format('HH:mm');
|
||||||
|
return "Every day at " + localTime;
|
||||||
|
}
|
||||||
|
|
||||||
.filter('colWidth', function () {
|
return "Every " + durationHumanize(parseInt(schedule));
|
||||||
return function (widgetWidth) {
|
}
|
||||||
if (widgetWidth == 1) {
|
})
|
||||||
return 6;
|
|
||||||
}
|
.filter('toHuman', function () {
|
||||||
return 12;
|
return function (text) {
|
||||||
}
|
return text.replace(/_/g, ' ').replace(/(?:^|\s)\S/g, function (a) {
|
||||||
});
|
return a.toUpperCase();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
.filter('colWidth', function () {
|
||||||
|
return function (widgetWidth) {
|
||||||
|
if (widgetWidth == 1) {
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
return 12;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
.filter('capitalize', function () {
|
||||||
|
return function (text) {
|
||||||
|
if (text) {
|
||||||
|
return _.str.capitalize(text);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
.filter('linkify', function () {
|
||||||
|
return function (text) {
|
||||||
|
return text.replace(urlPattern, "$1<a href='$2' target='_blank'>$2</a>");
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
.filter('markdown', ['$sce', function ($sce) {
|
||||||
|
return function (text) {
|
||||||
|
if (!text) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return $sce.trustAsHtml(marked(text));
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
|
||||||
|
.filter('trustAsHtml', ['$sce', function ($sce) {
|
||||||
|
return function (text) {
|
||||||
|
if (!text) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return $sce.trustAsHtml(text);
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
angular.module('highchart', [])
|
|
||||||
.directive('chart', ['$timeout', function ($timeout) {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
template: '<div></div>',
|
|
||||||
scope: {
|
|
||||||
options: "=options",
|
|
||||||
series: "=series"
|
|
||||||
},
|
|
||||||
transclude: true,
|
|
||||||
replace: true,
|
|
||||||
|
|
||||||
link: function (scope, element, attrs) {
|
|
||||||
var chartsDefaults = {
|
|
||||||
chart: {
|
|
||||||
renderTo: element[0],
|
|
||||||
type: attrs.type || null,
|
|
||||||
height: attrs.height || null,
|
|
||||||
width: attrs.width || null
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var deepCopy = true;
|
|
||||||
var newSettings = {};
|
|
||||||
$.extend(deepCopy, newSettings, chartsDefaults, scope.options);
|
|
||||||
|
|
||||||
// Making sure that the DOM is ready before creating the chart element, so it gets proper width.
|
|
||||||
$timeout(function(){
|
|
||||||
scope.chart = new Highcharts.Chart(newSettings);
|
|
||||||
|
|
||||||
//Update when charts data changes
|
|
||||||
scope.$watch(function () {
|
|
||||||
return (scope.series && scope.series.length) || 0;
|
|
||||||
}, function (length) {
|
|
||||||
if (!length || length == 0) {
|
|
||||||
scope.chart.showLoading();
|
|
||||||
} else {
|
|
||||||
|
|
||||||
while(scope.chart.series.length > 0) {
|
|
||||||
scope.chart.series[0].remove(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.chart.counters.color = 0;
|
|
||||||
|
|
||||||
|
|
||||||
_.each(scope.series, function(s) {
|
|
||||||
scope.chart.addSeries(s);
|
|
||||||
})
|
|
||||||
|
|
||||||
scope.chart.redraw();
|
|
||||||
scope.chart.hideLoading();
|
|
||||||
};
|
|
||||||
}, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}]);
|
|
||||||
414
rd_ui/app/scripts/ng_highchart.js
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var ColorPalette = {
|
||||||
|
'Blue':'#4572A7',
|
||||||
|
'Red':'#AA4643',
|
||||||
|
'Green': '#89A54E',
|
||||||
|
'Purple': '#80699B',
|
||||||
|
'Cyan': '#3D96AE',
|
||||||
|
'Orange': '#DB843D',
|
||||||
|
'Light Blue': '#92A8CD',
|
||||||
|
'Lilac': '#A47D7C',
|
||||||
|
'Light Green': '#B5CA92',
|
||||||
|
};
|
||||||
|
|
||||||
|
Highcharts.setOptions({
|
||||||
|
colors: _.values(ColorPalette)
|
||||||
|
});
|
||||||
|
|
||||||
|
var defaultOptions = {
|
||||||
|
title: {
|
||||||
|
"text": null
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'datetime'
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
title: {
|
||||||
|
text: null
|
||||||
|
},
|
||||||
|
// showEmpty: true // by default
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: {
|
||||||
|
text: null
|
||||||
|
},
|
||||||
|
opposite: true,
|
||||||
|
showEmpty: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
tooltip: {
|
||||||
|
valueDecimals: 2,
|
||||||
|
formatter: function () {
|
||||||
|
if (!this.points) {
|
||||||
|
this.points = [this.point];
|
||||||
|
}
|
||||||
|
;
|
||||||
|
|
||||||
|
if (moment.isMoment(this.x)) {
|
||||||
|
var s = '<b>' + this.x.toDate().toLocaleString() + '</b>',
|
||||||
|
pointsCount = this.points.length;
|
||||||
|
|
||||||
|
$.each(this.points, function (i, point) {
|
||||||
|
s += '<br/><span style="color:' + point.series.color + '">' + point.series.name + '</span>: ' +
|
||||||
|
Highcharts.numberFormat(point.y);
|
||||||
|
|
||||||
|
if (pointsCount > 1 && point.percentage) {
|
||||||
|
s += " (" + Highcharts.numberFormat(point.percentage) + "%)";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var points = this.points;
|
||||||
|
var name = points[0].key || points[0].name;
|
||||||
|
|
||||||
|
var s = "<b>" + name + "</b>";
|
||||||
|
|
||||||
|
$.each(points, function (i, point) {
|
||||||
|
if (points.length > 1) {
|
||||||
|
s += '<br/><span style="color:' + point.series.color + '">' + point.series.name + '</span>: ' + Highcharts.numberFormat(point.y);
|
||||||
|
} else {
|
||||||
|
s += ": " + Highcharts.numberFormat(point.y);
|
||||||
|
if (point.percentage < 100) {
|
||||||
|
s += ' (' + Highcharts.numberFormat(point.percentage) + '%)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
},
|
||||||
|
shared: true
|
||||||
|
},
|
||||||
|
exporting: {
|
||||||
|
chartOptions: {
|
||||||
|
title: {
|
||||||
|
text: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
contextButton: {
|
||||||
|
menuItems: [
|
||||||
|
{
|
||||||
|
text: 'Toggle % Stacking',
|
||||||
|
onclick: function () {
|
||||||
|
var newStacking = "normal";
|
||||||
|
if (this.series[0].options.stacking == "normal") {
|
||||||
|
newStacking = "percent";
|
||||||
|
}
|
||||||
|
|
||||||
|
_.each(this.series, function (series) {
|
||||||
|
series.update({stacking: newStacking}, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Select All',
|
||||||
|
onclick: function () {
|
||||||
|
_.each(this.series, function (s) {
|
||||||
|
s.setVisible(true, false);
|
||||||
|
});
|
||||||
|
this.redraw();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Unselect All',
|
||||||
|
onclick: function () {
|
||||||
|
_.each(this.series, function (s) {
|
||||||
|
s.setVisible(false, false);
|
||||||
|
});
|
||||||
|
this.redraw();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Show Total',
|
||||||
|
onclick: function () {
|
||||||
|
var hasTotalsAlready = _.some(this.series, function (s) {
|
||||||
|
var res = (s.name == 'Total');
|
||||||
|
//if 'Total' already exists - just make it visible
|
||||||
|
if (res) s.setVisible(true, false);
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
var data = {};
|
||||||
|
_.each(this.series, function (s) {
|
||||||
|
if (s.name != 'Total') s.setVisible(false, false);
|
||||||
|
if (!hasTotalsAlready) {
|
||||||
|
_.each(s.data, function (p) {
|
||||||
|
data[p.x] = data[p.x] || {'x': p.x, 'y': 0};
|
||||||
|
data[p.x].y = data[p.x].y + p.y;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasTotalsAlready) {
|
||||||
|
this.addSeries({
|
||||||
|
data: _.values(data),
|
||||||
|
type: 'line',
|
||||||
|
name: 'Total'
|
||||||
|
}, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.redraw();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Save Image',
|
||||||
|
onclick: function () {
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
window.canvg(canvas, this.getSVG());
|
||||||
|
var href = canvas.toDataURL('image/png');
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = href;
|
||||||
|
var filenameSuffix = new Date().toISOString().replace(/:/g,'_').replace('Z', '');
|
||||||
|
if (this.title) {
|
||||||
|
filenameSuffix = this.title.text;
|
||||||
|
}
|
||||||
|
a.download = 'redash_charts_'+filenameSuffix+'.png';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
credits: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
plotOptions: {
|
||||||
|
area: {
|
||||||
|
marker: {
|
||||||
|
enabled: false,
|
||||||
|
symbol: 'circle',
|
||||||
|
radius: 2,
|
||||||
|
states: {
|
||||||
|
hover: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
stacking: "normal",
|
||||||
|
pointPadding: 0,
|
||||||
|
borderWidth: 1,
|
||||||
|
groupPadding: 0,
|
||||||
|
shadow: false
|
||||||
|
},
|
||||||
|
line: {
|
||||||
|
marker: {
|
||||||
|
radius: 1
|
||||||
|
},
|
||||||
|
lineWidth: 2,
|
||||||
|
states: {
|
||||||
|
hover: {
|
||||||
|
lineWidth: 2,
|
||||||
|
marker: {
|
||||||
|
radius: 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pie: {
|
||||||
|
allowPointSelect: true,
|
||||||
|
cursor: 'pointer',
|
||||||
|
dataLabels: {
|
||||||
|
enabled: true,
|
||||||
|
color: '#000000',
|
||||||
|
connectorColor: '#000000',
|
||||||
|
format: '<b>{point.name}</b>: {point.y} ({point.percentage:.1f} %)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scatter: {
|
||||||
|
marker: {
|
||||||
|
radius: 5,
|
||||||
|
states: {
|
||||||
|
hover: {
|
||||||
|
enabled: true,
|
||||||
|
lineColor: 'rgb(100,100,100)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
headerFormat: '<b>{series.name}</b><br>',
|
||||||
|
pointFormat: '{point.x}, {point.y}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: []
|
||||||
|
};
|
||||||
|
|
||||||
|
angular.module('highchart', [])
|
||||||
|
.constant('ColorPalette', ColorPalette)
|
||||||
|
.directive('chart', ['$timeout', function ($timeout) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
template: '<div></div>',
|
||||||
|
scope: {
|
||||||
|
options: "=options",
|
||||||
|
series: "=series"
|
||||||
|
},
|
||||||
|
transclude: true,
|
||||||
|
replace: true,
|
||||||
|
|
||||||
|
link: function (scope, element, attrs) {
|
||||||
|
var chartsDefaults = {
|
||||||
|
chart: {
|
||||||
|
renderTo: element[0],
|
||||||
|
type: attrs.type || null,
|
||||||
|
height: attrs.height || null,
|
||||||
|
width: attrs.width || null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var chartOptions = $.extend(true, {}, defaultOptions, chartsDefaults);
|
||||||
|
|
||||||
|
// $timeout makes sure that this function invoked after the DOM ready. When draw/init
|
||||||
|
// invoked after the DOM is ready, we see first an empty HighCharts objects and later
|
||||||
|
// they get filled up. Which gives the feeling that the charts loading faster (otherwise
|
||||||
|
// we stare at an empty screen until the HighCharts object is ready).
|
||||||
|
$timeout(function () {
|
||||||
|
// Update when options change
|
||||||
|
scope.$watch('options', function (newOptions) {
|
||||||
|
initChart(newOptions);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
//Update when charts data changes
|
||||||
|
scope.$watchCollection('series', function (series) {
|
||||||
|
if (!series || series.length == 0) {
|
||||||
|
scope.chart.showLoading();
|
||||||
|
} else {
|
||||||
|
drawChart();
|
||||||
|
}
|
||||||
|
;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function initChart(options) {
|
||||||
|
if (scope.chart) {
|
||||||
|
scope.chart.destroy();
|
||||||
|
}
|
||||||
|
;
|
||||||
|
|
||||||
|
$.extend(true, chartOptions, options);
|
||||||
|
|
||||||
|
scope.chart = new Highcharts.Chart(chartOptions);
|
||||||
|
drawChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawChart() {
|
||||||
|
while (scope.chart.series.length > 0) {
|
||||||
|
scope.chart.series[0].remove(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// We check either for true or undefined for backward compatibility.
|
||||||
|
var series = scope.series;
|
||||||
|
|
||||||
|
|
||||||
|
// If this is a chart that has just one row for multiple columns, sort
|
||||||
|
// by the Y values. For example:
|
||||||
|
//
|
||||||
|
// A | B | C
|
||||||
|
// 20 | 30 | 15
|
||||||
|
//
|
||||||
|
// Will be sorted:
|
||||||
|
// C | A | B
|
||||||
|
// 15 | 20 | 30
|
||||||
|
var sortable = _.every(series, function(s) { return s.data.length == 1 });
|
||||||
|
|
||||||
|
if (sortable) {
|
||||||
|
series = _.sortBy(series, function (s) {
|
||||||
|
return s.data[0].y
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('xAxis' in chartOptions && 'type' in chartOptions['xAxis'])) {
|
||||||
|
if (series.length > 0 && _.some(series[0].data, function (p) {
|
||||||
|
return (angular.isString(p.x) || angular.isDefined(p.name));
|
||||||
|
})) {
|
||||||
|
chartOptions['xAxis'] = chartOptions['xAxis'] || {};
|
||||||
|
chartOptions['xAxis']['type'] = 'category';
|
||||||
|
} else {
|
||||||
|
chartOptions['xAxis'] = chartOptions['xAxis'] || {};
|
||||||
|
chartOptions['xAxis']['type'] = 'datetime';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chartOptions['xAxis']['type'] == 'category' || chartOptions['series']['type']=='pie') {
|
||||||
|
if (!angular.isDefined(series[0].data[0].name)) {
|
||||||
|
// We need to make sure that for each category, each series has a value.
|
||||||
|
var categories = _.union.apply(this, _.map(series, function (s) {
|
||||||
|
return _.pluck(s.data, 'x')
|
||||||
|
}));
|
||||||
|
|
||||||
|
_.each(series, function (s) {
|
||||||
|
// TODO: move this logic to Query#getChartData
|
||||||
|
var yValues = _.groupBy(s.data, 'x');
|
||||||
|
|
||||||
|
var newData = _.map(categories, function (category) {
|
||||||
|
return {
|
||||||
|
name: category,
|
||||||
|
y: (yValues[category] && yValues[category][0].y) || 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
s.data = newData;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chartOptions['sortX'] === true || chartOptions['sortX'] === undefined) {
|
||||||
|
var seriesCopy = [];
|
||||||
|
|
||||||
|
_.each(series, function (s) {
|
||||||
|
// make a copy of series data, so we don't override original.
|
||||||
|
var fieldName = 'x';
|
||||||
|
if (s.data.length > 0 && _.has(s.data[0], 'name')) {
|
||||||
|
fieldName = 'name';
|
||||||
|
};
|
||||||
|
|
||||||
|
var sorted = _.extend({}, s, {data: _.sortBy(s.data, fieldName)});
|
||||||
|
seriesCopy.push(sorted);
|
||||||
|
});
|
||||||
|
|
||||||
|
series = seriesCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.chart.counters.color = 0;
|
||||||
|
|
||||||
|
_.each(series, function (s) {
|
||||||
|
// here we override the series with the visualization config
|
||||||
|
s = _.extend(s, chartOptions['series']);
|
||||||
|
|
||||||
|
if (s.type == 'area') {
|
||||||
|
_.each(s.data, function (p) {
|
||||||
|
// This is an insane hack: somewhere deep in HighChart's code,
|
||||||
|
// when you stack areas, it tries to convert the string representation
|
||||||
|
// of point's x into a number. With the default implementation of toString
|
||||||
|
// it fails....
|
||||||
|
|
||||||
|
if (moment.isMoment(p.x)) {
|
||||||
|
p.x.toString = function () {
|
||||||
|
return String(this.toDate().getTime());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
;
|
||||||
|
|
||||||
|
scope.chart.addSeries(s, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.chart.redraw();
|
||||||
|
scope.chart.hideLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}]);
|
||||||
|
})();
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
sortPredicate: '',
|
sortPredicate: '',
|
||||||
formatFunction: '',
|
formatFunction: '',
|
||||||
formatParameter: '',
|
formatParameter: '',
|
||||||
filterPredicate: '',
|
filterPredicate: undefined,
|
||||||
cellTemplateUrl: '',
|
cellTemplateUrl: '',
|
||||||
headerClass: '',
|
headerClass: '',
|
||||||
cellClass: ''
|
cellClass: ''
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
|
|
||||||
//insert columns from column config
|
//insert columns from column config
|
||||||
//TODO add a way to clean all columns
|
//TODO add a way to clean all columns
|
||||||
scope.$watch('columnCollection', function (oldValue, newValue) {
|
scope.$watchCollection('columnCollection', function (oldValue, newValue) {
|
||||||
if (scope.columnCollection) {
|
if (scope.columnCollection) {
|
||||||
scope.columns.length = 0;
|
scope.columns.length = 0;
|
||||||
for (var i = 0, l = scope.columnCollection.length; i < l; i++) {
|
for (var i = 0, l = scope.columnCollection.length; i < l; i++) {
|
||||||
@@ -113,8 +113,10 @@
|
|||||||
|
|
||||||
//if item are added or removed into the data model from outside the grid
|
//if item are added or removed into the data model from outside the grid
|
||||||
scope.$watch('dataCollection', function (oldValue, newValue) {
|
scope.$watch('dataCollection', function (oldValue, newValue) {
|
||||||
if (oldValue !== newValue) {
|
// evme:
|
||||||
ctrl.sortBy();//it will trigger the refresh... some hack ?
|
// reset sorting when data updates (executing query again)
|
||||||
|
if (newValue) {
|
||||||
|
ctrl.resetSort();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -184,7 +186,7 @@
|
|||||||
replace: false,
|
replace: false,
|
||||||
link: function (scope, element, attr, ctrl) {
|
link: function (scope, element, attr, ctrl) {
|
||||||
|
|
||||||
scope.searchValue = '';
|
scope.searchValue = undefined;
|
||||||
|
|
||||||
scope.$watch('searchValue', function (value) {
|
scope.$watch('searchValue', function (value) {
|
||||||
//todo perf improvement only filter on blur ?
|
//todo perf improvement only filter on blur ?
|
||||||
@@ -203,11 +205,10 @@
|
|||||||
column = scope.column,
|
column = scope.column,
|
||||||
row = scope.dataRow,
|
row = scope.dataRow,
|
||||||
format = filter('format'),
|
format = filter('format'),
|
||||||
getter = parse(column.map),
|
|
||||||
childScope;
|
childScope;
|
||||||
|
|
||||||
//can be useful for child directives
|
//can be useful for child directives
|
||||||
scope.formatedValue = format(getter(row), column.formatFunction, column.formatParameter);
|
scope.formatedValue = format(row[column.map], column.formatFunction, column.formatParameter);
|
||||||
|
|
||||||
function defaultContent() {
|
function defaultContent() {
|
||||||
//clear content
|
//clear content
|
||||||
@@ -215,7 +216,7 @@
|
|||||||
element.html('<div editable-cell="" row="dataRow" column="column" type="column.type"></div>');
|
element.html('<div editable-cell="" row="dataRow" column="column" type="column.type"></div>');
|
||||||
compile(element.contents())(scope);
|
compile(element.contents())(scope);
|
||||||
} else {
|
} else {
|
||||||
element.text(scope.formatedValue);
|
element.html(scope.formatedValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,12 +266,11 @@
|
|||||||
replace: true,
|
replace: true,
|
||||||
link: function (scope, element, attrs, ctrl) {
|
link: function (scope, element, attrs, ctrl) {
|
||||||
var form = angular.element(element.children()[1]),
|
var form = angular.element(element.children()[1]),
|
||||||
input = angular.element(form.children()[0]),
|
input = angular.element(form.children()[0]);
|
||||||
getter = parse(scope.column.map);
|
|
||||||
|
|
||||||
//init values
|
//init values
|
||||||
scope.isEditMode = false;
|
scope.isEditMode = false;
|
||||||
scope.value = getter(scope.row);
|
scope.value = scope.row[scope.column.map];
|
||||||
|
|
||||||
|
|
||||||
scope.submit = function () {
|
scope.submit = function () {
|
||||||
@@ -283,7 +283,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
scope.toggleEditMode = function () {
|
scope.toggleEditMode = function () {
|
||||||
scope.value = getter(scope.row);
|
scope.value = scope.row[scope.column.map];
|
||||||
scope.isEditMode = scope.isEditMode !== true;
|
scope.isEditMode = scope.isEditMode !== true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -381,7 +381,10 @@
|
|||||||
function sortDataRow(array, column) {
|
function sortDataRow(array, column) {
|
||||||
var sortAlgo = (scope.sortAlgorithm && angular.isFunction(scope.sortAlgorithm)) === true ? scope.sortAlgorithm : filter('orderBy');
|
var sortAlgo = (scope.sortAlgorithm && angular.isFunction(scope.sortAlgorithm)) === true ? scope.sortAlgorithm : filter('orderBy');
|
||||||
if (column) {
|
if (column) {
|
||||||
return arrayUtility.sort(array, sortAlgo, column.sortPredicate, column.reverse);
|
var predicate = function(o) {
|
||||||
|
return o[column.sortPredicate];
|
||||||
|
};
|
||||||
|
return arrayUtility.sort(array, sortAlgo, predicate, column.reverse);
|
||||||
} else {
|
} else {
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
@@ -464,14 +467,13 @@
|
|||||||
* @param column
|
* @param column
|
||||||
*/
|
*/
|
||||||
this.search = function (input, column) {
|
this.search = function (input, column) {
|
||||||
|
|
||||||
//update column and global predicate
|
//update column and global predicate
|
||||||
if (column && scope.columns.indexOf(column) !== -1) {
|
if (column && scope.columns.indexOf(column) !== -1) {
|
||||||
predicate.$ = '';
|
predicate.$ = '';
|
||||||
column.filterPredicate = input;
|
column.filterPredicate = input;
|
||||||
} else {
|
} else {
|
||||||
for (var j = 0, l = scope.columns.length; j < l; j++) {
|
for (var j = 0, l = scope.columns.length; j < l; j++) {
|
||||||
scope.columns[j].filterPredicate = '';
|
scope.columns[j].filterPredicate = undefined;
|
||||||
}
|
}
|
||||||
predicate.$ = input;
|
predicate.$ = input;
|
||||||
}
|
}
|
||||||
@@ -497,6 +499,12 @@
|
|||||||
return scope.isPaginationEnabled ? arrayUtility.fromTo(output, (scope.currentPage - 1) * scope.itemsByPage, scope.itemsByPage) : output;
|
return scope.isPaginationEnabled ? arrayUtility.fromTo(output, (scope.currentPage - 1) * scope.itemsByPage, scope.itemsByPage) : output;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.resetSort = function() {
|
||||||
|
lastColumnSort = null;
|
||||||
|
predicate = {};
|
||||||
|
this.sortBy();
|
||||||
|
};
|
||||||
|
|
||||||
/*////////////
|
/*////////////
|
||||||
Column API
|
Column API
|
||||||
///////////*/
|
///////////*/
|
||||||
@@ -588,13 +596,11 @@
|
|||||||
*/
|
*/
|
||||||
this.updateDataRow = function (dataRow, propertyName, newValue) {
|
this.updateDataRow = function (dataRow, propertyName, newValue) {
|
||||||
var index = scope.displayedCollection.indexOf(dataRow),
|
var index = scope.displayedCollection.indexOf(dataRow),
|
||||||
getter = parse(propertyName),
|
|
||||||
setter = getter.assign,
|
|
||||||
oldValue;
|
oldValue;
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
oldValue = getter(scope.displayedCollection[index]);
|
oldValue = scope.displayedCollection[index][propertyName];
|
||||||
if (oldValue !== newValue) {
|
if (oldValue !== newValue) {
|
||||||
setter(scope.displayedCollection[index], newValue);
|
scope.displayedCollection[index][propertyName] = newValue;
|
||||||
scope.$emit('updateDataRow', {item: scope.displayedCollection[index]});
|
scope.$emit('updateDataRow', {item: scope.displayedCollection[index]});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
var renderers = angular.module('redash.renderers', []);
|
|
||||||
var defaultChartOptions = {
|
|
||||||
"title": {
|
|
||||||
"text": null
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
valueDecimals: 2
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
type: 'datetime'
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
title: {
|
|
||||||
text: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
exporting: {
|
|
||||||
chartOptions: {
|
|
||||||
title: {
|
|
||||||
text: this.description
|
|
||||||
}
|
|
||||||
},
|
|
||||||
buttons: {
|
|
||||||
contextButton: {
|
|
||||||
menuItems: [
|
|
||||||
{
|
|
||||||
text: 'Toggle % Stacking',
|
|
||||||
onclick: function () {
|
|
||||||
var newStacking = "normal";
|
|
||||||
if (this.series[0].options.stacking == "normal") {
|
|
||||||
newStacking = "percent";
|
|
||||||
}
|
|
||||||
|
|
||||||
_.each(this.series, function (series) {
|
|
||||||
series.update({stacking: newStacking}, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
credits: {
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
plotOptions: {
|
|
||||||
"column": {
|
|
||||||
"stacking": "normal",
|
|
||||||
"pointPadding": 0,
|
|
||||||
"borderWidth": 1,
|
|
||||||
"groupPadding": 0,
|
|
||||||
"shadow": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"series": []
|
|
||||||
};
|
|
||||||
|
|
||||||
renderers.directive('chartRenderer', function () {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
scope: {
|
|
||||||
queryResult: '=',
|
|
||||||
stacking: '&'
|
|
||||||
},
|
|
||||||
template: "<chart options='chartOptions' series='chartSeries' class='graph'></chart>",
|
|
||||||
replace: false,
|
|
||||||
controller: ['$scope', function ($scope) {
|
|
||||||
$scope.chartSeries = [];
|
|
||||||
$scope.chartOptions = defaultChartOptions;
|
|
||||||
|
|
||||||
$scope.$watch('queryResult && queryResult.getData()', function (data) {
|
|
||||||
if (!data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($scope.queryResult.getData() == null) {
|
|
||||||
$scope.chartSeries.splice(0, $scope.chartSeries.length);
|
|
||||||
} else {
|
|
||||||
$scope.chartSeries.splice(0, $scope.chartSeries.length);
|
|
||||||
|
|
||||||
var stacking = null;
|
|
||||||
if ($scope.stacking() === undefined) {
|
|
||||||
stacking = 'normal';
|
|
||||||
}
|
|
||||||
|
|
||||||
_.each($scope.queryResult.getChartData(), function (s) {
|
|
||||||
$scope.chartSeries.push(_.extend(s, {'stacking': stacking}));
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: move this to the parent controller
|
|
||||||
// notifications.showNotification("RedDash", $scope.query.name + " updated.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
renderers.directive('gridRenderer', function () {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
scope: {
|
|
||||||
queryResult: '=',
|
|
||||||
itemsPerPage: '='
|
|
||||||
},
|
|
||||||
templateUrl: "/views/grid_renderer.html",
|
|
||||||
replace: false,
|
|
||||||
controller: ['$scope', function ($scope) {
|
|
||||||
$scope.gridColumns = [];
|
|
||||||
$scope.gridData = [];
|
|
||||||
$scope.gridConfig = {
|
|
||||||
isPaginationEnabled: true,
|
|
||||||
itemsByPage: $scope.itemsPerPage || 15,
|
|
||||||
maxSize: 8
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.$watch('queryResult && queryResult.getData()', function (data) {
|
|
||||||
if (!data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($scope.queryResult.getData() == null) {
|
|
||||||
$scope.gridColumns = [];
|
|
||||||
$scope.gridData = [];
|
|
||||||
$scope.filters = [];
|
|
||||||
} else {
|
|
||||||
|
|
||||||
|
|
||||||
$scope.filters = $scope.queryResult.getFilters();
|
|
||||||
|
|
||||||
var gridData = _.map($scope.queryResult.getData(), function (row) {
|
|
||||||
var newRow = {};
|
|
||||||
_.each(row, function (val, key) {
|
|
||||||
// TODO: hack to detect date fields, needed only for backward compatability
|
|
||||||
if (val > 1000 * 1000 * 1000 * 100) {
|
|
||||||
newRow[$scope.queryResult.getColumnCleanName(key)] = moment(val);
|
|
||||||
} else {
|
|
||||||
newRow[$scope.queryResult.getColumnCleanName(key)] = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
return newRow;
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.gridColumns = _.map($scope.queryResult.getColumnCleanNames(), function (col, i) {
|
|
||||||
var columnDefinition = {
|
|
||||||
'label': $scope.queryResult.getColumnFriendlyNames()[i],
|
|
||||||
'map': col
|
|
||||||
};
|
|
||||||
|
|
||||||
if (gridData.length > 0) {
|
|
||||||
var exampleData = gridData[0][col];
|
|
||||||
if (angular.isNumber(exampleData)) {
|
|
||||||
columnDefinition['formatFunction'] = 'number';
|
|
||||||
columnDefinition['formatParameter'] = 2;
|
|
||||||
} else if (moment.isMoment(exampleData)) {
|
|
||||||
columnDefinition['formatFunction'] = function(value) {
|
|
||||||
return value.format("DD/MM/YY HH:mm");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return columnDefinition;
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.gridData = _.clone(gridData);
|
|
||||||
|
|
||||||
$scope.$watch('filters', function (filters) {
|
|
||||||
$scope.gridData = _.filter(gridData, function (row) {
|
|
||||||
return _.reduce(filters, function (memo, filter) {
|
|
||||||
if (filter.current == 'All') {
|
|
||||||
return memo && true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (memo && row[$scope.queryResult.getColumnCleanName(filter.name)] == filter.current);
|
|
||||||
}, true);
|
|
||||||
});
|
|
||||||
}, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
renderers.directive('pivotTableRenderer', function () {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
scope: {
|
|
||||||
queryResult: '='
|
|
||||||
},
|
|
||||||
template: "",
|
|
||||||
replace: false,
|
|
||||||
link: function($scope, element, attrs) {
|
|
||||||
$scope.$watch('queryResult && queryResult.getData()', function (data) {
|
|
||||||
if (!data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($scope.queryResult.getData() == null) {
|
|
||||||
} else {
|
|
||||||
$(element).pivotUI($scope.queryResult.getData(), {
|
|
||||||
renderers: $.pivotUtilities.renderers
|
|
||||||
}, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
renderers.directive('cohortRenderer', function() {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
scope: {
|
|
||||||
queryResult: '='
|
|
||||||
},
|
|
||||||
template: "",
|
|
||||||
replace: false,
|
|
||||||
link: function($scope, element, attrs) {
|
|
||||||
$scope.$watch('queryResult && queryResult.getData()', function (data) {
|
|
||||||
if (!data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($scope.queryResult.getData() == null) {
|
|
||||||
|
|
||||||
} else {
|
|
||||||
var sortedData = _.sortBy($scope.queryResult.getData(), "date");
|
|
||||||
var grouped = _.groupBy(sortedData, "date");
|
|
||||||
var data = _.map(grouped, function(values, date) {
|
|
||||||
var row = [values[0].total];
|
|
||||||
_.each(values, function(value) { row.push(value.value); });
|
|
||||||
return row;
|
|
||||||
});
|
|
||||||
|
|
||||||
var initialDate = moment(sortedData[0].date).toDate(),
|
|
||||||
container = angular.element(element)[0];
|
|
||||||
|
|
||||||
Cornelius.draw({
|
|
||||||
initialDate: initialDate,
|
|
||||||
container: container,
|
|
||||||
cohort: data,
|
|
||||||
title: null,
|
|
||||||
timeInterval: 'daily',
|
|
||||||
labels: {
|
|
||||||
time: 'Activation Day',
|
|
||||||
people: 'Users'
|
|
||||||
},
|
|
||||||
formatHeaderLabel: function (i) {
|
|
||||||
return "Day " + (i - 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
(function () {
|
|
||||||
var QueryResult = function($resource, $timeout) {
|
|
||||||
var QueryResultResource = $resource('/api/query_results/:id', {id: '@id'}, {'post': {'method': 'POST'}});
|
|
||||||
var Job = $resource('/api/jobs/:id', {id: '@id'});
|
|
||||||
|
|
||||||
var updateFunction = function (props) {
|
|
||||||
angular.extend(this, props);
|
|
||||||
if ('query_result' in props) {
|
|
||||||
this.status = "done";
|
|
||||||
|
|
||||||
_.each(this.query_result.data.rows, function (row) {
|
|
||||||
_.each(row, function (v, k) {
|
|
||||||
if (_.isString(v) && v.match(/^\d{4}-\d{2}-\d{2}/)) {
|
|
||||||
row[k] = moment(v);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (this.job.status == 3) {
|
|
||||||
this.status = "processing";
|
|
||||||
} else {
|
|
||||||
this.status = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function QueryResult(props) {
|
|
||||||
this.job = {};
|
|
||||||
this.query_result = {};
|
|
||||||
this.status = "waiting";
|
|
||||||
|
|
||||||
this.updatedAt = moment();
|
|
||||||
|
|
||||||
if (props) {
|
|
||||||
updateFunction.apply(this, [props]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var statuses = {
|
|
||||||
1: "waiting",
|
|
||||||
2: "processing",
|
|
||||||
3: "done",
|
|
||||||
4: "failed"
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResult.prototype.update = updateFunction;
|
|
||||||
|
|
||||||
QueryResult.prototype.getId = function() {
|
|
||||||
var id = null;
|
|
||||||
if ('query_result' in this) {
|
|
||||||
id = this.query_result.id;
|
|
||||||
}
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResult.prototype.getStatus = function() {
|
|
||||||
return this.status || statuses[this.job.status];
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResult.prototype.getError = function() {
|
|
||||||
// TODO: move this logic to the server...
|
|
||||||
if (this.job.error == "None") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.job.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResult.prototype.getUpdatedAt = function() {
|
|
||||||
return this.query_result.retrieved_at || this.job.updated_at*1000.0 || this.updatedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResult.prototype.getRuntime = function() {
|
|
||||||
return this.query_result.runtime;
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResult.prototype.getData = function() {
|
|
||||||
if (!this.query_result.data) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = this.query_result.data.rows;
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResult.prototype.getChartData = function () {
|
|
||||||
var series = {};
|
|
||||||
|
|
||||||
_.each(this.getData(), function (row) {
|
|
||||||
var point = {};
|
|
||||||
var seriesName = "";
|
|
||||||
var yName = "";
|
|
||||||
var xName = "";
|
|
||||||
|
|
||||||
_.map(row, function (value, definition) {
|
|
||||||
var type = definition.split("::")[1];
|
|
||||||
var name = definition.split("::")[0];
|
|
||||||
|
|
||||||
if (type == 'x') {
|
|
||||||
xName = name;
|
|
||||||
point[type] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == 'y') {
|
|
||||||
yName = name;
|
|
||||||
point[type] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == 'series') {
|
|
||||||
seriesName = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (seriesName == "") {
|
|
||||||
seriesName = yName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (series[seriesName] == undefined) {
|
|
||||||
series[seriesName] = {
|
|
||||||
name: seriesName,
|
|
||||||
type: 'column',
|
|
||||||
data: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
series[seriesName]['data'].push(point);
|
|
||||||
});
|
|
||||||
|
|
||||||
return _.values(series);
|
|
||||||
};
|
|
||||||
|
|
||||||
QueryResult.prototype.getColumns = function () {
|
|
||||||
if (this.columns == undefined) {
|
|
||||||
this.columns = _.map(this.query_result.data.columns, function(v) {
|
|
||||||
return v.name;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.columns;
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResult.prototype.getColumnCleanName = function (column) {
|
|
||||||
var parts = column.split('::');
|
|
||||||
var name = parts[1];
|
|
||||||
if (parts[0] != '') {
|
|
||||||
name = parts[0].replace(/ /g, '_').replace(/\?/g,'');
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResult.prototype.getColumnFriendlyName = function (column) {
|
|
||||||
return this.getColumnCleanName(column).replace(/_/g, ' ').replace(/(?:^|\s)\S/g, function (a) {
|
|
||||||
return a.toUpperCase();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResult.prototype.getColumnCleanNames = function () {
|
|
||||||
return _.map(this.getColumns(), function (col) {
|
|
||||||
return this.getColumnCleanName(col);
|
|
||||||
}, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResult.prototype.getColumnFriendlyNames = function () {
|
|
||||||
return _.map(this.getColumns(), function (col) {
|
|
||||||
return this.getColumnFriendlyName(col);
|
|
||||||
}, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResult.prototype.getFilters = function () {
|
|
||||||
var filterNames = [];
|
|
||||||
_.each(this.getColumns(), function (col) {
|
|
||||||
if (col.split('::')[1] == 'filter') {
|
|
||||||
filterNames.push(col);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var filterValues = [];
|
|
||||||
_.each(this.getData(), function (row) {
|
|
||||||
_.each(filterNames, function (filter, i) {
|
|
||||||
if (filterValues[i] == undefined) {
|
|
||||||
filterValues[i] = [];
|
|
||||||
}
|
|
||||||
filterValues[i].push(row[filter]);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
var filters = _.map(filterNames, function (filter, i) {
|
|
||||||
var f = {
|
|
||||||
name: filter,
|
|
||||||
friendlyName: this.getColumnFriendlyName(filter),
|
|
||||||
values: _.uniq(filterValues[i])
|
|
||||||
};
|
|
||||||
|
|
||||||
f.current = f.values[0];
|
|
||||||
return f;
|
|
||||||
}, this);
|
|
||||||
|
|
||||||
return filters;
|
|
||||||
};
|
|
||||||
|
|
||||||
var refreshStatus = function(queryResult, query, ttl) {
|
|
||||||
Job.get({'id': queryResult.job.id}, function(response) {
|
|
||||||
queryResult.update(response);
|
|
||||||
|
|
||||||
if (queryResult.getStatus() == "processing" && queryResult.job.query_result_id && queryResult.job.query_result_id != "None") {
|
|
||||||
QueryResultResource.get({'id': queryResult.job.query_result_id}, function(response) {
|
|
||||||
queryResult.update(response);
|
|
||||||
});
|
|
||||||
} else if (queryResult.getStatus() != "failed") {
|
|
||||||
$timeout(function () {
|
|
||||||
refreshStatus(queryResult, query, ttl);
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResult.getById = function (id) {
|
|
||||||
var queryResult = new QueryResult();
|
|
||||||
|
|
||||||
QueryResultResource.get({'id': id}, function (response) {
|
|
||||||
queryResult.update(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
return queryResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResult.get = function (query, ttl) {
|
|
||||||
var queryResult = new QueryResult();
|
|
||||||
|
|
||||||
QueryResultResource.post({'query': query, 'ttl': ttl}, function (response) {
|
|
||||||
queryResult.update(response);
|
|
||||||
|
|
||||||
if ('job' in response) {
|
|
||||||
refreshStatus(queryResult, query, ttl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return queryResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
return QueryResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
var Query = function ($resource, QueryResult) {
|
|
||||||
var Query = $resource('/api/queries/:id', {id: '@id'});
|
|
||||||
|
|
||||||
Query.prototype.getQueryResult = function(ttl) {
|
|
||||||
if (ttl == undefined) {
|
|
||||||
ttl = this.ttl;
|
|
||||||
}
|
|
||||||
|
|
||||||
var queryResult = null;
|
|
||||||
if (this.latest_query_data && ttl != 0) {
|
|
||||||
queryResult = new QueryResult({'query_result': this.latest_query_data});
|
|
||||||
} else if (this.latest_query_data_id && ttl != 0) {
|
|
||||||
queryResult = QueryResult.getById(this.latest_query_data_id);
|
|
||||||
} else {
|
|
||||||
queryResult = QueryResult.get(this.query, ttl);
|
|
||||||
}
|
|
||||||
|
|
||||||
return queryResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Query;
|
|
||||||
}
|
|
||||||
|
|
||||||
angular.module('redash.services', [])
|
|
||||||
.factory('QueryResult', ['$resource', '$timeout', QueryResult])
|
|
||||||
.factory('Query', ['$resource', 'QueryResult', Query])
|
|
||||||
|
|
||||||
})();
|
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
(function () {
|
(function () {
|
||||||
var Dashboard = function($resource) {
|
var Dashboard = function($resource) {
|
||||||
var resource = $resource('/api/dashboards/:slug', {slug: '@slug'});
|
var resource = $resource('/api/dashboards/:slug', {slug: '@slug'}, {
|
||||||
|
recent: {
|
||||||
|
method: 'get',
|
||||||
|
isArray: true,
|
||||||
|
url: "/api/dashboards/recent"
|
||||||
|
}});
|
||||||
|
|
||||||
resource.prototype.canEdit = function() {
|
resource.prototype.canEdit = function() {
|
||||||
return currentUser.is_admin || currentUser.name == this.user;
|
return currentUser.hasPermission('admin') || currentUser.canEdit(this);
|
||||||
}
|
}
|
||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
(function () {
|
(function () {
|
||||||
var notifications = function () {
|
var notifications = function (Events) {
|
||||||
var notificationService = {};
|
var notificationService = {};
|
||||||
var lastNotification = null;
|
|
||||||
|
|
||||||
notificationService.isSupported = function () {
|
notificationService.isSupported = function () {
|
||||||
if (window.webkitNotifications) {
|
if ("Notification" in window) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
console.log("HTML5 notifications are not supported.");
|
console.log("HTML5 notifications are not supported.");
|
||||||
@@ -17,8 +16,12 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!window.webkitNotifications.checkPermission() == 0) { // 0 is PERMISSION_ALLOWED
|
if (Notification.permission !== "granted") {
|
||||||
window.webkitNotifications.requestPermission();
|
Notification.requestPermission(function (status) {
|
||||||
|
if (Notification.permission !== status) {
|
||||||
|
Notification.permission = status;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,27 +30,21 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.webkitVisibilityState && document.webkitVisibilityState == 'visible') {
|
//using the 'tag' to avoid showing duplicate notifications
|
||||||
return;
|
var notification = new Notification(title, {'tag': title+content, 'body': content});
|
||||||
}
|
setTimeout(function(){
|
||||||
|
notification.close();
|
||||||
if (lastNotification) {
|
},3000);
|
||||||
lastNotification.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
var notification = window.webkitNotifications.createNotification('', title, content);
|
|
||||||
lastNotification = notification;
|
|
||||||
notification.onclick = function () {
|
notification.onclick = function () {
|
||||||
window.focus();
|
window.focus();
|
||||||
this.cancel();
|
this.cancel();
|
||||||
|
Events.record(currentUser, 'click', 'notification');
|
||||||
};
|
};
|
||||||
|
|
||||||
notification.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return notificationService;
|
return notificationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
angular.module('redash.services')
|
angular.module('redash.services')
|
||||||
.factory('notifications', notifications);
|
.factory('notifications', ['Events', notifications]);
|
||||||
})();
|
})();
|
||||||
|
|||||||
528
rd_ui/app/scripts/services/resources.js
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
(function () {
|
||||||
|
var QueryResult = function ($resource, $timeout, $q) {
|
||||||
|
var QueryResultResource = $resource('/api/query_results/:id', {id: '@id'}, {'post': {'method': 'POST'}});
|
||||||
|
var Job = $resource('/api/jobs/:id', {id: '@id'});
|
||||||
|
|
||||||
|
var updateFunction = function (props) {
|
||||||
|
angular.extend(this, props);
|
||||||
|
if ('query_result' in props) {
|
||||||
|
this.status = "done";
|
||||||
|
this.filters = undefined;
|
||||||
|
this.filterFreeze = undefined;
|
||||||
|
|
||||||
|
var columnTypes = {};
|
||||||
|
|
||||||
|
// TODO: we should stop manipulating incoming data, and switch to relaying on the column type set by the backend.
|
||||||
|
// This logic is prone to errors, and better be removed. Kept for now, for backward compatability.
|
||||||
|
_.each(this.query_result.data.rows, function (row) {
|
||||||
|
_.each(row, function (v, k) {
|
||||||
|
if (angular.isNumber(v)) {
|
||||||
|
columnTypes[k] = 'float';
|
||||||
|
} else if (_.isString(v) && v.match(/^\d{4}-\d{2}-\d{2}T/)) {
|
||||||
|
row[k] = moment(v);
|
||||||
|
columnTypes[k] = 'datetime';
|
||||||
|
} else if (_.isString(v) && v.match(/^\d{4}-\d{2}-\d{2}/)) {
|
||||||
|
row[k] = moment(v);
|
||||||
|
columnTypes[k] = 'date';
|
||||||
|
} else if (typeof(v) == 'object' && v !== null) {
|
||||||
|
row[k] = JSON.stringify(v);
|
||||||
|
}
|
||||||
|
}, this);
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
_.each(this.query_result.data.columns, function(column) {
|
||||||
|
if (columnTypes[column.name]) {
|
||||||
|
if (column.type == null || column.type == 'string') {
|
||||||
|
column.type = columnTypes[column.name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.deferred.resolve(this);
|
||||||
|
} else if (this.job.status == 3) {
|
||||||
|
this.status = "processing";
|
||||||
|
} else {
|
||||||
|
this.status = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueryResult(props) {
|
||||||
|
this.deferred = $q.defer();
|
||||||
|
this.job = {};
|
||||||
|
this.query_result = {};
|
||||||
|
this.status = "waiting";
|
||||||
|
this.filters = undefined;
|
||||||
|
this.filterFreeze = undefined;
|
||||||
|
|
||||||
|
this.updatedAt = moment();
|
||||||
|
|
||||||
|
if (props) {
|
||||||
|
updateFunction.apply(this, [props]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var statuses = {
|
||||||
|
1: "waiting",
|
||||||
|
2: "processing",
|
||||||
|
3: "done",
|
||||||
|
4: "failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResult.prototype.update = updateFunction;
|
||||||
|
|
||||||
|
QueryResult.prototype.getId = function () {
|
||||||
|
var id = null;
|
||||||
|
if ('query_result' in this) {
|
||||||
|
id = this.query_result.id;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResult.prototype.cancelExecution = function () {
|
||||||
|
Job.delete({id: this.job.id});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResult.prototype.getStatus = function () {
|
||||||
|
return this.status || statuses[this.job.status];
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResult.prototype.getError = function () {
|
||||||
|
// TODO: move this logic to the server...
|
||||||
|
if (this.job.error == "None") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.job.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResult.prototype.getUpdatedAt = function () {
|
||||||
|
return this.query_result.retrieved_at || this.job.updated_at * 1000.0 || this.updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResult.prototype.getRuntime = function () {
|
||||||
|
return this.query_result.runtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResult.prototype.getRawData = function () {
|
||||||
|
if (!this.query_result.data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = this.query_result.data.rows;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResult.prototype.getData = function () {
|
||||||
|
if (!this.query_result.data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var filterValues = function (filters) {
|
||||||
|
if (!filters) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.reduce(filters, function (str, filter) {
|
||||||
|
return str + filter.current;
|
||||||
|
}, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
var filters = this.getFilters();
|
||||||
|
var filterFreeze = filterValues(filters);
|
||||||
|
|
||||||
|
if (this.filterFreeze != filterFreeze) {
|
||||||
|
this.filterFreeze = filterFreeze;
|
||||||
|
|
||||||
|
if (filters) {
|
||||||
|
this.filteredData = _.filter(this.query_result.data.rows, function (row) {
|
||||||
|
return _.reduce(filters, function (memo, filter) {
|
||||||
|
if (!_.isArray(filter.current)) {
|
||||||
|
filter.current = [filter.current];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (memo && _.some(filter.current, function(v) {
|
||||||
|
// We compare with either the value or the String representation of the value,
|
||||||
|
// because Select2 casts true/false to "true"/"false".
|
||||||
|
return v == row[filter.name] || String(row[filter.name]) == v
|
||||||
|
}));
|
||||||
|
}, true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.filteredData = this.query_result.data.rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.filteredData;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResult.prototype.getChartData = function (mapping) {
|
||||||
|
var series = {};
|
||||||
|
|
||||||
|
_.each(this.getData(), function (row) {
|
||||||
|
var point = {};
|
||||||
|
var seriesName = undefined;
|
||||||
|
var xValue = 0;
|
||||||
|
var yValues = {};
|
||||||
|
|
||||||
|
_.each(row, function (value, definition) {
|
||||||
|
var name = definition.split("::")[0];
|
||||||
|
var type = definition.split("::")[1];
|
||||||
|
if (mapping) {
|
||||||
|
type = mapping[definition];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == 'unused') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == 'x') {
|
||||||
|
xValue = value;
|
||||||
|
point[type] = value;
|
||||||
|
}
|
||||||
|
if (type == 'y') {
|
||||||
|
if (value == null) {
|
||||||
|
value = 0;
|
||||||
|
}
|
||||||
|
yValues[name] = value;
|
||||||
|
point[type] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == 'series') {
|
||||||
|
seriesName = String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == 'multi-filter') {
|
||||||
|
seriesName = String(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var addPointToSeries = function (seriesName, point) {
|
||||||
|
if (series[seriesName] == undefined) {
|
||||||
|
series[seriesName] = {
|
||||||
|
name: seriesName,
|
||||||
|
type: 'column',
|
||||||
|
data: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
series[seriesName]['data'].push(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seriesName === undefined) {
|
||||||
|
_.each(yValues, function (yValue, seriesName) {
|
||||||
|
addPointToSeries(seriesName, {'x': xValue, 'y': yValue});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
addPointToSeries(seriesName, point);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return _.values(series);
|
||||||
|
};
|
||||||
|
|
||||||
|
QueryResult.prototype.getColumns = function () {
|
||||||
|
if (this.columns == undefined && this.query_result.data) {
|
||||||
|
this.columns = this.query_result.data.columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResult.prototype.getColumnNames = function () {
|
||||||
|
if (this.columnNames == undefined && this.query_result.data) {
|
||||||
|
this.columnNames = _.map(this.query_result.data.columns, function (v) {
|
||||||
|
return v.name;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.columnNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResult.prototype.getColumnNameWithoutType = function (column) {
|
||||||
|
var parts = column.split('::');
|
||||||
|
if (parts[0] == "" && parts.length == 2) {
|
||||||
|
return parts[1];
|
||||||
|
}
|
||||||
|
return parts[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
QueryResult.prototype.getColumnCleanName = function (column) {
|
||||||
|
var name = this.getColumnNameWithoutType(column);
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResult.prototype.getColumnFriendlyName = function (column) {
|
||||||
|
return this.getColumnNameWithoutType(column).replace(/(?:^|\s)\S/g, function (a) {
|
||||||
|
return a.toUpperCase();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResult.prototype.getColumnCleanNames = function () {
|
||||||
|
return _.map(this.getColumnNames(), function (col) {
|
||||||
|
return this.getColumnCleanName(col);
|
||||||
|
}, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResult.prototype.getColumnFriendlyNames = function () {
|
||||||
|
return _.map(this.getColumnNames(), function (col) {
|
||||||
|
return this.getColumnFriendlyName(col);
|
||||||
|
}, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResult.prototype.getFilters = function () {
|
||||||
|
if (!this.filters) {
|
||||||
|
this.prepareFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.filters;
|
||||||
|
};
|
||||||
|
|
||||||
|
QueryResult.prototype.prepareFilters = function () {
|
||||||
|
var filters = [];
|
||||||
|
var filterTypes = ['filter', 'multi-filter'];
|
||||||
|
_.each(this.getColumnNames(), function (col) {
|
||||||
|
var type = col.split('::')[1]
|
||||||
|
if (_.contains(filterTypes, type)) {
|
||||||
|
// filter found
|
||||||
|
var filter = {
|
||||||
|
name: col,
|
||||||
|
friendlyName: this.getColumnFriendlyName(col),
|
||||||
|
values: [],
|
||||||
|
multiple: (type=='multi-filter')
|
||||||
|
}
|
||||||
|
filters.push(filter);
|
||||||
|
}
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
_.each(this.getRawData(), function (row) {
|
||||||
|
_.each(filters, function (filter) {
|
||||||
|
filter.values.push(row[filter.name]);
|
||||||
|
if (filter.values.length == 1) {
|
||||||
|
filter.current = row[filter.name];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
_.each(filters, function(filter) {
|
||||||
|
filter.values = _.uniq(filter.values);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.filters = filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
var refreshStatus = function (queryResult, query) {
|
||||||
|
Job.get({'id': queryResult.job.id}, function (response) {
|
||||||
|
queryResult.update(response);
|
||||||
|
|
||||||
|
if (queryResult.getStatus() == "processing" && queryResult.job.query_result_id && queryResult.job.query_result_id != "None") {
|
||||||
|
QueryResultResource.get({'id': queryResult.job.query_result_id}, function (response) {
|
||||||
|
queryResult.update(response);
|
||||||
|
});
|
||||||
|
} else if (queryResult.getStatus() != "failed") {
|
||||||
|
$timeout(function () {
|
||||||
|
refreshStatus(queryResult, query);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResult.getById = function (id) {
|
||||||
|
var queryResult = new QueryResult();
|
||||||
|
|
||||||
|
QueryResultResource.get({'id': id}, function (response) {
|
||||||
|
queryResult.update(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
return queryResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
QueryResult.prototype.toPromise = function() {
|
||||||
|
return this.deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResult.get = function (data_source_id, query, maxAge, queryId) {
|
||||||
|
var queryResult = new QueryResult();
|
||||||
|
|
||||||
|
var params = {'data_source_id': data_source_id, 'query': query, 'max_age': maxAge};
|
||||||
|
if (queryId !== undefined) {
|
||||||
|
params['query_id'] = queryId;
|
||||||
|
};
|
||||||
|
|
||||||
|
QueryResultResource.post(params, function (response) {
|
||||||
|
queryResult.update(response);
|
||||||
|
|
||||||
|
if ('job' in response) {
|
||||||
|
refreshStatus(queryResult, query);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return queryResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return QueryResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
var Query = function ($resource, QueryResult, DataSource) {
|
||||||
|
var Query = $resource('/api/queries/:id', {id: '@id'},
|
||||||
|
{
|
||||||
|
search: {
|
||||||
|
method: 'get',
|
||||||
|
isArray: true,
|
||||||
|
url: "/api/queries/search"
|
||||||
|
},
|
||||||
|
recent: {
|
||||||
|
method: 'get',
|
||||||
|
isArray: true,
|
||||||
|
url: "/api/queries/recent"
|
||||||
|
}});
|
||||||
|
|
||||||
|
Query.newQuery = function () {
|
||||||
|
return new Query({
|
||||||
|
query: "",
|
||||||
|
name: "New Query",
|
||||||
|
schedule: null,
|
||||||
|
user: currentUser
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Query.collectParamsFromQueryString = function($location, query) {
|
||||||
|
var parameterNames = query.getParameters();
|
||||||
|
var parameters = {};
|
||||||
|
|
||||||
|
var queryString = $location.search();
|
||||||
|
_.each(parameterNames, function(param, i) {
|
||||||
|
var qsName = "p_" + param;
|
||||||
|
if (qsName in queryString) {
|
||||||
|
parameters[param] = queryString[qsName];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return parameters;
|
||||||
|
};
|
||||||
|
|
||||||
|
Query.prototype.getSourceLink = function () {
|
||||||
|
return '/queries/' + this.id + '/source';
|
||||||
|
};
|
||||||
|
|
||||||
|
Query.prototype.hasDailySchedule = function() {
|
||||||
|
return (this.schedule && this.schedule.match(/\d\d:\d\d/) !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Query.prototype.scheduleInLocalTime = function() {
|
||||||
|
var parts = this.schedule.split(':');
|
||||||
|
return moment.utc().hour(parts[0]).minute(parts[1]).local().format('HH:mm');
|
||||||
|
}
|
||||||
|
|
||||||
|
Query.prototype.getQueryResult = function (maxAge, parameters) {
|
||||||
|
// if (ttl == undefined) {
|
||||||
|
// ttl = this.ttl;
|
||||||
|
// }
|
||||||
|
|
||||||
|
var queryText = this.query;
|
||||||
|
|
||||||
|
var queryParameters = this.getParameters();
|
||||||
|
var paramsRequired = !_.isEmpty(queryParameters);
|
||||||
|
|
||||||
|
var missingParams = parameters === undefined ? queryParameters : _.difference(queryParameters, _.keys(parameters));
|
||||||
|
|
||||||
|
if (paramsRequired && missingParams.length > 0) {
|
||||||
|
var paramsWord = "parameter";
|
||||||
|
if (missingParams.length > 1) {
|
||||||
|
paramsWord = "parameters";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new QueryResult({job: {error: "Missing values for " + missingParams.join(', ') + " "+paramsWord+".", status: 4}});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paramsRequired) {
|
||||||
|
queryText = Mustache.render(queryText, parameters);
|
||||||
|
|
||||||
|
// Need to clear latest results, to make sure we don't used results for different params.
|
||||||
|
this.latest_query_data = null;
|
||||||
|
this.latest_query_data_id = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.latest_query_data && maxAge != 0) {
|
||||||
|
if (!this.queryResult) {
|
||||||
|
this.queryResult = new QueryResult({'query_result': this.latest_query_data});
|
||||||
|
}
|
||||||
|
} else if (this.latest_query_data_id && maxAge != 0) {
|
||||||
|
if (!this.queryResult) {
|
||||||
|
this.queryResult = QueryResult.getById(this.latest_query_data_id);
|
||||||
|
}
|
||||||
|
} else if (this.data_source_id) {
|
||||||
|
this.queryResult = QueryResult.get(this.data_source_id, queryText, maxAge, this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.queryResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
Query.prototype.getQueryResultPromise = function() {
|
||||||
|
return this.getQueryResult().toPromise();
|
||||||
|
};
|
||||||
|
|
||||||
|
Query.prototype.getParameters = function() {
|
||||||
|
var parts = Mustache.parse(this.query);
|
||||||
|
var parameters = [];
|
||||||
|
var collectParams = function(parts) {
|
||||||
|
parameters = [];
|
||||||
|
_.each(parts, function(part) {
|
||||||
|
if (part[0] == 'name' || part[0] == '&') {
|
||||||
|
parameters.push(part[1]);
|
||||||
|
} else if (part[0] == '#') {
|
||||||
|
parameters = _.union(parameters, collectParams(part[4]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return parameters;
|
||||||
|
};
|
||||||
|
|
||||||
|
parameters = collectParams(parts);
|
||||||
|
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Query;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var DataSource = function ($resource) {
|
||||||
|
var actions = {
|
||||||
|
'get': {'method': 'GET', 'cache': true, 'isArray': true},
|
||||||
|
'getSchema': {'method': 'GET', 'cache': true, 'isArray': true, 'url': '/api/data_sources/:id/schema'}
|
||||||
|
};
|
||||||
|
|
||||||
|
var DataSourceResource = $resource('/api/data_sources/:id', {id: '@id'}, actions);
|
||||||
|
|
||||||
|
return DataSourceResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
var Widget = function ($resource, Query) {
|
||||||
|
var WidgetResource = $resource('/api/widgets/:id', {id: '@id'});
|
||||||
|
|
||||||
|
WidgetResource.prototype.getQuery = function () {
|
||||||
|
if (!this.query && this.visualization) {
|
||||||
|
this.query = new Query(this.visualization.query);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.query;
|
||||||
|
};
|
||||||
|
|
||||||
|
WidgetResource.prototype.getName = function () {
|
||||||
|
if (this.visualization) {
|
||||||
|
return this.visualization.query.name + ' (' + this.visualization.name + ')';
|
||||||
|
}
|
||||||
|
return _.str.truncate(this.text, 20);
|
||||||
|
};
|
||||||
|
|
||||||
|
return WidgetResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('redash.services')
|
||||||
|
.factory('QueryResult', ['$resource', '$timeout', '$q', QueryResult])
|
||||||
|
.factory('Query', ['$resource', 'QueryResult', 'DataSource', Query])
|
||||||
|
.factory('DataSource', ['$resource', DataSource])
|
||||||
|
.factory('Widget', ['$resource', 'Query', Widget]);
|
||||||
|
})();
|
||||||
52
rd_ui/app/scripts/services/services.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
(function () {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
function KeyboardShortcuts() {
|
||||||
|
this.bind = function bind(keymap) {
|
||||||
|
_.forEach(keymap, function (fn, key) {
|
||||||
|
Mousetrap.bindGlobal(key, function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
fn();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.unbind = function unbind(keymap) {
|
||||||
|
_.forEach(keymap, function (fn, key) {
|
||||||
|
Mousetrap.unbind(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Events($http) {
|
||||||
|
this.events = [];
|
||||||
|
|
||||||
|
this.post = _.debounce(function() {
|
||||||
|
var events = this.events;
|
||||||
|
this.events = [];
|
||||||
|
|
||||||
|
$http.post('/api/events', events);
|
||||||
|
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
this.record = function (user, action, object_type, object_id, additional_properties) {
|
||||||
|
|
||||||
|
var event = {
|
||||||
|
"user_id": user.id,
|
||||||
|
"action": action,
|
||||||
|
"object_type": object_type,
|
||||||
|
"object_id": object_id,
|
||||||
|
"timestamp": Date.now()/1000.0
|
||||||
|
};
|
||||||
|
_.extend(event, additional_properties);
|
||||||
|
this.events.push(event);
|
||||||
|
|
||||||
|
this.post();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('redash.services', [])
|
||||||
|
.service('KeyboardShortcuts', [KeyboardShortcuts])
|
||||||
|
.service('Events', ['$http', Events])
|
||||||
|
})();
|
||||||
201
rd_ui/app/scripts/visualizations/base.js
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
(function () {
|
||||||
|
var VisualizationProvider = function () {
|
||||||
|
this.visualizations = {};
|
||||||
|
this.visualizationTypes = {};
|
||||||
|
var defaultConfig = {
|
||||||
|
defaultOptions: {},
|
||||||
|
skipTypes: false,
|
||||||
|
editorTemplate: null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registerVisualization = function (config) {
|
||||||
|
var visualization = _.extend({}, defaultConfig, config);
|
||||||
|
|
||||||
|
// TODO: this is prone to errors; better refactor.
|
||||||
|
if (_.isEmpty(this.visualizations)) {
|
||||||
|
this.defaultVisualization = visualization;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.visualizations[config.type] = visualization;
|
||||||
|
|
||||||
|
if (!config.skipTypes) {
|
||||||
|
this.visualizationTypes[config.name] = config.type;
|
||||||
|
}
|
||||||
|
;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getSwitchTemplate = function (property) {
|
||||||
|
var pattern = /(<[a-zA-Z0-9-]*?)( |>)/
|
||||||
|
|
||||||
|
var mergedTemplates = _.reduce(this.visualizations, function (templates, visualization) {
|
||||||
|
if (visualization[property]) {
|
||||||
|
var ngSwitch = '$1 ng-switch-when="' + visualization.type + '" $2';
|
||||||
|
var template = visualization[property].replace(pattern, ngSwitch);
|
||||||
|
|
||||||
|
return templates + "\n" + template;
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates;
|
||||||
|
}, "");
|
||||||
|
|
||||||
|
mergedTemplates = '<div ng-switch on="visualization.type">' + mergedTemplates + "</div>";
|
||||||
|
|
||||||
|
return mergedTemplates;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$get = ['$resource', function ($resource) {
|
||||||
|
var Visualization = $resource('/api/visualizations/:id', {id: '@id'});
|
||||||
|
Visualization.visualizations = this.visualizations;
|
||||||
|
Visualization.visualizationTypes = this.visualizationTypes;
|
||||||
|
Visualization.renderVisualizationsTemplate = this.getSwitchTemplate('renderTemplate');
|
||||||
|
Visualization.editorTemplate = this.getSwitchTemplate('editorTemplate');
|
||||||
|
Visualization.defaultVisualization = this.defaultVisualization;
|
||||||
|
|
||||||
|
return Visualization;
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
|
||||||
|
var VisualizationName = function(Visualization) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
visualization: '='
|
||||||
|
},
|
||||||
|
template: '<small>{{name}}</small>',
|
||||||
|
replace: false,
|
||||||
|
link: function (scope) {
|
||||||
|
if (Visualization.visualizations[scope.visualization.type].name != scope.visualization.name) {
|
||||||
|
scope.name = scope.visualization.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var VisualizationRenderer = function ($location, Visualization) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
visualization: '=',
|
||||||
|
queryResult: '='
|
||||||
|
},
|
||||||
|
// TODO: using switch here (and in the options editor) might introduce errors and bad
|
||||||
|
// performance wise. It's better to eventually show the correct template based on the
|
||||||
|
// visualization type and not make the browser render all of them.
|
||||||
|
template: '<filters></filters>\n' + Visualization.renderVisualizationsTemplate,
|
||||||
|
replace: false,
|
||||||
|
link: function (scope) {
|
||||||
|
scope.select2Options = {
|
||||||
|
width: '50%'
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.$watch('queryResult && queryResult.getFilters()', function (filters) {
|
||||||
|
if (filters) {
|
||||||
|
scope.filters = filters;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var VisualizationOptionsEditor = function (Visualization) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
template: Visualization.editorTemplate,
|
||||||
|
replace: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var Filters = function () {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
templateUrl: '/views/visualizations/filters.html'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var EditVisualizationForm = function (Events, Visualization, growl) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
templateUrl: '/views/visualizations/edit_visualization.html',
|
||||||
|
replace: true,
|
||||||
|
scope: {
|
||||||
|
query: '=',
|
||||||
|
queryResult: '=',
|
||||||
|
visualization: '=?',
|
||||||
|
openEditor: '@',
|
||||||
|
onNewSuccess: '=?'
|
||||||
|
},
|
||||||
|
link: function (scope, element, attrs) {
|
||||||
|
scope.editRawOptions = currentUser.hasPermission('edit_raw_chart');
|
||||||
|
scope.visTypes = Visualization.visualizationTypes;
|
||||||
|
|
||||||
|
scope.newVisualization = function () {
|
||||||
|
return {
|
||||||
|
'type': Visualization.defaultVisualization.type,
|
||||||
|
'name': Visualization.defaultVisualization.name,
|
||||||
|
'description': '',
|
||||||
|
'options': Visualization.defaultVisualization.defaultOptions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scope.visualization) {
|
||||||
|
var unwatch = scope.$watch('query.id', function (queryId) {
|
||||||
|
if (queryId) {
|
||||||
|
unwatch();
|
||||||
|
|
||||||
|
scope.visualization = scope.newVisualization();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.$watch('visualization.type', function (type, oldType) {
|
||||||
|
// if not edited by user, set name to match type
|
||||||
|
if (type && oldType != type && scope.visualization && !scope.visForm.name.$dirty) {
|
||||||
|
scope.visualization.name = _.string.titleize(scope.visualization.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type && oldType != type && scope.visualization) {
|
||||||
|
scope.visualization.options = Visualization.visualizations[scope.visualization.type].defaultOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.submit = function () {
|
||||||
|
if (scope.visualization.id) {
|
||||||
|
Events.record(currentUser, "update", "visualization", scope.visualization.id, {'type': scope.visualization.type});
|
||||||
|
} else {
|
||||||
|
Events.record(currentUser, "create", "visualization", null, {'type': scope.visualization.type});
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.visualization.query_id = scope.query.id;
|
||||||
|
|
||||||
|
Visualization.save(scope.visualization, function success(result) {
|
||||||
|
growl.addSuccessMessage("Visualization saved");
|
||||||
|
|
||||||
|
scope.visualization = scope.newVisualization(scope.query);
|
||||||
|
|
||||||
|
var visIds = _.pluck(scope.query.visualizations, 'id');
|
||||||
|
var index = visIds.indexOf(result.id);
|
||||||
|
if (index > -1) {
|
||||||
|
scope.query.visualizations[index] = result;
|
||||||
|
} else {
|
||||||
|
// new visualization
|
||||||
|
scope.query.visualizations.push(result);
|
||||||
|
scope.onNewSuccess && scope.onNewSuccess(result);
|
||||||
|
}
|
||||||
|
}, function error() {
|
||||||
|
growl.addErrorMessage("Visualization could not be saved");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
angular.module('redash.visualization', [])
|
||||||
|
.provider('Visualization', VisualizationProvider)
|
||||||
|
.directive('visualizationRenderer', ['$location', 'Visualization', VisualizationRenderer])
|
||||||
|
.directive('visualizationOptionsEditor', ['Visualization', VisualizationOptionsEditor])
|
||||||
|
.directive('visualizationName', ['Visualization', VisualizationName])
|
||||||
|
.directive('filters', Filters)
|
||||||
|
.directive('editVisulatizationForm', ['Events', 'Visualization', 'growl', EditVisualizationForm])
|
||||||
|
})();
|
||||||
259
rd_ui/app/scripts/visualizations/chart.js
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
(function () {
|
||||||
|
var chartVisualization = angular.module('redash.visualization');
|
||||||
|
|
||||||
|
chartVisualization.config(['VisualizationProvider', function (VisualizationProvider) {
|
||||||
|
var renderTemplate = '<chart-renderer options="visualization.options" query-result="queryResult"></chart-renderer>';
|
||||||
|
var editTemplate = '<chart-editor></chart-editor>';
|
||||||
|
var defaultOptions = {
|
||||||
|
'series': {
|
||||||
|
// 'type': 'column',
|
||||||
|
'stacking': null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
VisualizationProvider.registerVisualization({
|
||||||
|
type: 'CHART',
|
||||||
|
name: 'Chart',
|
||||||
|
renderTemplate: renderTemplate,
|
||||||
|
editorTemplate: editTemplate,
|
||||||
|
defaultOptions: defaultOptions
|
||||||
|
});
|
||||||
|
}]);
|
||||||
|
|
||||||
|
chartVisualization.directive('chartRenderer', function () {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
queryResult: '=',
|
||||||
|
options: '=?'
|
||||||
|
},
|
||||||
|
template: "<chart options='chartOptions' series='chartSeries' class='graph'></chart>",
|
||||||
|
replace: false,
|
||||||
|
controller: ['$scope', function ($scope) {
|
||||||
|
$scope.chartSeries = [];
|
||||||
|
$scope.chartOptions = {};
|
||||||
|
|
||||||
|
var reloadData = function(data) {
|
||||||
|
if (!data || ($scope.queryResult && $scope.queryResult.getData()) == null) {
|
||||||
|
$scope.chartSeries.splice(0, $scope.chartSeries.length);
|
||||||
|
} else {
|
||||||
|
$scope.chartSeries.splice(0, $scope.chartSeries.length);
|
||||||
|
|
||||||
|
_.each($scope.queryResult.getChartData($scope.options.columnMapping), function (s) {
|
||||||
|
var additional = {'stacking': 'normal'};
|
||||||
|
if ('globalSeriesType' in $scope.options) {
|
||||||
|
additional['type'] = $scope.options.globalSeriesType;
|
||||||
|
}
|
||||||
|
if ($scope.options.seriesOptions && $scope.options.seriesOptions[s.name]) {
|
||||||
|
additional = $scope.options.seriesOptions[s.name];
|
||||||
|
if (!additional.name || additional.name == "") {
|
||||||
|
additional.name = s.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$scope.chartSeries.push(_.extend(s, additional));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('options', function (chartOptions) {
|
||||||
|
if (chartOptions) {
|
||||||
|
$scope.chartOptions = chartOptions;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch('options.seriesOptions', function () {
|
||||||
|
reloadData(true);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
|
||||||
|
$scope.$watchCollection('options.columnMapping', function (chartOptions) {
|
||||||
|
reloadData(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch('queryResult && queryResult.getData()', function (data) {
|
||||||
|
reloadData(data);
|
||||||
|
});
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
chartVisualization.directive('chartEditor', function (ColorPalette) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
templateUrl: '/views/visualizations/chart_editor.html',
|
||||||
|
link: function (scope, element, attrs) {
|
||||||
|
scope.palette = ColorPalette;
|
||||||
|
|
||||||
|
scope.seriesTypes = {
|
||||||
|
'Line': 'line',
|
||||||
|
'Column': 'column',
|
||||||
|
'Area': 'area',
|
||||||
|
'Scatter': 'scatter',
|
||||||
|
'Pie': 'pie'
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.globalSeriesType = scope.visualization.options.globalSeriesType || 'column';
|
||||||
|
|
||||||
|
scope.stackingOptions = {
|
||||||
|
"None": "none",
|
||||||
|
"Normal": "normal",
|
||||||
|
"Percent": "percent"
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.xAxisOptions = {
|
||||||
|
"Date/Time": "datetime",
|
||||||
|
"Linear": "linear",
|
||||||
|
"Category": "category"
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.xAxisType = "datetime";
|
||||||
|
scope.stacking = "none";
|
||||||
|
|
||||||
|
|
||||||
|
scope.columnTypes = {
|
||||||
|
"X": "x",
|
||||||
|
"Y": "y",
|
||||||
|
"Series": "series",
|
||||||
|
"Unused": "unused"
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.series = [];
|
||||||
|
|
||||||
|
scope.columnTypeSelection = {};
|
||||||
|
|
||||||
|
var chartOptionsUnwatch = null,
|
||||||
|
columnsWatch = null;
|
||||||
|
|
||||||
|
scope.$watch('globalSeriesType', function(type, old) {
|
||||||
|
scope.visualization.options.globalSeriesType = type;
|
||||||
|
|
||||||
|
if (type && old && type !== old && scope.visualization.options.seriesOptions) {
|
||||||
|
_.each(scope.visualization.options.seriesOptions, function(sOptions) {
|
||||||
|
sOptions.type = type;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.$watch('visualization.type', function (visualizationType) {
|
||||||
|
if (visualizationType == 'CHART') {
|
||||||
|
if (scope.visualization.options.series.stacking === null) {
|
||||||
|
scope.stacking = "none";
|
||||||
|
} else if (scope.visualization.options.series.stacking === undefined) {
|
||||||
|
scope.stacking = "normal";
|
||||||
|
} else {
|
||||||
|
scope.stacking = scope.visualization.options.series.stacking;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope.visualization.options.sortX === undefined) {
|
||||||
|
scope.visualization.options.sortX = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var refreshSeries = function() {
|
||||||
|
scope.series = _.map(scope.queryResult.getChartData(scope.visualization.options.columnMapping), function (s) { return s.name; });
|
||||||
|
|
||||||
|
// TODO: remove uneeded ones?
|
||||||
|
if (scope.visualization.options.seriesOptions == undefined) {
|
||||||
|
scope.visualization.options.seriesOptions = {
|
||||||
|
type: scope.globalSeriesType
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
_.each(scope.series, function(s, i) {
|
||||||
|
if (scope.visualization.options.seriesOptions[s] == undefined) {
|
||||||
|
scope.visualization.options.seriesOptions[s] = {'type': scope.visualization.options.globalSeriesType, 'yAxis': 0};
|
||||||
|
}
|
||||||
|
scope.visualization.options.seriesOptions[s].zIndex = scope.visualization.options.seriesOptions[s].zIndex === undefined ? i : scope.visualization.options.seriesOptions[s].zIndex;
|
||||||
|
scope.visualization.options.seriesOptions[s].index = scope.visualization.options.seriesOptions[s].index === undefined ? i : scope.visualization.options.seriesOptions[s].index;
|
||||||
|
});
|
||||||
|
scope.zIndexes = _.range(scope.series.length);
|
||||||
|
scope.yAxes = [[0, 'left'], [1, 'right']];
|
||||||
|
};
|
||||||
|
|
||||||
|
var initColumnMapping = function() {
|
||||||
|
scope.columns = scope.queryResult.getColumns();
|
||||||
|
|
||||||
|
if (scope.visualization.options.columnMapping == undefined) {
|
||||||
|
scope.visualization.options.columnMapping = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.columnTypeSelection = scope.visualization.options.columnMapping;
|
||||||
|
|
||||||
|
_.each(scope.columns, function(column) {
|
||||||
|
var definition = column.name.split("::"),
|
||||||
|
definedColumns = _.keys(scope.visualization.options.columnMapping);
|
||||||
|
|
||||||
|
if (_.indexOf(definedColumns, column.name) != -1) {
|
||||||
|
// Skip already defined columns.
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (definition.length == 1) {
|
||||||
|
scope.columnTypeSelection[column.name] = scope.visualization.options.columnMapping[column.name] = 'unused';
|
||||||
|
} else if (definition == 'multi-filter') {
|
||||||
|
scope.columnTypeSelection[column.name] = scope.visualization.options.columnMapping[column.name] = 'series';
|
||||||
|
} else if (_.indexOf(_.values(scope.columnTypes), definition[1]) != -1) {
|
||||||
|
scope.columnTypeSelection[column.name] = scope.visualization.options.columnMapping[column.name] = definition[1];
|
||||||
|
} else {
|
||||||
|
scope.columnTypeSelection[column.name] = scope.visualization.options.columnMapping[column.name] = 'unused';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
columnsWatch = scope.$watch('queryResult.getId()', function(id) {
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initColumnMapping();
|
||||||
|
refreshSeries();
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.$watchCollection('columnTypeSelection', function(selections) {
|
||||||
|
_.each(scope.columnTypeSelection, function(type, name) {
|
||||||
|
scope.visualization.options.columnMapping[name] = type;
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshSeries();
|
||||||
|
});
|
||||||
|
|
||||||
|
chartOptionsUnwatch = scope.$watch("stacking", function (stacking) {
|
||||||
|
if (stacking == "none") {
|
||||||
|
scope.visualization.options.series.stacking = null;
|
||||||
|
} else {
|
||||||
|
scope.visualization.options.series.stacking = stacking;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.visualization.options.xAxis = scope.visualization.options.xAxis || {};
|
||||||
|
scope.visualization.options.xAxis.labels = scope.visualization.options.xAxis.labels || {};
|
||||||
|
if (scope.visualization.options.xAxis.labels.enabled === undefined) {
|
||||||
|
scope.visualization.options.xAxis.labels.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.xAxisType = (scope.visualization.options.xAxis && scope.visualization.options.xAxis.type) || scope.xAxisType;
|
||||||
|
|
||||||
|
xAxisUnwatch = scope.$watch("xAxisType", function (xAxisType) {
|
||||||
|
scope.visualization.options.xAxis = scope.visualization.options.xAxis || {};
|
||||||
|
scope.visualization.options.xAxis.type = xAxisType;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (chartOptionsUnwatch) {
|
||||||
|
chartOptionsUnwatch();
|
||||||
|
chartOptionsUnwatch = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (columnsWatch) {
|
||||||
|
columnWatch();
|
||||||
|
columnWatch = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xAxisUnwatch) {
|
||||||
|
xAxisUnwatch();
|
||||||
|
xAxisUnwatch = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}());
|
||||||
64
rd_ui/app/scripts/visualizations/cohort.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
(function () {
|
||||||
|
var cohortVisualization = angular.module('redash.visualization');
|
||||||
|
|
||||||
|
cohortVisualization.config(['VisualizationProvider', function(VisualizationProvider) {
|
||||||
|
VisualizationProvider.registerVisualization({
|
||||||
|
type: 'COHORT',
|
||||||
|
name: 'Cohort',
|
||||||
|
renderTemplate: '<cohort-renderer options="visualization.options" query-result="queryResult"></cohort-renderer>'
|
||||||
|
});
|
||||||
|
}]);
|
||||||
|
|
||||||
|
cohortVisualization.directive('cohortRenderer', function() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
queryResult: '='
|
||||||
|
},
|
||||||
|
template: "",
|
||||||
|
replace: false,
|
||||||
|
link: function($scope, element, attrs) {
|
||||||
|
$scope.$watch('queryResult && queryResult.getData()', function (data) {
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scope.queryResult.getData() == null) {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
var sortedData = _.sortBy($scope.queryResult.getData(), "date");
|
||||||
|
var grouped = _.groupBy(sortedData, "date");
|
||||||
|
var maxColumns = _.reduce(grouped, function(memo, data){
|
||||||
|
return (data.length > memo)? data.length : memo;
|
||||||
|
}, 0);
|
||||||
|
var data = _.map(grouped, function(values, date) {
|
||||||
|
var row = [values[0].total];
|
||||||
|
_.each(values, function(value) { row.push(value.value); });
|
||||||
|
_.each(_.range(values.length, maxColumns), function() { row.push(null); });
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
|
||||||
|
var initialDate = moment(sortedData[0].date).toDate(),
|
||||||
|
container = angular.element(element)[0];
|
||||||
|
|
||||||
|
Cornelius.draw({
|
||||||
|
initialDate: initialDate,
|
||||||
|
container: container,
|
||||||
|
cohort: data,
|
||||||
|
title: null,
|
||||||
|
timeInterval: 'daily',
|
||||||
|
labels: {
|
||||||
|
time: 'Activation Day',
|
||||||
|
people: 'Users'
|
||||||
|
},
|
||||||
|
formatHeaderLabel: function (i) {
|
||||||
|
return "Day " + (i - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}());
|
||||||
61
rd_ui/app/scripts/visualizations/counter.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
var module = angular.module('redash.visualization');
|
||||||
|
|
||||||
|
module.config(['VisualizationProvider', function(VisualizationProvider) {
|
||||||
|
var renderTemplate =
|
||||||
|
'<counter-renderer ' +
|
||||||
|
'options="visualization.options" query-result="queryResult">' +
|
||||||
|
'</counter-renderer>';
|
||||||
|
|
||||||
|
var editTemplate = '<counter-editor></counter-editor>';
|
||||||
|
var defaultOptions = {};
|
||||||
|
|
||||||
|
VisualizationProvider.registerVisualization({
|
||||||
|
type: 'COUNTER',
|
||||||
|
name: 'Counter',
|
||||||
|
renderTemplate: renderTemplate,
|
||||||
|
editorTemplate: editTemplate,
|
||||||
|
defaultOptions: defaultOptions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
module.directive('counterRenderer', function() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
templateUrl: '/views/visualizations/counter.html',
|
||||||
|
link: function($scope, elm, attrs) {
|
||||||
|
$scope.visualization.options.rowNumber =
|
||||||
|
$scope.visualization.options.rowNumber || 0;
|
||||||
|
|
||||||
|
$scope.$watch('[queryResult && queryResult.getData(), visualization.options]',
|
||||||
|
function() {
|
||||||
|
var queryData = $scope.queryResult.getData();
|
||||||
|
if (queryData) {
|
||||||
|
var rowNumber = $scope.visualization.options.rowNumber || 0;
|
||||||
|
var counterColName = $scope.visualization.options.counterColName || 'counter';
|
||||||
|
var targetColName = $scope.visualization.options.targetColName || 'target';
|
||||||
|
|
||||||
|
$scope.counterValue = queryData[rowNumber][counterColName];
|
||||||
|
$scope.targetValue = queryData[rowNumber][targetColName];
|
||||||
|
|
||||||
|
if ($scope.targetValue) {
|
||||||
|
$scope.delta = $scope.counterValue - $scope.targetValue;
|
||||||
|
$scope.trendPositive = $scope.delta >= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.directive('counterEditor', function() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
templateUrl: '/views/visualizations/counter_editor.html'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
238
rd_ui/app/scripts/visualizations/map.js
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
var module = angular.module('redash.visualization');
|
||||||
|
|
||||||
|
module.config(['VisualizationProvider', function(VisualizationProvider) {
|
||||||
|
var renderTemplate =
|
||||||
|
'<map-renderer ' +
|
||||||
|
'options="visualization.options" query-result="queryResult">' +
|
||||||
|
'</map-renderer>';
|
||||||
|
|
||||||
|
var editTemplate = '<map-editor></map-editor>';
|
||||||
|
var defaultOptions = {
|
||||||
|
'height': 500,
|
||||||
|
'draw': 'Marker',
|
||||||
|
'classify':'none'
|
||||||
|
};
|
||||||
|
|
||||||
|
VisualizationProvider.registerVisualization({
|
||||||
|
type: 'MAP',
|
||||||
|
name: 'Map',
|
||||||
|
renderTemplate: renderTemplate,
|
||||||
|
editorTemplate: editTemplate,
|
||||||
|
defaultOptions: defaultOptions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
module.directive('mapRenderer', function() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
templateUrl: '/views/visualizations/map.html',
|
||||||
|
link: function($scope, elm, attrs) {
|
||||||
|
|
||||||
|
var setBounds = function(){
|
||||||
|
var b = $scope.visualization.options.bounds;
|
||||||
|
|
||||||
|
if(b){
|
||||||
|
$scope.map.fitBounds([[b._southWest.lat, b._southWest.lng],[b._northEast.lat, b._northEast.lng]]);
|
||||||
|
} else if ($scope.features.length > 0){
|
||||||
|
var group= new L.featureGroup($scope.features);
|
||||||
|
$scope.map.fitBounds(group.getBounds());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('[queryResult && queryResult.getData(), visualization.options.draw,visualization.options.latColName,'+
|
||||||
|
'visualization.options.lonColName,visualization.options.classify,visualization.options.classify]',
|
||||||
|
function() {
|
||||||
|
var marker = function(lat,lon){
|
||||||
|
if (lat == null || lon == null) return;
|
||||||
|
|
||||||
|
return L.marker([lat, lon]);
|
||||||
|
};
|
||||||
|
|
||||||
|
var heatpoint = function(lat,lon,obj){
|
||||||
|
if (lat == null || lon == null) return;
|
||||||
|
|
||||||
|
var color = 'red';
|
||||||
|
|
||||||
|
if (obj &&
|
||||||
|
obj[$scope.visualization.options.classify] &&
|
||||||
|
$scope.visualization.options.classification){
|
||||||
|
var v = $.grep($scope.visualization.options.classification,function(e){
|
||||||
|
return e.value == obj[$scope.visualization.options.classify];
|
||||||
|
});
|
||||||
|
if (v.length >0) color = v[0].color;
|
||||||
|
}
|
||||||
|
|
||||||
|
var style = {
|
||||||
|
fillColor:color,
|
||||||
|
fillOpacity:0.5,
|
||||||
|
stroke:false
|
||||||
|
};
|
||||||
|
|
||||||
|
return L.circleMarker([lat,lon],style)
|
||||||
|
};
|
||||||
|
|
||||||
|
var color = function(val){
|
||||||
|
// taken from http://jsfiddle.net/xgJ2e/2/
|
||||||
|
|
||||||
|
var h= Math.floor((100 - val) * 120 / 100);
|
||||||
|
var s = Math.abs(val - 50)/50;
|
||||||
|
var v = 1;
|
||||||
|
|
||||||
|
var rgb, i, data = [];
|
||||||
|
if (s === 0) {
|
||||||
|
rgb = [v,v,v];
|
||||||
|
} else {
|
||||||
|
h = h / 60;
|
||||||
|
i = Math.floor(h);
|
||||||
|
data = [v*(1-s), v*(1-s*(h-i)), v*(1-s*(1-(h-i)))];
|
||||||
|
switch(i) {
|
||||||
|
case 0:
|
||||||
|
rgb = [v, data[2], data[0]];
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
rgb = [data[1], v, data[0]];
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
rgb = [data[0], v, data[2]];
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
rgb = [data[0], data[1], v];
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
rgb = [data[2], data[0], v];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
rgb = [v, data[0], data[1]];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '#' + rgb.map(function(x){
|
||||||
|
return ("0" + Math.round(x*255).toString(16)).slice(-2);
|
||||||
|
}).join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Following line is used to avoid "Couldn't autodetect L.Icon.Default.imagePath" error
|
||||||
|
// https://github.com/Leaflet/Leaflet/issues/766#issuecomment-7741039
|
||||||
|
L.Icon.Default.imagePath = L.Icon.Default.imagePath || "//api.tiles.mapbox.com/mapbox.js/v2.2.1/images";
|
||||||
|
|
||||||
|
function getBounds(e) {
|
||||||
|
$scope.visualization.options.bounds = $scope.map.getBounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
var queryData = $scope.queryResult.getData();
|
||||||
|
var classify = $scope.visualization.options.classify;
|
||||||
|
|
||||||
|
if (queryData) {
|
||||||
|
$scope.visualization.options.classification = [];
|
||||||
|
|
||||||
|
for (var row in queryData) {
|
||||||
|
if (queryData[row][classify] &&
|
||||||
|
$.grep($scope.visualization.options.classification, function (e) {
|
||||||
|
return e.value == queryData[row][classify]
|
||||||
|
}).length == 0) {
|
||||||
|
$scope.visualization.options.classification.push({value: queryData[row][classify], color: null});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$.each($scope.visualization.options.classification, function (i, c) {
|
||||||
|
c.color = color(parseInt((i / $scope.visualization.options.classification.length) * 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!$scope.map) {
|
||||||
|
$scope.map = L.map(elm[0].children[0].children[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
}).addTo($scope.map);
|
||||||
|
|
||||||
|
$scope.features = $scope.features || [];
|
||||||
|
|
||||||
|
var tmp_features = [];
|
||||||
|
|
||||||
|
var lat_col = $scope.visualization.options.latColName || 'lat';
|
||||||
|
var lon_col = $scope.visualization.options.lonColName || 'lon';
|
||||||
|
|
||||||
|
for (var row in queryData) {
|
||||||
|
var feature;
|
||||||
|
|
||||||
|
if ($scope.visualization.options.draw == 'Marker') {
|
||||||
|
feature = marker(queryData[row][lat_col], queryData[row][lon_col])
|
||||||
|
} else if ($scope.visualization.options.draw == 'Color') {
|
||||||
|
feature = heatpoint(queryData[row][lat_col], queryData[row][lon_col], queryData[row])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!feature) continue;
|
||||||
|
|
||||||
|
var obj_description = '<ul style="list-style-type: none;padding-left: 0">';
|
||||||
|
for (var k in queryData[row]){
|
||||||
|
obj_description += "<li>" + k + ": " + queryData[row][k] + "</li>";
|
||||||
|
}
|
||||||
|
obj_description += '</ul>';
|
||||||
|
feature.bindPopup(obj_description);
|
||||||
|
tmp_features.push(feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.each($scope.features, function (i, f) {
|
||||||
|
$scope.map.removeLayer(f);
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.features = tmp_features;
|
||||||
|
|
||||||
|
$.each($scope.features, function (i, f) {
|
||||||
|
f.addTo($scope.map)
|
||||||
|
});
|
||||||
|
|
||||||
|
setBounds();
|
||||||
|
|
||||||
|
$scope.map.on('focus',function(){
|
||||||
|
$scope.map.on('moveend', getBounds);
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.map.on('blur',function(){
|
||||||
|
$scope.map.off('moveend', getBounds);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// We redraw the map if it was loaded in a hidden tab
|
||||||
|
if ($('a[href="#'+$scope.visualization.id+'"]').length > 0) {
|
||||||
|
|
||||||
|
$('a[href="#'+$scope.visualization.id+'"]').on('click', function () {
|
||||||
|
setTimeout(function() {
|
||||||
|
$scope.map.invalidateSize(false);
|
||||||
|
|
||||||
|
setBounds();
|
||||||
|
},500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
$scope.$watch('visualization.options.height', function() {
|
||||||
|
|
||||||
|
if (!$scope.map) return;
|
||||||
|
$scope.map.invalidateSize(false);
|
||||||
|
setBounds();
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.directive('mapEditor', function() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
templateUrl: '/views/visualizations/map_editor.html',
|
||||||
|
link: function($scope, elm, attrs) {
|
||||||
|
$scope.draw_options = ['Marker','Color'];
|
||||||
|
$scope.classify_columns = $scope.queryResult.columnNames.concat('none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
29
rd_ui/app/scripts/visualizations/pivot.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
var renderers = angular.module('redash.renderers', []);
|
||||||
|
|
||||||
|
renderers.directive('pivotTableRenderer', function () {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
queryResult: '='
|
||||||
|
},
|
||||||
|
template: "",
|
||||||
|
replace: false,
|
||||||
|
link: function($scope, element, attrs) {
|
||||||
|
$scope.$watch('queryResult && queryResult.getData()', function (data) {
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scope.queryResult.getData() == null) {
|
||||||
|
} else {
|
||||||
|
// We need to give the pivot table its own copy of the data, because its change
|
||||||
|
// it which interferes with other visualizations.
|
||||||
|
var data = $.extend(true, [], $scope.queryResult.getData());
|
||||||
|
$(element).pivotUI(data, {
|
||||||
|
renderers: $.pivotUtilities.renderers
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
109
rd_ui/app/scripts/visualizations/table.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
(function () {
|
||||||
|
var tableVisualization = angular.module('redash.visualization');
|
||||||
|
|
||||||
|
tableVisualization.config(['VisualizationProvider', function (VisualizationProvider) {
|
||||||
|
VisualizationProvider.registerVisualization({
|
||||||
|
type: 'TABLE',
|
||||||
|
name: 'Table',
|
||||||
|
renderTemplate: '<grid-renderer options="visualization.options" query-result="queryResult"></grid-renderer>',
|
||||||
|
skipTypes: true
|
||||||
|
});
|
||||||
|
}]);
|
||||||
|
|
||||||
|
tableVisualization.directive('gridRenderer', function () {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
queryResult: '=',
|
||||||
|
itemsPerPage: '='
|
||||||
|
},
|
||||||
|
templateUrl: "/views/grid_renderer.html",
|
||||||
|
replace: false,
|
||||||
|
controller: ['$scope', '$filter', function ($scope, $filter) {
|
||||||
|
$scope.gridColumns = [];
|
||||||
|
$scope.gridData = [];
|
||||||
|
$scope.gridConfig = {
|
||||||
|
isPaginationEnabled: true,
|
||||||
|
itemsByPage: $scope.itemsPerPage || 15,
|
||||||
|
maxSize: 8
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('queryResult && queryResult.getData()', function (data) {
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scope.queryResult.getData() == null) {
|
||||||
|
$scope.gridColumns = [];
|
||||||
|
$scope.gridData = [];
|
||||||
|
$scope.filters = [];
|
||||||
|
} else {
|
||||||
|
$scope.filters = $scope.queryResult.getFilters();
|
||||||
|
|
||||||
|
var prepareGridData = function (data) {
|
||||||
|
var gridData = _.map(data, function (row) {
|
||||||
|
var newRow = {};
|
||||||
|
_.each(row, function (val, key) {
|
||||||
|
newRow[$scope.queryResult.getColumnCleanName(key)] = val;
|
||||||
|
})
|
||||||
|
return newRow;
|
||||||
|
});
|
||||||
|
|
||||||
|
return gridData;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.gridData = prepareGridData($scope.queryResult.getData());
|
||||||
|
|
||||||
|
var columns = $scope.queryResult.getColumns();
|
||||||
|
$scope.gridColumns = _.map($scope.queryResult.getColumnCleanNames(), function (col, i) {
|
||||||
|
var columnDefinition = {
|
||||||
|
'label': $scope.queryResult.getColumnFriendlyNames()[i],
|
||||||
|
'map': col
|
||||||
|
};
|
||||||
|
|
||||||
|
var columnType = columns[i].type;
|
||||||
|
|
||||||
|
if (columnType === 'integer') {
|
||||||
|
columnDefinition.formatFunction = 'number';
|
||||||
|
columnDefinition.formatParameter = 0;
|
||||||
|
} else if (columnType === 'float') {
|
||||||
|
columnDefinition.formatFunction = 'number';
|
||||||
|
columnDefinition.formatParameter = 2;
|
||||||
|
} else if (columnType === 'boolean') {
|
||||||
|
columnDefinition.formatFunction = function (value) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
return "" + value;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
} else if (columnType === 'date') {
|
||||||
|
columnDefinition.formatFunction = function (value) {
|
||||||
|
if (value && moment.isMoment(value)) {
|
||||||
|
return value.toDate().toLocaleDateString();
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
} else if (columnType === 'datetime') {
|
||||||
|
columnDefinition.formatFunction = function (value) {
|
||||||
|
if (value && moment.isMoment(value)) {
|
||||||
|
return value.toDate().toLocaleString();
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
columnDefinition.formatFunction = function (value) {
|
||||||
|
if (angular.isString(value)) {
|
||||||
|
value = $filter('linkify')(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return columnDefinition;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}());
|
||||||
40
rd_ui/app/styles/login.css
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
.main {
|
||||||
|
max-width: 320px;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-top:20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-or {
|
||||||
|
position: relative;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #aaa;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-or {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: -2px;
|
||||||
|
margin-left: -25px;
|
||||||
|
background-color: #fff;
|
||||||
|
width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-or {
|
||||||
|
background-color: #cdcdcd;
|
||||||
|
height: 1px;
|
||||||
|
margin-top: 0px !important;
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.login-button {
|
||||||
|
width: 250px;
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
@@ -2,8 +2,24 @@ body {
|
|||||||
padding-top: 70px;
|
padding-top: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.page-title {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
a.navbar-brand {
|
a.navbar-brand {
|
||||||
font-style: italic;
|
padding: 5px 5px 0px 0px;
|
||||||
|
margin-left: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.navbar-brand img {
|
||||||
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph {
|
.graph {
|
||||||
@@ -14,21 +30,53 @@ a.navbar-brand {
|
|||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
.avatar img {
|
||||||
.edit-in-place span {
|
width: 40px;
|
||||||
cursor: pointer;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-in-place input {
|
#logout {
|
||||||
display: none;
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
left: -9px;
|
||||||
|
bottom: -11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.details-toggle::before {
|
||||||
|
content: '▸';
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
.details-toggle.open::before {
|
||||||
|
content: '▾';
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-in-place span {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
.edit-in-place span.editable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-in-place span.editable:hover {
|
||||||
|
background: #FCFCA2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-in-place input,
|
||||||
|
.edit-in-place textarea {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-in-place.active span {
|
.edit-in-place.active span {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-in-place.active input {
|
.edit-in-place.active input,
|
||||||
display: inline-block;
|
.edit-in-place.active textarea {
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-button {
|
.delete-button {
|
||||||
@@ -39,6 +87,19 @@ a.navbar-brand {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-heading > p:last-child {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-heading > a,
|
||||||
|
.panel-heading .query-link {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-heading .query-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
/* angular-growl */
|
/* angular-growl */
|
||||||
.growl {
|
.growl {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -46,6 +107,7 @@ a.navbar-brand {
|
|||||||
right: 10px;
|
right: 10px;
|
||||||
float: right;
|
float: right;
|
||||||
width: 250px;
|
width: 250px;
|
||||||
|
z-index: 10000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.growl-item.ng-enter,
|
.growl-item.ng-enter,
|
||||||
@@ -126,4 +188,154 @@ to add those CSS styles here. */
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: #428bca;
|
background-color: #428bca;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dropdown submenus */
|
||||||
|
.dropdown-submenu {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-submenu > .dropdown-menu {
|
||||||
|
top: 0;
|
||||||
|
left: 100%;
|
||||||
|
margin-top: -6px;
|
||||||
|
margin-left: -1px;
|
||||||
|
-webkit-border-radius: 0 6px 6px 6px;
|
||||||
|
-moz-border-radius: 0 6px 6px 6px;
|
||||||
|
border-radius: 0 6px 6px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-submenu:hover > .dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-submenu > a:after {
|
||||||
|
display: block;
|
||||||
|
content: " ";
|
||||||
|
float: right;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-color: transparent;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 5px 0 5px 5px;
|
||||||
|
border-left-color: #cccccc;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-right: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-submenu:hover > a:after {
|
||||||
|
/*border-left-color: #ffffff;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-submenu.pull-left {
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-submenu.pull-left > .dropdown-menu {
|
||||||
|
left: -100%;
|
||||||
|
margin-left: 10px;
|
||||||
|
-webkit-border-radius: 6px 0 6px 6px;
|
||||||
|
-moz-border-radius: 6px 0 6px 6px;
|
||||||
|
border-radius: 6px 0 6px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rd-tab .remove {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #A09797;
|
||||||
|
padding: 0 3px 1px 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.rd-tab .remove:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: #FF8080;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.nav-tabs > li.rd-tab-btn {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* light version of bootstrap's form-control */
|
||||||
|
.rd-form-control {
|
||||||
|
display: block;
|
||||||
|
padding: 6px 12px;
|
||||||
|
line-height: 1.428571429;
|
||||||
|
color: #555555;
|
||||||
|
vertical-align: middle;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||||
|
-webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
|
||||||
|
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
|
||||||
|
}
|
||||||
|
.rd-form-control {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
counter-renderer {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
counter-renderer counter {
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 15px 50px;
|
||||||
|
display: block;;
|
||||||
|
}
|
||||||
|
counter-renderer value,
|
||||||
|
counter-renderer counter-target {
|
||||||
|
font-size: 80px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
counter-renderer counter-target {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
counter-renderer counter.positive value {
|
||||||
|
color: #5cb85c;
|
||||||
|
}
|
||||||
|
counter-renderer counter.negative value {
|
||||||
|
color: #d9534f;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
counter-renderer counter-name {
|
||||||
|
font-size: 40px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rd-widget-textbox p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iframe-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-container {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-browser {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.table-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
bootstrap's hidden-xs class adds display:block when not hidden
|
||||||
|
use this class when you need to keep the original display value
|
||||||
|
*/
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.rd-hidden-xs {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
BIN
rd_ui/app/styles/select2-spinner.gif
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
rd_ui/app/styles/select2.png
Normal file
|
After Width: | Height: | Size: 613 B |
BIN
rd_ui/app/styles/select2x2.png
Normal file
|
After Width: | Height: | Size: 845 B |
@@ -10,6 +10,28 @@
|
|||||||
{{name | toHuman}}
|
{{name | toHuman}}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<ul class="list-group col-lg-4">
|
||||||
|
<li class="list-group-item active">Manager</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<span class="badge" am-time-ago="manager.last_refresh_at*1000.0"></span>
|
||||||
|
Last Refresh
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<span class="badge" am-time-ago="manager.started_at*1000.0"></span>
|
||||||
|
Started
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<span class="badge">{{manager.outdated_queries_count}}</span>
|
||||||
|
Outdated Queries Count
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="list-group col-lg-4">
|
||||||
|
<li class="list-group-item active">Queues</li>
|
||||||
|
<li class="list-group-item" ng-repeat="(name, value) in manager.queues">
|
||||||
|
<span class="badge">{{value.size}}</span>
|
||||||
|
{{name}} ({{value.data_sources}})
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-footer">Next refresh: <span am-time-ago="refresh_time"></span></div>
|
<div class="panel-footer">Next refresh: <span am-time-ago="refresh_time"></span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||