Compare commits
779 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9386cc2c3 | ||
|
|
38f348e642 | ||
|
|
656ec9c48f | ||
|
|
f5e8f9334b | ||
|
|
7be2105fd9 | ||
|
|
49ee388dcf | ||
|
|
6cb4b4061c | ||
|
|
85e50ece2e | ||
|
|
32ae0ea13e | ||
|
|
1b3bddd622 | ||
|
|
731bb6ba03 | ||
|
|
fed06ef97d | ||
|
|
a58330f4d8 | ||
|
|
5a46c2397b | ||
|
|
f79588c191 | ||
|
|
9f766557f1 | ||
|
|
846cf934f5 | ||
|
|
162eb4ca35 | ||
|
|
2f18444a43 | ||
|
|
8a4ff5bddc | ||
|
|
4ae7648bea | ||
|
|
dc75559758 | ||
|
|
acd944a649 | ||
|
|
3c1b30e3c1 | ||
|
|
5daefc8b8e | ||
|
|
715b9cbad0 | ||
|
|
7194e91d3b | ||
|
|
69d56b8ed7 | ||
|
|
982a5b1a39 | ||
|
|
6b8cb4ac7f | ||
|
|
397f253180 | ||
|
|
f93b6f7d85 | ||
|
|
edb2cf2cf2 | ||
|
|
fb12dd4688 | ||
|
|
0a561e5aeb | ||
|
|
c140c8cac9 | ||
|
|
06ab063671 | ||
|
|
1591a486cc | ||
|
|
b53efed1ef | ||
|
|
7a202db5ad | ||
|
|
af1ae7cab4 | ||
|
|
24519cbf78 | ||
|
|
01305462aa | ||
|
|
7a4077405e | ||
|
|
9594effb6c | ||
|
|
e750498696 | ||
|
|
69e18905f5 | ||
|
|
ac4524cf9b | ||
|
|
7baad7ff10 | ||
|
|
d7ef6daeb8 | ||
|
|
93e3908a63 | ||
|
|
bf5f6ce97c | ||
|
|
ed2ca5fced | ||
|
|
8dfe85f23e | ||
|
|
846992930c | ||
|
|
a1d652d578 | ||
|
|
d0e057722b | ||
|
|
d1be4e2a90 | ||
|
|
3cde783d1d | ||
|
|
e933369f56 | ||
|
|
e109f8d69c | ||
|
|
d198057eaf | ||
|
|
88c2f18b20 | ||
|
|
a30a604228 | ||
|
|
62b58d8bb0 | ||
|
|
a5951592f7 | ||
|
|
6392d6514e | ||
|
|
91d6641326 | ||
|
|
dd398f73c2 | ||
|
|
de10bd4ef6 | ||
|
|
56321da9c1 | ||
|
|
d0d1c5ea5f | ||
|
|
e16872c864 | ||
|
|
1e425244d2 | ||
|
|
0c6618d2f6 | ||
|
|
a9d7823186 | ||
|
|
6c0f5329aa | ||
|
|
a677a4feff | ||
|
|
4f674c8d19 | ||
|
|
e197163019 | ||
|
|
7ba0099fa9 | ||
|
|
f45797ec4b | ||
|
|
02e1ac12b2 | ||
|
|
bde138177d | ||
|
|
640c558446 | ||
|
|
c11ce99bb3 | ||
|
|
8ad83b8d58 | ||
|
|
873d3d7c4a | ||
|
|
d4adaaaf2b | ||
|
|
104493e725 | ||
|
|
b172e7afdc | ||
|
|
b1efbeb220 | ||
|
|
f2a6657b72 | ||
|
|
4eb4974909 | ||
|
|
3e2c898881 | ||
|
|
2fb176a244 | ||
|
|
53a6a7b305 | ||
|
|
2045380223 | ||
|
|
1c0fc3c924 | ||
|
|
0b966b7a28 | ||
|
|
882a477c0f | ||
|
|
ebcf2c3f68 | ||
|
|
54fafb3a76 | ||
|
|
01bd662046 | ||
|
|
9e26d3e85d | ||
|
|
17114778b7 | ||
|
|
98037ca0c6 | ||
|
|
4ec59e8211 | ||
|
|
1e6a5ff8ec | ||
|
|
c12d830162 | ||
|
|
a49a9b3b64 | ||
|
|
5da3da5962 | ||
|
|
8bb51d47f8 | ||
|
|
381b9ee7ee | ||
|
|
a301c96c9d | ||
|
|
f9c3cdab67 | ||
|
|
27ec0d532e | ||
|
|
3aa619b617 | ||
|
|
ef92272bee | ||
|
|
815ef05daf | ||
|
|
2ab19148c1 | ||
|
|
d12f4a4aee | ||
|
|
81b1939f92 | ||
|
|
834daeecd0 | ||
|
|
6aa0208316 | ||
|
|
aa87d8eb22 | ||
|
|
cd21fa7016 | ||
|
|
7ba4f6b93f | ||
|
|
e16a51ad06 | ||
|
|
6861dc137f | ||
|
|
e530e4d4bc | ||
|
|
d150bc1e52 | ||
|
|
cc8961360a | ||
|
|
c0539e483e | ||
|
|
f0b7099be3 | ||
|
|
ee183d4574 | ||
|
|
fa813024ca | ||
|
|
37176fa42d | ||
|
|
7ff8931def | ||
|
|
2a962bf8fd | ||
|
|
bbbcba8b98 | ||
|
|
ecdcd10612 | ||
|
|
10ea140358 | ||
|
|
74e0309241 | ||
|
|
c88bfbbf82 | ||
|
|
4edb89eeb9 | ||
|
|
cdb4b3cc7d | ||
|
|
ed8dd03fa1 | ||
|
|
e5bb7f7c2d | ||
|
|
a7b0f6dc9f | ||
|
|
c42986c07d | ||
|
|
a982dd1765 | ||
|
|
c3900398fc | ||
|
|
4b79a5e9da | ||
|
|
eed2feea97 | ||
|
|
60a2d9f624 | ||
|
|
3cd33b6ffc | ||
|
|
e3942b3438 | ||
|
|
0bf37b8c00 | ||
|
|
0e5dbf3889 | ||
|
|
785f96aabe | ||
|
|
5cec936128 | ||
|
|
02861142cb | ||
|
|
1cfc4d2f31 | ||
|
|
08e816a539 | ||
|
|
272cf64aac | ||
|
|
ed57a4099b | ||
|
|
79c5d48a3c | ||
|
|
6c70122e55 | ||
|
|
6c83699e6f | ||
|
|
ff254ce08d | ||
|
|
8a80ea26b8 | ||
|
|
b85fe62389 | ||
|
|
31a4d9204c | ||
|
|
69fdfd5cb3 | ||
|
|
16e7980982 | ||
|
|
e3e08cf8e7 | ||
|
|
cae6626b06 | ||
|
|
956d93e871 | ||
|
|
7c4d1b7b01 | ||
|
|
9866e0851b | ||
|
|
0a97a2435b | ||
|
|
5c80f03eae | ||
|
|
dd03a4b011 | ||
|
|
6cd1bc32fe | ||
|
|
dd0193a9a8 | ||
|
|
123faa6f8e | ||
|
|
f743288ce0 | ||
|
|
a1d764bd26 | ||
|
|
61f6bd2c80 | ||
|
|
fe620d8e44 | ||
|
|
22db28d3e7 | ||
|
|
1330488e13 | ||
|
|
22297be3cf | ||
|
|
29d7d6994a | ||
|
|
87e8162a2d | ||
|
|
38b71bf386 | ||
|
|
920689b80e | ||
|
|
e9c7523646 | ||
|
|
88bbc720ca | ||
|
|
1bf1469c80 | ||
|
|
c74eda20dc | ||
|
|
db0a5bd537 | ||
|
|
c6cb01aa3b | ||
|
|
7463f91878 | ||
|
|
482b31298f | ||
|
|
ce46ca2f39 | ||
|
|
de00d18a7e | ||
|
|
b0cf0c558d | ||
|
|
920e5c93e1 | ||
|
|
0aae78c6bd | ||
|
|
7fb048f423 | ||
|
|
e86207bb28 | ||
|
|
75d60ccb69 | ||
|
|
9245cd6aae | ||
|
|
d8183b60c3 | ||
|
|
a24272690d | ||
|
|
40b088d6a2 | ||
|
|
f1125d64de | ||
|
|
0cba818364 | ||
|
|
4285c751b3 | ||
|
|
1b0cddfa72 | ||
|
|
231a599f09 | ||
|
|
7ffc4d388a | ||
|
|
1f24171238 | ||
|
|
24917a6df5 | ||
|
|
67fde17209 | ||
|
|
6abad666db | ||
|
|
2d3f396571 | ||
|
|
51bcbdac75 | ||
|
|
e63889d5c4 | ||
|
|
fe6a7c58bf | ||
|
|
dac3cb15c4 | ||
|
|
ca654ccaf7 | ||
|
|
30512b7032 | ||
|
|
bc8fd62cff | ||
|
|
a9a51ee3c6 | ||
|
|
2d2da7c881 | ||
|
|
d22d8c4905 | ||
|
|
bcdc8eafa6 | ||
|
|
9cf6ace979 | ||
|
|
eef063cec2 | ||
|
|
8b9bdf9054 | ||
|
|
1c7b898b01 | ||
|
|
7ad18da08e | ||
|
|
5530915b49 | ||
|
|
77f380c94b | ||
|
|
c79ab84fdf | ||
|
|
d77448d84e | ||
|
|
8fc5a2785f | ||
|
|
27da2b026f | ||
|
|
2e9cc2a74e | ||
|
|
ffd370176d | ||
|
|
44f2a375f6 | ||
|
|
05cb97819c | ||
|
|
282c834d9f | ||
|
|
4a6ff9e2aa | ||
|
|
ce972d4f19 | ||
|
|
b250d10320 | ||
|
|
f63706d118 | ||
|
|
c41aa64719 | ||
|
|
285d246c65 | ||
|
|
2c85205259 | ||
|
|
2d866b9298 | ||
|
|
7a7629acf7 | ||
|
|
debf820037 | ||
|
|
f908ae8c40 | ||
|
|
022cbdda31 | ||
|
|
ae2523aa59 | ||
|
|
bf361d2b02 | ||
|
|
64f3303711 | ||
|
|
06f382c454 | ||
|
|
5f164d99ac | ||
|
|
2473ae3b47 | ||
|
|
3fb457ccd1 | ||
|
|
51333c9eda | ||
|
|
1aaf3961ff | ||
|
|
af4f3f62e9 | ||
|
|
cc31a12b8c | ||
|
|
bc9989f9be | ||
|
|
7f33bec71c | ||
|
|
3ea94c3484 | ||
|
|
68adaea128 | ||
|
|
4997068a0d | ||
|
|
4c59ec815e | ||
|
|
440ea666d9 | ||
|
|
6f1a6d5a56 | ||
|
|
69e80fd11c | ||
|
|
bef8cc2d70 | ||
|
|
eaa899e9cf | ||
|
|
743c95d0f9 | ||
|
|
a08cb52ad9 | ||
|
|
b9604bf3bc | ||
|
|
978a345ad8 | ||
|
|
d5ffe6acef | ||
|
|
622c1a1dad | ||
|
|
79fea549ef | ||
|
|
bce6e75cfa | ||
|
|
34f36fff5c | ||
|
|
f4e24038fe | ||
|
|
81747e1623 | ||
|
|
d6f1c379c0 | ||
|
|
6794260e3f | ||
|
|
1be840f19d | ||
|
|
f59bb6461a | ||
|
|
bd3bae3af0 | ||
|
|
0fbace7285 | ||
|
|
af8fec941c | ||
|
|
139791b0d8 | ||
|
|
7fe76d32d0 | ||
|
|
352ad3385a | ||
|
|
010baad532 | ||
|
|
af52b20c4a | ||
|
|
e82d171041 | ||
|
|
4f261389db | ||
|
|
6003fee33f | ||
|
|
b56c3eb035 | ||
|
|
a19a2c70ab | ||
|
|
40a491a80b | ||
|
|
06ec91c899 | ||
|
|
10f9022d7c | ||
|
|
e78c48620f | ||
|
|
563dd898c1 | ||
|
|
a97bcc3ca7 | ||
|
|
fa31fc046e | ||
|
|
a68a179c1e | ||
|
|
77b0d36b55 | ||
|
|
f2a6fc4d5a | ||
|
|
c9501d1119 | ||
|
|
323ff3d491 | ||
|
|
98b3126e32 | ||
|
|
88ea524f44 | ||
|
|
a0ab9113fc | ||
|
|
6dae8f44b9 | ||
|
|
bba3f3000f | ||
|
|
d40e21a7e0 | ||
|
|
94d2ae2a6a | ||
|
|
3099198e47 | ||
|
|
48e9b5f4be | ||
|
|
430e2e439b | ||
|
|
81a21c03b2 | ||
|
|
064a97e734 | ||
|
|
5e9ed95684 | ||
|
|
017d5617a5 | ||
|
|
26bb2e0193 | ||
|
|
ff91430fcc | ||
|
|
32a41a8422 | ||
|
|
8b93e20a0b | ||
|
|
92bec31ccb | ||
|
|
96a0d0aefa | ||
|
|
15f2b2cf9a | ||
|
|
bf9eaea334 | ||
|
|
7b45a2ec51 | ||
|
|
48eb2083f2 | ||
|
|
2c6ea276c1 | ||
|
|
5a3db0505f | ||
|
|
6ca73f6df0 | ||
|
|
762dab618a | ||
|
|
a65c61442e | ||
|
|
4883b2a296 | ||
|
|
b1abe72ab6 | ||
|
|
a640d55297 | ||
|
|
99009a11ed | ||
|
|
2150bbf191 | ||
|
|
6f8cb743b5 | ||
|
|
682d2a1675 | ||
|
|
6022784e42 | ||
|
|
4d4478d969 | ||
|
|
c0935c84ee | ||
|
|
17e040abe4 | ||
|
|
6152a5e3c2 | ||
|
|
4558486cbd | ||
|
|
3df592c702 | ||
|
|
05fabc73c2 | ||
|
|
e949761107 | ||
|
|
4311a20c5f | ||
|
|
1440d1a147 | ||
|
|
4b170ca9a3 | ||
|
|
d6e844c67c | ||
|
|
71a307270a | ||
|
|
0f88b470e8 | ||
|
|
4798aa4789 | ||
|
|
a9d96ccc8c | ||
|
|
e0c9ddbfba | ||
|
|
bbc5dae1d2 | ||
|
|
9e7c55728f | ||
|
|
a6fa01f89b | ||
|
|
6c71754e51 | ||
|
|
e729b3734d | ||
|
|
6337c77532 | ||
|
|
fb08b71884 | ||
|
|
d749549135 | ||
|
|
58a2ab4fbd | ||
|
|
cc96cfe0c7 | ||
|
|
656b3e53a8 | ||
|
|
4e5dcafa1b | ||
|
|
afc8380f23 | ||
|
|
0319051891 | ||
|
|
4805a3bc23 | ||
|
|
dc63f0ddd0 | ||
|
|
6ff188e4d9 | ||
|
|
44edaad19d | ||
|
|
5304221e46 | ||
|
|
c6b1fe5349 | ||
|
|
80574334cf | ||
|
|
dd4eaa0758 | ||
|
|
93550e9ea5 | ||
|
|
3157fc651d | ||
|
|
dd4f27e3fa | ||
|
|
455e80513b | ||
|
|
65af872ec6 | ||
|
|
917cd35005 | ||
|
|
b989183fce | ||
|
|
0845d5d451 | ||
|
|
f002ef105e | ||
|
|
3d202c2ef9 | ||
|
|
953eec7326 | ||
|
|
c3956b4d6f | ||
|
|
d2421bb216 | ||
|
|
afdc19ce9d | ||
|
|
4a9380cc95 | ||
|
|
9f60745e57 | ||
|
|
666d640216 | ||
|
|
cb479d737b | ||
|
|
4ee455fad2 | ||
|
|
2da04e72f5 | ||
|
|
141ea7ba91 | ||
|
|
5ae0239c26 | ||
|
|
2dc4434a49 | ||
|
|
39c068bd53 | ||
|
|
b26dfd8246 | ||
|
|
3185db9609 | ||
|
|
61a618e473 | ||
|
|
c4a4ecfc81 | ||
|
|
f9b0ce0f75 | ||
|
|
20607c00b1 | ||
|
|
12e2bf2f85 | ||
|
|
685a2fec6c | ||
|
|
f9cd4a4470 | ||
|
|
b761aad903 | ||
|
|
9ee4fcb36c | ||
|
|
1929490deb | ||
|
|
bb3b31829f | ||
|
|
e0a58dd1fe | ||
|
|
7d6e04ac77 | ||
|
|
6502cff8fe | ||
|
|
fdffb03eba | ||
|
|
d4d3ae7530 | ||
|
|
647feb7b33 | ||
|
|
625781c7f4 | ||
|
|
02ef6c4e07 | ||
|
|
43eba6cc31 | ||
|
|
3775991ac8 | ||
|
|
08d2492839 | ||
|
|
d0d0e8349f | ||
|
|
1a97f79d54 | ||
|
|
9fc6c4888f | ||
|
|
f2de18508a | ||
|
|
6342571afe | ||
|
|
00e5bb61fc | ||
|
|
b506e7c267 | ||
|
|
061d1262d4 | ||
|
|
8b3c89e267 | ||
|
|
c634ee81fc | ||
|
|
6969a4121c | ||
|
|
4f8b2ad245 | ||
|
|
03353cb652 | ||
|
|
6f80862517 | ||
|
|
edd8d63caf | ||
|
|
ffbdea78ee | ||
|
|
47a20e6a2f | ||
|
|
8a80623d0c | ||
|
|
adbe8142b6 | ||
|
|
3579a18da8 | ||
|
|
36882ea2a4 | ||
|
|
3fec2cdfd6 | ||
|
|
28de5cbd97 | ||
|
|
39cdf85788 | ||
|
|
7c3046e011 | ||
|
|
1bc8526640 | ||
|
|
0795af8d42 | ||
|
|
bd7c045b1c | ||
|
|
c6812f569f | ||
|
|
17ffb167e2 | ||
|
|
019d077f5a | ||
|
|
c000f438ae | ||
|
|
c0d7ddf1fb | ||
|
|
5bf794e24e | ||
|
|
a9cfb160c9 | ||
|
|
468c9a9061 | ||
|
|
da1279aa5b | ||
|
|
3ec053bea7 | ||
|
|
b2f9f81eaf | ||
|
|
939e957fda | ||
|
|
062fe72030 | ||
|
|
cdcbb872d5 | ||
|
|
048763053c | ||
|
|
c6489d9b01 | ||
|
|
299053f2d5 | ||
|
|
937ac84538 | ||
|
|
5c0d1355a5 | ||
|
|
a64604de6b | ||
|
|
1a3dac0c17 | ||
|
|
ffd73e8bfb | ||
|
|
27c536b1a1 | ||
|
|
dc5973a0f3 | ||
|
|
b812b1c579 | ||
|
|
b89480a284 | ||
|
|
5846c71095 | ||
|
|
142f081d1e | ||
|
|
ec2b4f584c | ||
|
|
88d991ef45 | ||
|
|
dc382a6df7 | ||
|
|
c6e57d64d7 | ||
|
|
dc3cd430c8 | ||
|
|
9848600335 | ||
|
|
cd79b73cb0 | ||
|
|
21aa1b43fd | ||
|
|
2f3a96f7a7 | ||
|
|
b761dcde45 | ||
|
|
b6cdb0f885 | ||
|
|
472969ae2a | ||
|
|
9e3514a993 | ||
|
|
aee3ddd06a | ||
|
|
9558d404fa | ||
|
|
0ca6b67132 | ||
|
|
9390b1eef5 | ||
|
|
0198167b27 | ||
|
|
83e9bc4816 | ||
|
|
87bf6b3800 | ||
|
|
7b3df02640 | ||
|
|
499246abae | ||
|
|
71f78cc895 | ||
|
|
5896eee693 | ||
|
|
ce6b60653c | ||
|
|
2b865e3505 | ||
|
|
dee0e5fce7 | ||
|
|
56269758c4 | ||
|
|
436f6bda3e | ||
|
|
4d1102db0b | ||
|
|
4987a2158e | ||
|
|
435a5a67bc | ||
|
|
fc686ca618 | ||
|
|
ec99096d52 | ||
|
|
68e520fa2d | ||
|
|
d46e612cb1 | ||
|
|
c48df8522a | ||
|
|
b0a2c26b22 | ||
|
|
f865da6d6a | ||
|
|
58e4dc1b6c | ||
|
|
17c03bed21 | ||
|
|
4bfc5355db | ||
|
|
d865618051 | ||
|
|
a995857cca | ||
|
|
fdbdebac32 | ||
|
|
a242c40b23 | ||
|
|
f82b84eac7 | ||
|
|
70be333691 | ||
|
|
3d9a4dcbf3 | ||
|
|
577efbf0c2 | ||
|
|
a3fca638ee | ||
|
|
027855f891 | ||
|
|
b9b04fd932 | ||
|
|
2b1dcaf5e3 | ||
|
|
9e32a5613d | ||
|
|
61b43a0828 | ||
|
|
7fc1fed91e | ||
|
|
1fd97f8732 | ||
|
|
aa03de8e52 | ||
|
|
eb9a7267bd | ||
|
|
21b7c6a2c0 | ||
|
|
4cd53ce119 | ||
|
|
e2283e53b6 | ||
|
|
d51d5af992 | ||
|
|
b98a8ee83a | ||
|
|
21cf1f6c47 | ||
|
|
918ea5d12f | ||
|
|
3b7551e1e4 | ||
|
|
5dbc0aedcc | ||
|
|
8819d559b7 | ||
|
|
448a5c00fd | ||
|
|
8194c62f4e | ||
|
|
e75debbf81 | ||
|
|
5b475a05ef | ||
|
|
f69dcf38ef | ||
|
|
966ba97b2c | ||
|
|
675688cb80 | ||
|
|
59c7edfd90 | ||
|
|
660fbfd73c | ||
|
|
7b011c1d96 | ||
|
|
3fffd08ae4 | ||
|
|
abc8077a96 | ||
|
|
5a125c7fe5 | ||
|
|
02fb2baf62 | ||
|
|
23c9f973cc | ||
|
|
db90fa71d4 | ||
|
|
c3a6ae1622 | ||
|
|
76aab2a2ac | ||
|
|
a02effc32e | ||
|
|
965c1f0353 | ||
|
|
e7086cf6df | ||
|
|
af073dad46 | ||
|
|
b79e8b8130 | ||
|
|
3b25200868 | ||
|
|
3985e52a3a | ||
|
|
fee44d83c8 | ||
|
|
d70c81f03b | ||
|
|
387ec89b95 | ||
|
|
60dd68490c | ||
|
|
378e55ed0c | ||
|
|
16900ad421 | ||
|
|
0bf1e8f252 | ||
|
|
0aa5505d7f | ||
|
|
60f68abd31 | ||
|
|
0a677449dc | ||
|
|
6e9723325f | ||
|
|
e9a046e74d | ||
|
|
44f0c749d5 | ||
|
|
082d2c739e | ||
|
|
6354b1aec0 | ||
|
|
512dbf1980 | ||
|
|
ed491b0caf | ||
|
|
d6814587ad | ||
|
|
586399a814 | ||
|
|
867186fd66 | ||
|
|
48c18ee8d1 | ||
|
|
f5d5d9a504 | ||
|
|
f958924b79 | ||
|
|
67582aaee4 | ||
|
|
8ebe260628 | ||
|
|
a6d2590834 | ||
|
|
923cd045cd | ||
|
|
f64bcf0d08 | ||
|
|
c48b6e23eb | ||
|
|
b2eabda5b6 | ||
|
|
305e12be1d | ||
|
|
30ad784d95 | ||
|
|
7be7b07c19 | ||
|
|
88c46f4612 | ||
|
|
1c1b9b5c9d | ||
|
|
f5f3256824 | ||
|
|
d056b1f1e1 | ||
|
|
c86a30921f | ||
|
|
14f09e3787 | ||
|
|
d2a342a94e | ||
|
|
c61e4c02bd | ||
|
|
b8ce61ae45 | ||
|
|
5a25b0885c | ||
|
|
337cbb2844 | ||
|
|
fa3b84a615 | ||
|
|
4e47447dec | ||
|
|
a1772d26b5 | ||
|
|
77e5e75b2f | ||
|
|
77bfd85e9e | ||
|
|
272ea9fe17 | ||
|
|
8aed1aa634 | ||
|
|
2bec41b80e | ||
|
|
38633b6db4 | ||
|
|
e62dc00d7b | ||
|
|
f619fc3e7e | ||
|
|
6c7d74c43b | ||
|
|
268cead331 | ||
|
|
70521f0756 | ||
|
|
1f283d93ca | ||
|
|
7dc422887c | ||
|
|
7dea0dcfc4 | ||
|
|
ab11604bfb | ||
|
|
0e8c026854 | ||
|
|
3b1cc1cc34 | ||
|
|
85a8f2f147 | ||
|
|
37c43199ca | ||
|
|
f22fcc2e59 | ||
|
|
05d7d58cd5 | ||
|
|
10e89f6802 | ||
|
|
551b802d89 | ||
|
|
5ae8607771 | ||
|
|
7b47f40979 | ||
|
|
a9a76b9010 | ||
|
|
cf68725c89 | ||
|
|
989d703d1d | ||
|
|
cf2ef0955d | ||
|
|
2fe3b0de55 | ||
|
|
b47047a91d | ||
|
|
1c56ac7e48 | ||
|
|
47fb553d38 | ||
|
|
bd6362f09f | ||
|
|
9cc735bdf2 | ||
|
|
f5d992f609 | ||
|
|
25407fb5f0 | ||
|
|
9eb9bd8488 | ||
|
|
7d6eafb2f2 | ||
|
|
bfdf25a162 | ||
|
|
6e0e6f5ec4 | ||
|
|
dce8b15937 | ||
|
|
25e2e07631 | ||
|
|
b5fb8b6d82 | ||
|
|
171c5aa50c | ||
|
|
b5d378c425 | ||
|
|
145d65fd60 | ||
|
|
5c78fe1070 | ||
|
|
31b1203317 | ||
|
|
eaa200a766 | ||
|
|
f422c84414 | ||
|
|
4562f31b6b | ||
|
|
6911e184f9 | ||
|
|
9627212510 | ||
|
|
0fc8c4e071 | ||
|
|
6c93dc6a4c | ||
|
|
1dbbfbbba6 | ||
|
|
88ab36c45b | ||
|
|
14247ddabb | ||
|
|
fd8561ac55 | ||
|
|
e1e6ba36ca | ||
|
|
435a50de4b | ||
|
|
93b2b9b7b0 | ||
|
|
faa5199a9a | ||
|
|
5768d7a247 | ||
|
|
d9f2fca66d | ||
|
|
91b48258f0 | ||
|
|
2ac7b9dabf | ||
|
|
505f0f65d0 | ||
|
|
8262a8dea2 | ||
|
|
406286b970 | ||
|
|
6346f835af | ||
|
|
73fc437c7d | ||
|
|
af66739207 | ||
|
|
65bb6a8b73 | ||
|
|
8bda5aa2a7 | ||
|
|
dd2b43dc73 | ||
|
|
c925014bb5 | ||
|
|
32b11b104f | ||
|
|
e25a73f9af | ||
|
|
15c1b48b25 | ||
|
|
d14a86069a | ||
|
|
186f753aec | ||
|
|
f180707b6f | ||
|
|
9a53779b6c | ||
|
|
551771c6cf | ||
|
|
a3aca0bae4 | ||
|
|
ed0c71fa56 | ||
|
|
574ecdb512 | ||
|
|
7848a35941 | ||
|
|
dfe0314ba0 | ||
|
|
4618ef0cbf | ||
|
|
bb281649fc | ||
|
|
012ffcf6f6 | ||
|
|
bef61cf019 | ||
|
|
5ce6464461 | ||
|
|
a0c3f99d80 | ||
|
|
f6ba577cf4 | ||
|
|
6ef03b7c6c | ||
|
|
05303dab45 | ||
|
|
9edaa407f2 | ||
|
|
be03f6adb6 | ||
|
|
c37496f2b0 | ||
|
|
8743ba2886 | ||
|
|
c100054d68 | ||
|
|
2fbb87e17a | ||
|
|
1eadf52f5e | ||
|
|
9f3681642a | ||
|
|
a4ca679b65 | ||
|
|
d198095d7a | ||
|
|
438455bc8c | ||
|
|
7914f65f68 | ||
|
|
d0b79ad335 | ||
|
|
7dc4484f6a | ||
|
|
257ea391da | ||
|
|
4c64bcfae7 | ||
|
|
99d2f537c2 | ||
|
|
6c32365e00 | ||
|
|
95925aafb1 | ||
|
|
0a3d4a5ab0 | ||
|
|
2328c60d12 | ||
|
|
61cd5cf4e6 | ||
|
|
1eb9efe2d5 | ||
|
|
d5882f2efe | ||
|
|
5f3991127b | ||
|
|
b995dc54f1 | ||
|
|
79404e754e |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,11 +1,14 @@
|
||||
node_modules
|
||||
coverage/
|
||||
.aws-config.json
|
||||
dist
|
||||
|
||||
# locally required config files
|
||||
web.config
|
||||
config.js
|
||||
src/css/*.min.css
|
||||
|
||||
# Editor junk
|
||||
*.sublime-workspace
|
||||
*.swp
|
||||
.idea/
|
||||
|
||||
21
.jsfmtrc
Normal file
21
.jsfmtrc
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"preset" : "default",
|
||||
|
||||
"lineBreak" : {
|
||||
"before" : {
|
||||
"VariableDeclarationWithoutInit" : 0,
|
||||
},
|
||||
|
||||
"after": {
|
||||
"AssignmentOperator": -1,
|
||||
"ArgumentListArrayExpression": ">=1"
|
||||
}
|
||||
},
|
||||
|
||||
"whiteSpace" : {
|
||||
"before" : {
|
||||
},
|
||||
"after" : {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
git:
|
||||
depth: 1
|
||||
before_script:
|
||||
- npm install -g grunt-cli
|
||||
- npm install -g grunt-cli
|
||||
after_script:
|
||||
- npm run coveralls
|
||||
|
||||
355
CHANGELOG.md
355
CHANGELOG.md
@@ -1,88 +1,309 @@
|
||||
# vNext
|
||||
# 1.9.1 (2014-12-29)
|
||||
|
||||
**Enhancements**
|
||||
- [Issue #1028](https://github.com/grafana/grafana/issues/1028). Graph: New legend option ``hideEmtpy`` to hide series with only null values from legend
|
||||
- [Issue #1242](https://github.com/grafana/grafana/issues/1242). OpenTSDB: Downsample query field now supports interval template variable
|
||||
- [Issue #1126](https://github.com/grafana/grafana/issues/1126). InfluxDB: Support more than 10 series name segments when using alias ``$number`` patterns
|
||||
|
||||
**Fixes**
|
||||
- [Issue #1251](https://github.com/grafana/grafana/issues/1251). Graph: Fix for y axis and scaled units (GiB etc) caused rounding, for example 400 GiB instead of 378 GiB
|
||||
- [Issue #1199](https://github.com/grafana/grafana/issues/1199). Graph: fix for series tooltip when one series is hidden/disabled
|
||||
- [Issue #1207](https://github.com/grafana/grafana/issues/1207). Graphite: movingAverage / movingMedian parameter type impovement, now handles int and interval parameter
|
||||
|
||||
# 1.9.0 (2014-12-02)
|
||||
|
||||
**Enhancements**
|
||||
- [Issue #1130](https://github.com/grafana/grafana/issues/1130). SinglestatPanel: Added null point handling, and value to text mapping
|
||||
|
||||
|
||||
**Fixes**
|
||||
- [Issue #1087](https://github.com/grafana/grafana/issues/1087). Panel: Fixed IE9 crash due to angular drag drop
|
||||
- [Issue #1093](https://github.com/grafana/grafana/issues/1093). SingleStatPanel: Fixed position for drilldown link tooltip when dashboard requires scrolling
|
||||
- [Issue #1095](https://github.com/grafana/grafana/issues/1095). DrilldownLink: template variables in params property was not interpolated
|
||||
- [Issue #1114](https://github.com/grafana/grafana/issues/1114). Graphite: Lexer fix, allow equal sign (=) in metric paths
|
||||
- [Issue #1136](https://github.com/grafana/grafana/issues/1136). Graph: Fix to legend value Max and negative values
|
||||
- [Issue #1150](https://github.com/grafana/grafana/issues/1150). SinglestatPanel: Fixed absolute drilldown link issue
|
||||
- [Issue #1123](https://github.com/grafana/grafana/issues/1123). Firefox: Workaround for Firefox bug, casued input text fields to not be selectable and not have placeable cursor
|
||||
- [Issue #1108](https://github.com/grafana/grafana/issues/1108). Graph: Fix for tooltip series order when series draw order was changed with zindex property
|
||||
|
||||
# 1.9.0-rc1 (2014-11-17)
|
||||
|
||||
**UI Improvements**
|
||||
- [Issue #770](https://github.com/grafana/grafana/issues/770). UI: Panel dropdown menu replaced with a new panel menu
|
||||
|
||||
**Graph**
|
||||
- [Issue #877](https://github.com/grafana/grafana/issues/877). Graph: Smart auto decimal precision when using scaled unit formats
|
||||
- [Issue #850](https://github.com/grafana/grafana/issues/850). Graph: Shared tooltip that shows multiple series & crosshair line, thx @toni-moreno
|
||||
- [Issue #940](https://github.com/grafana/grafana/issues/940). Graph: New series style override option "Fill below to", useful to visualize max & min as a shadow for the mean
|
||||
- [Issue #1030](https://github.com/grafana/grafana/issues/1030). Graph: Legend table display/look changed, now includes column headers for min/max/avg, and full width (unless on right side)
|
||||
- [Issue #861](https://github.com/grafana/grafana/issues/861). Graph: Export graph time series data as csv file
|
||||
|
||||
**New Panels**
|
||||
- [Issue #951](https://github.com/grafana/grafana/issues/951). SingleStat: New singlestat panel
|
||||
|
||||
**Misc**
|
||||
- [Issue #864](https://github.com/grafana/grafana/issues/846). Panel: Share panel feature, get a link to panel with the current time range
|
||||
- [Issue #938](https://github.com/grafana/grafana/issues/938). Panel: Plugin panels now reside outside of app/panels directory
|
||||
- [Issue #952](https://github.com/grafana/grafana/issues/952). Help: Shortcut "?" to open help modal with list of all shortcuts
|
||||
- [Issue #991](https://github.com/grafana/grafana/issues/991). ScriptedDashboard: datasource services are now available in scripted dashboards, you can query datasource for metric keys, generate dashboards, and even save them in a scripted dashboard (see scripted_gen_and_save.js for example)
|
||||
- [Issue #1041](https://github.com/grafana/grafana/issues/1041). Panel: All panels can now have links to other dashboards or absolute links, these links are available in the panel menu.
|
||||
|
||||
**Changes**
|
||||
- [Issue #1007](https://github.com/grafana/grafana/issues/1007). Graph: Series hide/show toggle changed to be default exclusive, so clicking on a series name will show only that series. (SHIFT or meta)+click will toggle hide/show.
|
||||
|
||||
**OpenTSDB**
|
||||
- [Issue #930](https://github.com/grafana/grafana/issues/930). OpenTSDB: Adding counter max and counter reset value to open tsdb query editor, thx @rsimiciuc
|
||||
- [Issue #917](https://github.com/grafana/grafana/issues/917). OpenTSDB: Templating support for OpenTSDB series name and tags, thx @mchataigner
|
||||
|
||||
**InfluxDB**
|
||||
- [Issue #714](https://github.com/grafana/grafana/issues/714). InfluxDB: Support for sub second resolution graphs
|
||||
|
||||
**Fixes**
|
||||
- [Issue #925](https://github.com/grafana/grafana/issues/925). Graph: bar width calculation fix for some edge cases (bars would render on top of each other)
|
||||
- [Issue #505](https://github.com/grafana/grafana/issues/505). Graph: fix for second y axis tick unit labels wrapping on the next line
|
||||
- [Issue #987](https://github.com/grafana/grafana/issues/987). Dashboard: Collapsed rows became invisible when hide controls was enabled
|
||||
|
||||
=======
|
||||
# 1.8.1 (2014-09-30)
|
||||
|
||||
**Fixes**
|
||||
- [Issue #855](https://github.com/grafana/grafana/issues/855). Graph: Fix for scroll issue in graph edit mode when dropdown goes below screen
|
||||
- [Issue #847](https://github.com/grafana/grafana/issues/847). Graph: Fix for series draw order not being the same after hiding/unhiding series
|
||||
- [Issue #851](https://github.com/grafana/grafana/issues/851). Annotations: Fix for annotations not reloaded when switching between 2 dashboards with annotations
|
||||
- [Issue #846](https://github.com/grafana/grafana/issues/846). Edit panes: Issue when open row or json editor when scrolled down the page, unable to scroll and you did not see editor
|
||||
- [Issue #840](https://github.com/grafana/grafana/issues/840). Import: Fixes to import from json file and import from graphite. Issues was lingering state from previous dashboard.
|
||||
- [Issue #859](https://github.com/grafana/grafana/issues/859). InfluxDB: Fix for bug when saving dashboard where title is the same as slugified url id
|
||||
- [Issue #852](https://github.com/grafana/grafana/issues/852). White theme: Fixes for hidden series legend text and disabled annotations color
|
||||
|
||||
# 1.8.0 (2014-09-22)
|
||||
|
||||
Read this [blog post](http://grafana.org/blog/2014/09/11/grafana-1-8-0-rc1-released.html) for an overview of all improvements.
|
||||
|
||||
**Fixes**
|
||||
- [Issue #802](https://github.com/grafana/grafana/issues/802). Annotations: Fix when using InfluxDB datasource
|
||||
- [Issue #795](https://github.com/grafana/grafana/issues/795). Chrome: Fix for display issue in chrome beta & chrome canary when entering edit mode
|
||||
- [Issue #818](https://github.com/grafana/grafana/issues/818). Graph: Added percent y-axis format
|
||||
- [Issue #828](https://github.com/grafana/grafana/issues/828). Elasticsearch: saving new dashboard with title equal to slugified url would cause it to deleted.
|
||||
- [Issue #830](https://github.com/grafana/grafana/issues/830). Annotations: Fix for elasticsearch annotations and mapping nested fields
|
||||
|
||||
# 1.8.0-RC1 (2014-09-12)
|
||||
|
||||
**UI polish / changes**
|
||||
- [Issue #725](https://github.com/grafana/grafana/issues/725). UI: All modal editors are removed and replaced by an edit pane under menu. The look of editors is also updated and polished. Search dropdown is also shown as pane under menu and has seen some UI polish.
|
||||
|
||||
**Filtering/Templating feature overhaul**
|
||||
- Filtering renamed to Templating, and filter items to variables
|
||||
- Filter editing has gotten its own edit pane with much improved UI and options
|
||||
- [Issue #296](https://github.com/grafana/grafana/issues/296). Templating: Can now retrieve variable values from a non-default data source
|
||||
- [Issue #219](https://github.com/grafana/grafana/issues/219). Templating: Template variable value selection is now a typeahead autocomplete dropdown
|
||||
- [Issue #760](https://github.com/grafana/grafana/issues/760). Templating: Extend template variable syntax to include $variable syntax replacement
|
||||
- [Issue #234](https://github.com/grafana/grafana/issues/234). Templating: Interval variable type for time intervals summarize/group by parameter, included "auto" option, and auto step counts option.
|
||||
- [Issue #262](https://github.com/grafana/grafana/issues/262). Templating: Ability to use template variables for function parameters via custom variable type, can be used as parameter for movingAverage or scaleToSeconds for example
|
||||
- [Issue #312](https://github.com/grafana/grafana/issues/312). Templating: Can now use template variables in panel titles
|
||||
- [Issue #613](https://github.com/grafana/grafana/issues/613). Templating: Full support for InfluxDB, filter by part of series names, extract series substrings, nested queries, multipe where clauses!
|
||||
- Template variables can be initialized from url, with var-my_varname=value, breaking change, before it was just my_varname.
|
||||
- Templating and url state sync has some issues that are not solved for this release, see [Issue #772](https://github.com/grafana/grafana/issues/772) for more details.
|
||||
|
||||
**InfluxDB Breaking changes**
|
||||
- To better support templating, fill(0) and group by time low limit some changes has been made to the editor and query model schema
|
||||
- Currently some of these changes are breaking
|
||||
- If you used custom condition filter you need to open the graph in edit mode, the editor will update the schema, and the queries should work again
|
||||
- If you used a raw query you need to remove the time filter and replace it with $timeFilter (this is done automatically when you switch from query editor to raw query, but old raw queries needs to updated)
|
||||
- If you used group by and later removed the group by the graph could break, open in editor and should correct it
|
||||
- InfluxDB annotation queries that used [[timeFilter]] should be updated to use $timeFilter syntax instead
|
||||
- Might write an upgrade tool to update dashboards automatically, but right now master (1.8) includes the above breaking changes
|
||||
|
||||
**InfluxDB query editor enhancements**
|
||||
- [Issue #756](https://github.com/grafana/grafana/issues/756). InfluxDB: Add option for fill(0) and fill(null), integrated help in editor for why this option is important when stacking series
|
||||
- [Issue #743](https://github.com/grafana/grafana/issues/743). InfluxDB: A group by time option for all queries in graph panel that supports a low limit for auto group by time, very important for stacking and fill(0)
|
||||
- The above to enhancements solves the problems associated with stacked bars and lines when points are missing, these issues are solved:
|
||||
- [Issue #673](https://github.com/grafana/grafana/issues/673). InfluxDB: stacked bars missing intermediate data points, unless lines also enabled
|
||||
- [Issue #674](https://github.com/grafana/grafana/issues/674). InfluxDB: stacked chart ignoring series without latest values
|
||||
- [Issue #534](https://github.com/grafana/grafana/issues/534). InfluxDB: No order in stacked bars mode
|
||||
|
||||
**New features and improvements**
|
||||
- [Issue #117](https://github.com/grafana/grafana/issues/117). Graphite: Graphite query builder can now handle functions that multiple series as arguments!
|
||||
- [Issue #281](https://github.com/grafana/grafana/issues/281). Graphite: Metric node/segment selection is now a textbox with autocomplete dropdown, allow for custom glob expression for single node segment without entering text editor mode.
|
||||
- [Issue #304](https://github.com/grafana/grafana/issues/304). Dashboard: View dashboard json, edit/update any panel using json editor, makes it possible to quickly copy a graph from one dashboard to another.
|
||||
- [Issue #578](https://github.com/grafana/grafana/issues/578). Dashboard: Row option to display row title even when the row is visible
|
||||
- [Issue #672](https://github.com/grafana/grafana/issues/672). Dashboard: panel fullscreen & edit state is present in url, can now link to graph in edit & fullscreen mode.
|
||||
- [Issue #709](https://github.com/grafana/grafana/issues/709). Dashboard: Small UI look polish to search results, made dashboard title link are larger
|
||||
- [Issue #425](https://github.com/grafana/grafana/issues/425). Graph: New section in 'Display Styles' tab to override any display setting on per series bases (mix and match lines, bars, points, fill, stack, line width etc)
|
||||
- [Issue #634](https://github.com/grafana/grafana/issues/634). Dashboard: Dashboard tags now in different colors (from fixed palette) determined by tag name.
|
||||
- [Issue #685](https://github.com/grafana/grafana/issues/685). Dashboard: New config.js option to change/remove window title prefix.
|
||||
- [Issue #781](https://github.com/grafana/grafana/issues/781). Dashboard: Title URL is now slugified for greater URL readability, works with both ES & InfluxDB storage, is backward compatible
|
||||
- [Issue #785](https://github.com/grafana/grafana/issues/785). Elasticsearch: Support for full elasticsearch lucene search grammar when searching for dashboards, better async search
|
||||
- [Issue #787](https://github.com/grafana/grafana/issues/787). Dashboard: time range can now be read from URL parameters, will override dashboard saved time range
|
||||
|
||||
**Fixes**
|
||||
- [Issue #696](https://github.com/grafana/grafana/issues/696). Graph: Fix for y-axis format 'none' when values are in scientific notation (ex 2.3e-13)
|
||||
- [Issue #733](https://github.com/grafana/grafana/issues/733). Graph: Fix for tooltip current value decimal precision when 'none' axis format was selected
|
||||
- [Issue #697](https://github.com/grafana/grafana/issues/697). Graphite: Fix for Glob syntax in graphite queries ([1-9] and ?) that made the query editor / parser bail and fallback to a text box.
|
||||
- [Issue #702](https://github.com/grafana/grafana/issues/702). Graphite: Fix for nonNegativeDerivative function, now possible to not include optional first parameter maxValue
|
||||
- [Issue #277](https://github.com/grafana/grafana/issues/277). Dashboard: Fix for timepicker date & tooltip when UTC timezone selected.
|
||||
- [Issue #699](https://github.com/grafana/grafana/issues/699). Dashboard: Fix for bug when adding rows from dashboard settings dialog.
|
||||
- [Issue #723](https://github.com/grafana/grafana/issues/723). Dashboard: Fix for hide controls setting not used/initialized on dashboard load
|
||||
- [Issue #724](https://github.com/grafana/grafana/issues/724). Dashboard: Fix for zoom out causing right hand "to" range to be set in the future.
|
||||
|
||||
**Tech**
|
||||
- Upgraded from angularjs 1.1.5 to 1.3 beta 17;
|
||||
- Switch from underscore to lodash
|
||||
- helpers to easily unit test angularjs controllers and services
|
||||
- Test coverage through coveralls
|
||||
- Upgrade from jquery 1.8.0 to 2.1.1 (**Removes support for IE7 & IE8**)
|
||||
|
||||
# 1.7.1 (unreleased)
|
||||
|
||||
**Fixes**
|
||||
- [Issue #691](https://github.com/grafana/grafana/issues/691). Dashboard: Tooltip fixes, sometimes they would not show, and sometimes they would get stuck.
|
||||
- [Issue #695](https://github.com/grafana/grafana/issues/695). Dashboard: Tooltip on goto home menu icon would get stuck after clicking on it
|
||||
|
||||
# 1.7.0 (2014-08-11)
|
||||
|
||||
**Fixes**
|
||||
- [Issue #652](https://github.com/grafana/grafana/issues/652). Timepicker: Entering custom date range impossible when refresh is low (now is constantly reset)
|
||||
- [Issue #450](https://github.com/grafana/grafana/issues/450). Graph: Tooltip does not disappear sometimes and would get stuck
|
||||
- [Issue #655](https://github.com/grafana/grafana/issues/655). General: Auto refresh not initiated / started after dashboard loading
|
||||
- [Issue #657](https://github.com/grafana/grafana/issues/657). General: Fix for refresh icon in IE browsers
|
||||
- [Issue #661](https://github.com/grafana/grafana/issues/661). Annotations: Elasticsearch querystring with filter template replacements was not interpolated
|
||||
- [Issue #660](https://github.com/grafana/grafana/issues/660). OpenTSDB: fix opentsdb queries that returned more than one series
|
||||
|
||||
**Change**
|
||||
- [Issue #681](https://github.com/grafana/grafana/issues/681). Dashboard: The panel error bar has been replaced with a small error indicator, this indicator does not change panel height and is a lot less intrusive. Hover over it for short details, click on it for more details.
|
||||
|
||||
# 1.7.0-rc1 (2014-08-05)
|
||||
|
||||
**New features or improvements**
|
||||
- [Issue #581](https://github.com/grafana/grafana/issues/581). InfluxDB: Add continuous query in series results (series typeahead).
|
||||
- [Issue #584](https://github.com/grafana/grafana/issues/584). InfluxDB: Support for alias & alias patterns when using raw query mode
|
||||
- [Issue #394](https://github.com/grafana/grafana/issues/394). InfluxDB: Annotation support
|
||||
- [Issue #633](https://github.com/grafana/grafana/issues/633). InfluxDB: InfluxDB can now act as a datastore for dashboards
|
||||
- [Issue #610](https://github.com/grafana/grafana/issues/610). InfluxDB: Support for InfluxdB v0.8 list series response schemea (series typeahead)
|
||||
- [Issue #525](https://github.com/grafana/grafana/issues/525). InfluxDB: Enhanced series aliasing (legend names) with pattern replacements
|
||||
- [Issue #266](https://github.com/grafana/grafana/issues/266). Graphite: New option cacheTimeout to override graphite default memcache timeout
|
||||
- [Issue #606](https://github.com/grafana/grafana/issues/606). General: New global option in config.js to specify admin password (useful to hinder users from accidentally make changes)
|
||||
- [Issue #201](https://github.com/grafana/grafana/issues/201). Annotations: Elasticsearch datasource support for events
|
||||
- [Issue #344](https://github.com/grafana/grafana/issues/344). Annotations: Annotations can now be fetched from non default datasources
|
||||
- [Issue #631](https://github.com/grafana/grafana/issues/631). Search: max_results config.js option & scroll in search results (To show more or all dashboards)
|
||||
- [Issue #511](https://github.com/grafana/grafana/issues/511). Text panel: Allow [[..]] filter notation in all text panels (markdown/html/text)
|
||||
- [Issue #136](https://github.com/grafana/grafana/issues/136). Graph: New legend display option "Align as table"
|
||||
- [Issue #556](https://github.com/grafana/grafana/issues/556). Graph: New legend display option "Right side", will show legend to the right of the graph
|
||||
- [Issue #604](https://github.com/grafana/grafana/issues/604). Graph: New axis format, 'bps' (SI unit in steps of 1000) useful for network gear metics
|
||||
- [Issue #626](https://github.com/grafana/grafana/issues/626). Graph: Downscale y axis to more precise unit, value of 0.1 for seconds format will be formated as 100 ms. Thanks @kamaradclimber
|
||||
- [Issue #618](https://github.com/grafana/grafana/issues/618). OpenTSDB: Series alias option to override metric name returned from opentsdb. Thanks @heldr
|
||||
|
||||
**Documentation**
|
||||
- [Issue #635](https://github.com/grafana/grafana/issues/635). Docs for features and changes in v1.7, new troubleshooting guide, new Getting started guide, improved install & config guide.
|
||||
|
||||
|
||||
**Changes**
|
||||
- [Issue #536](https://github.com/grafana/grafana/issues/536). Graphite: Use unix epoch for Graphite from/to for absolute time ranges
|
||||
- [Issue #641](https://github.com/grafana/grafana/issues/536). General: Dashboard save temp copy feature settings moved from dashboard to config.js, default is enabled, and ttl to 30 days
|
||||
- [Issue #532](https://github.com/grafana/grafana/issues/532). Schema: Dashboard schema changes, "Unsaved changes" should not appear for schema changes. All changes are backward compatible with old schema.
|
||||
|
||||
**Fixes**
|
||||
- [Issue #545](https://github.com/grafana/grafana/issues/545). Graph: Fix formatting negative values (axis formats, legend values)
|
||||
- [Issue #460](https://github.com/grafana/grafana/issues/460). Graph: fix for max legend value when max value is zero
|
||||
- [Issue #628](https://github.com/grafana/grafana/issues/628). Filtering: Fix for nested filters, changing a child filter could result in infinite recursion in some cases
|
||||
- [Issue #528](https://github.com/grafana/grafana/issues/528). Graphite: Fix for graphite expressions parser failure when metric expressions starts with curly brace segment
|
||||
|
||||
# 1.6.1 (2014-06-24)
|
||||
|
||||
**New features or improvements**
|
||||
- [Issue #360](https://github.com/grafana/grafana/issues/360). Ability to set y min/max for right y-axis (RR #519)
|
||||
|
||||
**Fixes**
|
||||
|
||||
- [Issue #500](https://github.com/grafana/grafana/issues/360). Fixes regex InfluxDB queries intoduced in 1.6.0
|
||||
- [Issue #506](https://github.com/grafana/grafana/issues/506). Bug in when using % sign in legends (aliases), fixed by removing url decoding of metric names
|
||||
- [Issue #522](https://github.com/grafana/grafana/issues/522). Series names and column name typeahead cache fix
|
||||
- [Issue #504](https://github.com/grafana/grafana/issues/504). Fixed influxdb issue with raw query that caused wrong value column detection
|
||||
- [Issue #526](https://github.com/grafana/grafana/issues/526). Default property that marks which datasource is default in config.js is now optional
|
||||
- [Issue #342](https://github.com/grafana/grafana/issues/342). Auto-refresh caused 2 refreshes (and hence mulitple queries) each time (at least in firefox)
|
||||
|
||||
# 1.6.0 (2014-06-16)
|
||||
|
||||
#### New features or improvements
|
||||
- New Y-axis formater for metric values that represent seconds (Issue #427) - thx @jippi
|
||||
- Allow special characters in serie names (influxdb datasource), PR #390 - thx @majst01
|
||||
- Refactoring of filterSrv (Issue #428), thx @Tetha
|
||||
- New config for playlist feature. Set playlist_timespan to set default playlist interval (Issue #445) - thx @rmca
|
||||
- New graphite function definition added isNonNull (PR #461), - thx @tmonk42
|
||||
- New InfluxDB function difference add to function dropdown (PR #455)
|
||||
- Added parameter to keepLastValue graphite function definition (default 100), Closes #459
|
||||
- [Issue #427](https://github.com/grafana/grafana/issues/427). New Y-axis formater for metric values that represent seconds, Thanks @jippi
|
||||
- [Issue #390](https://github.com/grafana/grafana/issues/390). Allow special characters in serie names (influxdb datasource), Thanks @majst01
|
||||
- [Issue #428](https://github.com/grafana/grafana/issues/428). Refactoring of filterSrv, Thanks @Tetha
|
||||
- [Issue #445](https://github.com/grafana/grafana/issues/445). New config for playlist feature. Set playlist_timespan to set default playlist interval, Thanks @rmca
|
||||
- [Issue #461](https://github.com/grafana/grafana/issues/461). New graphite function definition added isNonNull, Thanks @tmonk42
|
||||
- [Issue #455](https://github.com/grafana/grafana/issues/455). New InfluxDB function difference add to function dropdown
|
||||
- [Issue #459](https://github.com/grafana/grafana/issues/459). Added parameter to keepLastValue graphite function definition (default 100)
|
||||
[Issue #418](https://github.com/grafana/grafana/issues/418). to the browser cache when upgrading grafana and improve load performance
|
||||
- [Issue #327](https://github.com/grafana/grafana/issues/327). Partial support for url encoded metrics when using Graphite datasource. Thanks @axe-felix
|
||||
- [Issue #473](https://github.com/grafana/grafana/issues/473). Improvement to InfluxDB query editor and function/value column selection
|
||||
- [Issue #375](https://github.com/grafana/grafana/issues/375). Initial support for filtering (templated queries) for InfluxDB. Thanks @mavimo
|
||||
- [Issue #475](https://github.com/grafana/grafana/issues/475). Row editing and adding new panel is now a lot quicker and easier with the new row menu
|
||||
- [Issue #211](https://github.com/grafana/grafana/issues/211). New datasource! Initial support for OpenTSDB, Thanks @mpage
|
||||
- [Issue #492](https://github.com/grafana/grafana/issues/492). Improvement and polish to the OpenTSDB query editor
|
||||
- [Issue #441](https://github.com/grafana/grafana/issues/441). Influxdb group by support, Thanks @piis3
|
||||
- improved asset (css/js) build pipeline, added revision to css and js. Will remove issues related
|
||||
to the browser cache when upgrading grafana and improve load performance (Fixes #418)
|
||||
- Partial support for url encoded metrics when using Graphite datasource (PR #327) - thx @axe-felix
|
||||
- Improvement to InfluxDB query editor and function/value column selection (Issue #473)
|
||||
- Initial support for filtering (templated queries) for InfluxDB (PR #375) - thx @mavimo
|
||||
- Row editing and adding new panel is now a lot quicker and easier with the new row menu (Issue #475)
|
||||
- New datasource! Initial support for OpenTSDB (PR #211) - thx @mpage
|
||||
- Improvement and polish to the OpenTSDB query editor (Issue #492)
|
||||
- Influxdb group by support (Issue #441) thx @piis3
|
||||
|
||||
|
||||
#### Changes
|
||||
- Graphite panel is now renamed graph (Existing dashboards will still work)
|
||||
- Add panel icon and Row edit button is replaced by the Row edit menu (Issue #475)
|
||||
- [Issue #475](https://github.com/grafana/grafana/issues/475). Add panel icon and Row edit button is replaced by the Row edit menu
|
||||
- New graphs now have a default empty query
|
||||
- Add Row button now creates a row with default height of 250px (no longer opens dashboard settings modal)
|
||||
- Clean up of config.sample.js, graphiteUrl removed (still works, but depricated, removed in future)
|
||||
Use datasources config instead. panel_names removed from config.js. Use plugins.panels to add custom panels
|
||||
- Graphite panel is now renamed graph (Existing dashboards will still work)
|
||||
|
||||
#### Fixes
|
||||
- Graphite query lexer change, can now handle regex parameters for aliasSub function (Fixes #126)
|
||||
- Filter option loading when having muliple nested filters now works better.
|
||||
Options are now reloaded correctly and there are no multiple renders/refresh inbetween (#447),
|
||||
After an option is changed and a nested template param is also reloaded, if the current value
|
||||
exists after the options are reloaded the current selected value is kept (Closes #447, Closes #412)
|
||||
- Legend Current value did not display when value was zero, Fixes #460
|
||||
- Fix to series toggling bug that caused annotations to be hidden when toggling (hiding) series. Fixes #328
|
||||
- Fix for graphite function selection menu that some times draws outside screen. It now displays upward (Fixes #293)
|
||||
- Fix for exclusive series toggling (hold down CTRL, SHIFT or META key) and left click a series for exclusive toggling
|
||||
CTRL does not work on MAC OSX but SHIFT or META should (depending on browser) (Closes #350, Fixes #472)
|
||||
- [Issue #126](https://github.com/grafana/grafana/issues/126). Graphite query lexer change, can now handle regex parameters for aliasSub function
|
||||
- [Issue #447](https://github.com/grafana/grafana/issues/447). Filter option loading when having muliple nested filters now works better. Options are now reloaded correctly and there are no multiple renders/refresh inbetween.
|
||||
- [Issue #412](https://github.com/grafana/grafana/issues/412). After a filter option is changed and a nested template param is reloaded, if the current value exists after the options are reloaded the current selected value is kept.
|
||||
- [Issue #460](https://github.com/grafana/grafana/issues/460). Legend Current value did not display when value was zero
|
||||
- [Issue #328](https://github.com/grafana/grafana/issues/328). Fix to series toggling bug that caused annotations to be hidden when toggling/hiding series.
|
||||
- [Issue #293](https://github.com/grafana/grafana/issues/293). Fix for graphite function selection menu that some times draws outside screen. It now displays upward
|
||||
- [Issue #350](https://github.com/grafana/grafana/issues/350). Fix for exclusive series toggling (hold down CTRL, SHIFT or META key) and left click a series for exclusive toggling
|
||||
- [Issue #472](https://github.com/grafana/grafana/issues/472). CTRL does not work on MAC OSX but SHIFT or META should (depending on browser)
|
||||
|
||||
# 1.5.4 (2014-05-13)
|
||||
### New features and improvements
|
||||
- InfluxDB enhancement: support for multiple hosts (with retries) and raw queries (Issue #318, thx @toddboom)
|
||||
- InfluxDB enhancement: support for multiple hosts (with retries) and raw queries ([Issue #318](https://github.com/grafana/grafana/issues/318), thx @toddboom)
|
||||
- Added rounding for graphites from and to time range filters
|
||||
for very short absolute ranges (Issue #320)
|
||||
- Increased resolution for graphite datapoints (maxDataPoints), now equal to panel pixel width. (Closes #5)
|
||||
- Improvement to influxdb query editor, can now add where clause and alias (Issue #331, thanks @mavimo)
|
||||
- New config setting for graphite datasource to control if json render request is POST or GET (Issue #345)
|
||||
- Unsaved changes warning feature (Issue #324)
|
||||
- Improvement to series toggling, CTRL+MouseClick on series name will now hide all others (Issue #350)
|
||||
for very short absolute ranges ([Issue #320](https://github.com/grafana/grafana/issues/320))
|
||||
- Increased resolution for graphite datapoints (maxDataPoints), now equal to panel pixel width. ([Issue #5](https://github.com/grafana/grafana/issues/5))
|
||||
- Improvement to influxdb query editor, can now add where clause and alias ([Issue #331](https://github.com/grafana/grafana/issues/331), thanks @mavimo)
|
||||
- New config setting for graphite datasource to control if json render request is POST or GET ([Issue #345](https://github.com/grafana/grafana/issues/345))
|
||||
- Unsaved changes warning feature ([Issue #324](https://github.com/grafana/grafana/issues/324))
|
||||
- Improvement to series toggling, CTRL+MouseClick on series name will now hide all others ([Issue #350](https://github.com/grafana/grafana/issues/350))
|
||||
|
||||
### Changes
|
||||
- Graph default setting for Y-Min changed from zero to auto scalling (will not effect existing dashboards). (Issue #386) - thx @kamaradclimber
|
||||
- Graph default setting for Y-Min changed from zero to auto scalling (will not effect existing dashboards). ([Issue #386](https://github.com/grafana/grafana/issues/386)) - thx @kamaradclimber
|
||||
|
||||
### Fixes
|
||||
- Fixes to filters and "All" option. It now never uses "*" as value, but all options in a {node1, node2, node3} expression (Issue #228, #359)
|
||||
- Fix for InfluxDB query generation with columns containing dots or dashes (Issue #369, #348) - Thanks to @jbripley
|
||||
- Fixes to filters and "All" option. It now never uses "*" as value, but all options in a {node1, node2, node3} expression ([Issue #228](https://github.com/grafana/grafana/issues/228), #359)
|
||||
- Fix for InfluxDB query generation with columns containing dots or dashes ([Issue #369](https://github.com/grafana/grafana/issues/369), #348) - Thanks to @jbripley
|
||||
|
||||
|
||||
# 1.5.3 (2014-04-17)
|
||||
- Add support for async scripted dashboards (Issue #274)
|
||||
- Text panel now accepts html (for links to other dashboards, etc) (Issue #236)
|
||||
- Fix for Text panel, now changes take effect directly (Issue #251)
|
||||
- Fix when adding functions without params that did not cause graph to update (Issue #267)
|
||||
- Graphite errors are now much easier to see and troubleshoot with the new inspector (Issue #265)
|
||||
- Use influxdb aliases to distinguish between multiple columns (Issue #283)
|
||||
- Correction to ms axis formater, now formats days correctly. (Issue #189)
|
||||
- Css fix for Firefox and using top menu dropdowns in panel fullscren / edit mode (Issue #106)
|
||||
- Browser page title is now Grafana - {{dashboard title}} (Issue #294)
|
||||
- Disable auto refresh zooming in (every time you change to an absolute time range), refresh will be restored when you change time range back to relative (Issue #282)
|
||||
- Add support for async scripted dashboards ([Issue #274](https://github.com/grafana/grafana/issues/274))
|
||||
- Text panel now accepts html (for links to other dashboards, etc) ([Issue #236](https://github.com/grafana/grafana/issues/236))
|
||||
- Fix for Text panel, now changes take effect directly ([Issue #251](https://github.com/grafana/grafana/issues/251))
|
||||
- Fix when adding functions without params that did not cause graph to update ([Issue #267](https://github.com/grafana/grafana/issues/267))
|
||||
- Graphite errors are now much easier to see and troubleshoot with the new inspector ([Issue #265](https://github.com/grafana/grafana/issues/265))
|
||||
- Use influxdb aliases to distinguish between multiple columns ([Issue #283](https://github.com/grafana/grafana/issues/283))
|
||||
- Correction to ms axis formater, now formats days correctly. ([Issue #189](https://github.com/grafana/grafana/issues/189))
|
||||
- Css fix for Firefox and using top menu dropdowns in panel fullscren / edit mode ([Issue #106](https://github.com/grafana/grafana/issues/106))
|
||||
- Browser page title is now Grafana - {{dashboard title}} ([Issue #294](https://github.com/grafana/grafana/issues/294))
|
||||
- Disable auto refresh zooming in (every time you change to an absolute time range), refresh will be restored when you change time range back to relative ([Issue #282](https://github.com/grafana/grafana/issues/282))
|
||||
- More graphite functions
|
||||
|
||||
# 1.5.2 (2014-03-24)
|
||||
### New Features and improvements
|
||||
- Support for second optional params for functions like aliasByNode (Issue #167). Read the wiki on the [Function Editor](https://github.com/torkelo/grafana/wiki/Graphite-Function-Editor) for more info.
|
||||
- More functions added to InfluxDB query editor (Issue #218)
|
||||
- Filters can now be used inside other filters (templated segments) (Issue #128)
|
||||
- Support for second optional params for functions like aliasByNode ([Issue #167](https://github.com/grafana/grafana/issues/167)). Read the wiki on the [Function Editor](https://github.com/torkelo/grafana/wiki/Graphite-Function-Editor) for more info.
|
||||
- More functions added to InfluxDB query editor ([Issue #218](https://github.com/grafana/grafana/issues/218))
|
||||
- Filters can now be used inside other filters (templated segments) ([Issue #128](https://github.com/grafana/grafana/issues/128))
|
||||
- More graphite functions added
|
||||
|
||||
### Fixes
|
||||
- Float arguments now work for functions like scale (Issue #223)
|
||||
- Float arguments now work for functions like scale ([Issue #223](https://github.com/grafana/grafana/issues/223))
|
||||
- Fix for graphite function editor, the graph & target was not updated after adding a function and leaving default params as is #191
|
||||
|
||||
The zip files now contains a sub folder with project name and version prefix. (Issue #209)
|
||||
The zip files now contains a sub folder with project name and version prefix. ([Issue #209](https://github.com/grafana/grafana/issues/209))
|
||||
|
||||
# 1.5.1 (2014-03-10)
|
||||
### Fixes
|
||||
@@ -95,22 +316,22 @@ Read this for more info:
|
||||
|
||||
# 1.5.0 (2014-03-09)
|
||||
### New Features and improvements
|
||||
- New function editor [video demo](http://youtu.be/I90WHRwE1ZM) (Issue #178)
|
||||
- Links to function documentation from function editor (Issue #3)
|
||||
- Reorder functions (Issue #130)
|
||||
- New function editor [video demo](http://youtu.be/I90WHRwE1ZM) ([Issue #178](https://github.com/grafana/grafana/issues/178))
|
||||
- Links to function documentation from function editor ([Issue #3](https://github.com/grafana/grafana/issues/3))
|
||||
- Reorder functions ([Issue #130](https://github.com/grafana/grafana/issues/130))
|
||||
- [Initial support for InfluxDB](https://github.com/torkelo/grafana/wiki/InfluxDB) as metric datasource (#103), need feedback!
|
||||
- [Dashboard playlist](https://github.com/torkelo/grafana/wiki/Dashboard-playlist) (Issue #36)
|
||||
- When adding aliasByNode smartly set node number (Issue #175)
|
||||
- Support graphite identifiers with embedded colons (Issue #173)
|
||||
- Typeahead & autocomplete when adding new function (Issue #164)
|
||||
- [Dashboard playlist](https://github.com/torkelo/grafana/wiki/Dashboard-playlist) ([Issue #36](https://github.com/grafana/grafana/issues/36))
|
||||
- When adding aliasByNode smartly set node number ([Issue #175](https://github.com/grafana/grafana/issues/175))
|
||||
- Support graphite identifiers with embedded colons ([Issue #173](https://github.com/grafana/grafana/issues/173))
|
||||
- Typeahead & autocomplete when adding new function ([Issue #164](https://github.com/grafana/grafana/issues/164))
|
||||
- More graphite function definitions
|
||||
- Make "ms" axis format include hour, day, weeks, month and year (Issue #149)
|
||||
- Microsecond axis format (Issue #146)
|
||||
- Specify template paramaters in URL (Issue #123)
|
||||
- Make "ms" axis format include hour, day, weeks, month and year ([Issue #149](https://github.com/grafana/grafana/issues/149))
|
||||
- Microsecond axis format ([Issue #146](https://github.com/grafana/grafana/issues/146))
|
||||
- Specify template parameters in URL ([Issue #123](https://github.com/grafana/grafana/issues/123))
|
||||
|
||||
### Fixes
|
||||
- Basic Auth fix (Issue #152)
|
||||
- Fix to annotations with graphite source & null values (Issue #138)
|
||||
- Basic Auth fix ([Issue #152](https://github.com/grafana/grafana/issues/152))
|
||||
- Fix to annotations with graphite source & null values ([Issue #138](https://github.com/grafana/grafana/issues/138))
|
||||
|
||||
# 1.4.0 (2014-02-21)
|
||||
### New Features
|
||||
@@ -176,7 +397,7 @@ Read this for more info:
|
||||
Thanks to everyone who contributed fixes and provided feedback :+1:
|
||||
|
||||
# 1.0.4 (2014-01-24)
|
||||
- Fixes #28 - Relative time range caused 500 graphite error in some cases (thx rsommer for the fix)
|
||||
- [Issue #28](https://github.com/grafana/grafana/issues/28) - Relative time range caused 500 graphite error in some cases (thx rsommer for the fix)
|
||||
|
||||
# 1.0.3 (2014-01-23)
|
||||
- #9 Add Y-axis format for milliseconds
|
||||
@@ -184,7 +405,7 @@ Thanks to everyone who contributed fixes and provided feedback :+1:
|
||||
- #13 Relative time ranges now uses relative time ranges when issuing graphite query
|
||||
|
||||
# 1.0.2 (2014-01-21)
|
||||
- Fixes #12, should now work ok without ElasticSearch
|
||||
- [Issue #12](https://github.com/grafana/grafana/issues/12), should now work ok without ElasticSearch
|
||||
|
||||
# 1.0.1 (2014-01-21)
|
||||
- Resize fix
|
||||
|
||||
135
README.md
135
README.md
@@ -1,19 +1,22 @@
|
||||
[Grafana](http://grafana.org) [](https://travis-ci.org/grafana/grafana)
|
||||
=================
|
||||
A beautiful, easy to use and feature rich Graphite dashboard replacement and graph editor. Visit [grafana.org](http://grafana.org) for screenshots, videos and feature descriptions.
|
||||
[Grafana](http://grafana.org) [](https://travis-ci.org/grafana/grafana) [](https://coveralls.io/r/grafana/grafana) [](https://gitter.im/grafana/grafana?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
================
|
||||
[Website](http://grafana.org) |
|
||||
[Twitter](http://twitter.com/grafana) |
|
||||
[IRC](http://webchat.freenode.net/?channels=grafana) |
|
||||
[Email](mailto:contact@grafana.org)
|
||||
|
||||

|
||||
Grafana is An open source, feature rich metrics dashboard and graph editor for
|
||||
Graphite, InfluxDB & OpenTSDB.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
### Graphite Target Editor
|
||||
- Graphite target expression parser
|
||||
- Quickly add / edit / remove function ([video demo](http://youtu.be/I90WHRwE1ZM))
|
||||
- Function parameters can be easily changed
|
||||
- Quickly navigate graphite metric structure
|
||||
- Templating
|
||||
- Integrated links to function documentation
|
||||
- Rearrange function order
|
||||
- Native Graphite PNG render support
|
||||
- Feature rich query composer
|
||||
- Quickly add and edit functions & parameters
|
||||
- Templated queries
|
||||
- [See it in action](http://grafana.org/docs/features/graphite)
|
||||
|
||||
### Graphing
|
||||
- Fast rendering, even over large timespans.
|
||||
@@ -22,90 +25,62 @@ A beautiful, easy to use and feature rich Graphite dashboard replacement and gra
|
||||
- Bars, Lines, Points.
|
||||
- Smart Y-axis formating
|
||||
- Series toggles & color selector
|
||||
- Axis labels
|
||||
- Legend values, and formating options
|
||||
- Grid thresholds, axis labels
|
||||
- [Annotations] (https://github.com/grafana/grafana/wiki/Annotations)
|
||||
- [Annotations](http://grafana.org/docs/features/annotations)
|
||||
|
||||
### Dashboards
|
||||
- Create and edit dashboards
|
||||
- Drag and drop graphs to rearrange
|
||||
- Set column spans and row heights
|
||||
- Save & [search dashboards](https://github.com/grafana/grafana/wiki/Search-features)
|
||||
- Create, edit, save & search dashboards
|
||||
- Change column spans and row heights
|
||||
- Drag and drop panels to rearrange
|
||||
- Use InfluxDB or Elasticsearch as dashboard storage
|
||||
- Import & export dashboard (json file)
|
||||
- Import dashboard from Graphite
|
||||
- Templating
|
||||
- [Scripted dashboards](https://github.com/grafana/grafana/wiki/Scripted-dashboards) (generate from js script and url parameters)
|
||||
- Flexible [time range controls](https://github.com/grafana/grafana/wiki/Time-range-controls)
|
||||
- [Dashboard playlists](https://github.com/grafana/grafana/wiki/Dashboard-playlist)
|
||||
- [Scripted dashboards](http://grafana.org/docs/features/scripted_dashboards)
|
||||
- [Dashboard playlists](http://grafana.org/docs/features/playlist)
|
||||
- [Time range controls](http://grafana.org/docs/features/time_range)
|
||||
|
||||
### InfluxDB
|
||||
- [Use InfluxDB](https://github.com/grafana/grafana/wiki/InfluxDB) as metric datasource
|
||||
- Use InfluxDB as a metric data source, annotation source and for dashboard storage
|
||||
- Query editor with series and column typeahead, easy group by and function selection
|
||||
|
||||
# Requirements
|
||||
Grafana is very easy to install. It is a client side web app with no backend. Any webserver will do. Optionally you will need ElasticSearch if you want to be able to save and load dashboards quickly instead of json files or local storage.
|
||||
### OpenTSDB
|
||||
- Use as metric data source
|
||||
- Query editor with metric name typeahead and tag filtering
|
||||
|
||||
# Installation
|
||||
- Download and extract the [latest release](https://github.com/grafana/grafana/releases).
|
||||
- Rename `config.sample.js` to `config.js`, then change `graphiteUrl` and `elasticsearch` to point to the correct urls. The urls entered here must be reachable by your browser.
|
||||
- Point your browser to the installation.
|
||||
## Requirements
|
||||
There are no dependencies, Grafana is a client side application that runs in your browser. It only needs a time series store where it can fetch metrics. If you use InfluxDB Grafana can use it to store dashboards. If you use Graphite or OpenTSDB you can use Elasticsearch to store dashboards or just use json files stored on disk.
|
||||
|
||||
To run from master:
|
||||
- Clone this repository
|
||||
- Start a web server in src folder
|
||||
- Or create a optimized & minified build:
|
||||
- npm install (requires nodejs)
|
||||
- grunt build (requires grunt-cli)
|
||||
## Installation
|
||||
Head to [grafana.org](http://grafana.org) and [download](http://grafana.org/download/)
|
||||
the latest release.
|
||||
|
||||
If you use ansible for provisioning and deployment [ansible-grafana](https://github.com/bobrik/ansible-grafana) should get you started.
|
||||
Then follow the quick [setup & config guide](http://grafana.org/docs/). If you have any problems please
|
||||
read the [troubleshooting guide](http://grafana.org/docs/troubleshooting).
|
||||
|
||||
When you have Grafana up an running, read the [Getting started](https://github.com/grafana/grafana/wiki/Getting-started) guide for
|
||||
an introduction on how to use Grafana and/or watch [this video](https://www.youtube.com/watch?v=OUvJamHeMpw) for a guide in creating a new dashboard and for creating
|
||||
templated dashboards.
|
||||
## Documentation & Support
|
||||
Be sure to read the [getting started guide](http://grafana.org/docs/features/intro) and the other
|
||||
feature guides.
|
||||
|
||||
# Graphite server config
|
||||
If you haven't used an alternative dashboard for graphite before you need to enable cross-domain origin request. For Apache 2.x:
|
||||
```
|
||||
Header set Access-Control-Allow-Origin "*"
|
||||
Header set Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
Header set Access-Control-Allow-Headers "origin, authorization, accept"
|
||||
```
|
||||
Note that using "\*" leaves your graphite instance quite open so you might want to consider using "http://my.graphite-dom.ain" in place of "\*"
|
||||
## Run from master
|
||||
Grafana uses nodejs and grunt for asset management (css & javascript), unit test runner and javascript syntax verification.
|
||||
- clone repository
|
||||
- install nodejs
|
||||
- npm install (in project root)
|
||||
- npm install -g grunt-cli
|
||||
- grunt (runt default task that will generate css files)
|
||||
- grunt build (creates optimized & minified release)
|
||||
- grunt release (same as grunt build but will also create tar & zip package)
|
||||
- grunt test (executes jshint and unit tests)
|
||||
|
||||
If your Graphite web is proteced by basic authentication, you have to enable the HTTP verb OPTIONS, origin
|
||||
(no wildcards are allowed in this case) and add Access-Control-Allow-Credentials. This looks like the following for Apache:
|
||||
```
|
||||
Header set Access-Control-Allow-Origin "http://mygrafana.com:5656"
|
||||
Header set Access-Control-Allow-Credentials true
|
||||
## Contribute
|
||||
If you have any idea for an improvement or found a bug do not hesitate to open an issue.
|
||||
And if you have time clone this repo and submit a pull request and help me make Grafana
|
||||
the kickass metrics & devops dashboard we all dream about!
|
||||
|
||||
<Location />
|
||||
AuthName "graphs restricted"
|
||||
AuthType Basic
|
||||
AuthUserFile /etc/apache2/htpasswd
|
||||
<LimitExcept OPTIONS>
|
||||
require valid-user
|
||||
</LimitExcept>
|
||||
</Location>
|
||||
```
|
||||
Before creating a pull request be sure that "grunt test" runs without any style or unit test errors, also
|
||||
please [sign the CLA](http://grafana.org/docs/contributing/cla.html)
|
||||
|
||||
# Roadmap
|
||||
- Improve and refine the target parser and editing
|
||||
- Improve graphite import feature
|
||||
- Refine and simplify common tasks
|
||||
- More panel types (not just graphs)
|
||||
- Use elasticsearch to search for metrics
|
||||
- Improve template support
|
||||
- Annotate graph by querying ElasticSearch for events (or other event sources)
|
||||
|
||||
# Contribute
|
||||
If you have any idea for an improvement or found a bug do not hesitate to open an issue. And if you have time clone this repo and submit a pull request and help me make Grafana the kickass metrics & devops dashboard we all dream about!
|
||||
|
||||
Clone repository:
|
||||
- npm install
|
||||
- grunt server (starts development web server in src folder)
|
||||
- grunt (runs jshint and less -> css compilation)
|
||||
|
||||
# Notice
|
||||
This software is based on the great log dashboard [kibana](https://github.com/elasticsearch/kibana).
|
||||
|
||||
# License
|
||||
## License
|
||||
Grafana is distributed under Apache 2.0 License.
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"folders":
|
||||
[
|
||||
{
|
||||
"follow_symlinks": true,
|
||||
"path": ".",
|
||||
"folder_exclude_patterns": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
],
|
||||
"settings":
|
||||
{
|
||||
"tab_size": 2,
|
||||
"translate_tabs_to_spaces": true,
|
||||
"trim_trailing_white_space_on_save": true
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "1.5.4",
|
||||
"url": "http://grafanarel.s3.amazonaws.com/grafana-1.5.4.tar.gz"
|
||||
}
|
||||
"version": "1.9.1",
|
||||
"url": "http://grafanarel.s3.amazonaws.com/grafana-1.9.1.tar.gz"
|
||||
}
|
||||
|
||||
78
package.json
78
package.json
@@ -4,60 +4,66 @@
|
||||
"company": "Coding Instinct AB"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "1.6.0",
|
||||
"version": "1.9.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/torkelo/grafana.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rjs-build-analysis": "0.0.3",
|
||||
"grunt": "~0.4.0",
|
||||
"grunt-ngmin": "0.0.3",
|
||||
"grunt-contrib-less": "~0.7.0",
|
||||
"grunt-contrib-copy": "~0.4.1",
|
||||
"grunt-git-describe": "~2.3.2",
|
||||
"grunt-contrib-clean": "~0.5.0",
|
||||
"grunt-contrib-cssmin": "~0.6.1",
|
||||
"grunt-contrib-jshint": "~0.10.0",
|
||||
"grunt-string-replace": "~0.2.4",
|
||||
"grunt-contrib-htmlmin": "~0.1.3",
|
||||
"grunt-contrib-requirejs": "~0.4.1",
|
||||
"grunt-angular-templates": "^0.5.5",
|
||||
"grunt-contrib-compress": "~0.5.2",
|
||||
"grunt-contrib-uglify": "~0.2.4",
|
||||
"load-grunt-tasks": "~0.2.0",
|
||||
"glob": "~3.2.7",
|
||||
"grunt-contrib-connect": "~0.5.0",
|
||||
"mocha": "~1.16.1",
|
||||
"expect.js": "~0.2.0",
|
||||
"karma-script-launcher": "~0.1.0",
|
||||
"karma-firefox-launcher": "~0.1.3",
|
||||
"glob": "~3.2.7",
|
||||
"grunt": "~0.4.0",
|
||||
"grunt-angular-templates": "^0.5.5",
|
||||
"grunt-cli": "~0.1.13",
|
||||
"grunt-contrib-clean": "~0.5.0",
|
||||
"grunt-contrib-compress": "~0.5.2",
|
||||
"grunt-contrib-concat": "^0.4.0",
|
||||
"grunt-contrib-connect": "~0.5.0",
|
||||
"grunt-contrib-copy": "~0.5.0",
|
||||
"grunt-contrib-cssmin": "~0.6.1",
|
||||
"grunt-contrib-htmlmin": "~0.1.3",
|
||||
"grunt-contrib-jshint": "~0.10.0",
|
||||
"grunt-contrib-less": "~0.7.0",
|
||||
"grunt-contrib-requirejs": "~0.4.1",
|
||||
"grunt-contrib-uglify": "~0.2.4",
|
||||
"grunt-contrib-watch": "^0.6.1",
|
||||
"grunt-filerev": "^0.2.1",
|
||||
"grunt-git-describe": "~2.3.2",
|
||||
"grunt-karma": "~0.8.3",
|
||||
"grunt-ngmin": "0.0.3",
|
||||
"grunt-string-replace": "~0.2.4",
|
||||
"grunt-usemin": "^2.1.1",
|
||||
"jshint-stylish": "~0.1.5",
|
||||
"karma": "~0.12.21",
|
||||
"karma-chrome-launcher": "~0.1.4",
|
||||
"karma-coffee-preprocessor": "~0.1.2",
|
||||
"karma-coverage": "^0.2.5",
|
||||
"karma-coveralls": "^0.1.4",
|
||||
"karma-expect": "~1.1.0",
|
||||
"karma-firefox-launcher": "~0.1.3",
|
||||
"karma-html2js-preprocessor": "~0.1.0",
|
||||
"karma-jasmine": "~0.2.2",
|
||||
"requirejs": "~2.1.9",
|
||||
"karma-requirejs": "~0.2.1",
|
||||
"karma-coffee-preprocessor": "~0.1.2",
|
||||
"karma-phantomjs-launcher": "~0.1.1",
|
||||
"karma": "~0.12.16",
|
||||
"grunt-karma": "~0.8.3",
|
||||
"karma-mocha": "~0.1.4",
|
||||
"karma-expect": "~1.1.0",
|
||||
"grunt-cli": "~0.1.13",
|
||||
"jshint-stylish": "~0.1.5",
|
||||
"grunt-contrib-concat": "^0.4.0",
|
||||
"grunt-usemin": "^2.1.1",
|
||||
"grunt-filerev": "^0.2.1"
|
||||
"karma-phantomjs-launcher": "~0.1.1",
|
||||
"karma-requirejs": "~0.2.1",
|
||||
"karma-script-launcher": "~0.1.0",
|
||||
"load-grunt-tasks": "~0.2.0",
|
||||
"mocha": "~1.16.1",
|
||||
"requirejs": "~2.1.14",
|
||||
"rjs-build-analysis": "0.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "0.10.x",
|
||||
"npm": "1.2.x"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "grunt test"
|
||||
"test": "grunt test",
|
||||
"coveralls": "grunt karma:coveralls && rm -rf ./coverage"
|
||||
},
|
||||
"license": "Apache License",
|
||||
"dependencies": {
|
||||
"grunt-jscs-checker": "^0.4.4"
|
||||
"grunt-jscs-checker": "^0.4.4",
|
||||
"karma-sinon": "^1.0.3",
|
||||
"sinon": "^1.10.3"
|
||||
}
|
||||
}
|
||||
|
||||
55
sample/start_dashboard_html.html
Normal file
55
sample/start_dashboard_html.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<br/>
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span6">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="http://grafana.org/docs#configuration" target="_blank">Configuration</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="http://grafana.org/docs/troubleshooting" target="_blank">Troubleshooting</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="http://grafana.org/docs/support" target="_blank">Support</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="http://grafana.org/docs/features/intro" target="_blank">Getting started</a> (Must read!)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="span6">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="http://grafana.org/docs/features/graphing" target="_blank">Graphing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="http://grafana.org/docs/features/annotations" target="_blank">Annotations</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="http://grafana.org/docs/features/graphite" target="_blank">Graphite</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="http://grafana.org/docs/features/influxdb" target="_blank">InfluxDB</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="http://grafana.org/docs/features/opentsdb" target="_blank">OpenTSDB</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
<ul>
|
||||
<li>Ctrl+S saves the current dashboard</li>
|
||||
<li>Ctrl+F Opens the dashboard finder</li>
|
||||
<li>Ctrl+H Hide/show row controls</li>
|
||||
<li>Click and drag graph title to move panel</li>
|
||||
<li>Hit Escape to exit graph when in fullscreen or edit mode</li>
|
||||
<li>Click the colored icon in the legend to change series color</li>
|
||||
<li>Ctrl or Shift + Click legend name to hide other series</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
127
src/app/app.js
127
src/app/app.js
@@ -4,21 +4,22 @@
|
||||
define([
|
||||
'angular',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'lodash',
|
||||
'require',
|
||||
'elasticjs',
|
||||
'config',
|
||||
'bootstrap',
|
||||
'angular-route',
|
||||
'angular-sanitize',
|
||||
'angular-strap',
|
||||
'angular-dragdrop',
|
||||
'extend-jquery',
|
||||
'bindonce'
|
||||
'bindonce',
|
||||
],
|
||||
function (angular, $, _, appLevelRequire) {
|
||||
function (angular, $, _, appLevelRequire, config) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var app = angular.module('kibana', []),
|
||||
var app = angular.module('grafana', []),
|
||||
// we will keep a reference to each module defined before boot, so that we can
|
||||
// go back and allow it to define new features later. Once we boot, this will be false
|
||||
pre_boot_modules = [],
|
||||
@@ -48,39 +49,9 @@ function (angular, $, _, appLevelRequire) {
|
||||
return module;
|
||||
};
|
||||
|
||||
app.safeApply = function ($scope, fn) {
|
||||
switch($scope.$$phase) {
|
||||
case '$apply':
|
||||
// $digest hasn't started, we should be good
|
||||
$scope.$eval(fn);
|
||||
break;
|
||||
case '$digest':
|
||||
// waiting to $apply the changes
|
||||
setTimeout(function () { app.safeApply($scope, fn); }, 10);
|
||||
break;
|
||||
default:
|
||||
// clear to begin an $apply $$phase
|
||||
$scope.$apply(fn);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
app.config(function ($routeProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) {
|
||||
|
||||
$routeProvider
|
||||
.when('/dashboard', {
|
||||
templateUrl: 'app/partials/dashboard.html',
|
||||
})
|
||||
.when('/dashboard/:kbnType/:kbnId', {
|
||||
templateUrl: 'app/partials/dashboard.html',
|
||||
})
|
||||
.when('/dashboard/:kbnType/:kbnId/:params', {
|
||||
templateUrl: 'app/partials/dashboard.html'
|
||||
})
|
||||
.otherwise({
|
||||
redirectTo: 'dashboard'
|
||||
});
|
||||
|
||||
$routeProvider.otherwise({ redirectTo: config.default_route });
|
||||
// this is how the internet told me to dynamically add modules :/
|
||||
register_fns.controller = $controllerProvider.register;
|
||||
register_fns.directive = $compileProvider.directive;
|
||||
@@ -90,60 +61,72 @@ function (angular, $, _, appLevelRequire) {
|
||||
});
|
||||
|
||||
var apps_deps = [
|
||||
'elasticjs.service',
|
||||
'$strap.directives',
|
||||
'ngRoute',
|
||||
'ngSanitize',
|
||||
'ngDragDrop',
|
||||
'kibana',
|
||||
'$strap.directives',
|
||||
'ang-drag-drop',
|
||||
'grafana',
|
||||
'pasvaz.bindonce'
|
||||
];
|
||||
|
||||
var module_types = ['controllers', 'directives', 'factories', 'services', 'services.dashboard', 'filters'];
|
||||
var module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];
|
||||
|
||||
_.each(module_types, function (type) {
|
||||
var module_name = 'kibana.'+type;
|
||||
var module_name = 'grafana.'+type;
|
||||
// create the module
|
||||
app.useModule(angular.module(module_name, []));
|
||||
// push it into the apps dependencies
|
||||
apps_deps.push(module_name);
|
||||
});
|
||||
|
||||
// load the core components
|
||||
require([
|
||||
var preBootRequires = [
|
||||
'services/all',
|
||||
'features/all',
|
||||
'controllers/all',
|
||||
'directives/all',
|
||||
'filters/all',
|
||||
'components/partials',
|
||||
], function () {
|
||||
'routes/all',
|
||||
];
|
||||
|
||||
// bootstrap the app
|
||||
angular
|
||||
.element(document)
|
||||
.ready(function() {
|
||||
$('body').attr('ng-controller', 'DashCtrl');
|
||||
angular.bootstrap(document, apps_deps)
|
||||
.invoke(['$rootScope', function ($rootScope) {
|
||||
_.each(pre_boot_modules, function (module) {
|
||||
_.extend(module, register_fns);
|
||||
});
|
||||
pre_boot_modules = false;
|
||||
|
||||
$rootScope.requireContext = appLevelRequire;
|
||||
$rootScope.require = function (deps, fn) {
|
||||
var $scope = this;
|
||||
$scope.requireContext(deps, function () {
|
||||
var deps = _.toArray(arguments);
|
||||
// Check that this is a valid scope.
|
||||
if($scope.$id) {
|
||||
$scope.$apply(function () {
|
||||
fn.apply($scope, deps);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}]);
|
||||
});
|
||||
_.each(config.plugins.dependencies, function(dep) {
|
||||
preBootRequires.push('../plugins/' + dep);
|
||||
});
|
||||
|
||||
app.boot = function() {
|
||||
require(preBootRequires, function () {
|
||||
|
||||
// disable tool tip animation
|
||||
$.fn.tooltip.defaults.animation = false;
|
||||
|
||||
// bootstrap the app
|
||||
angular
|
||||
.element(document)
|
||||
.ready(function() {
|
||||
angular.bootstrap(document, apps_deps)
|
||||
.invoke(['$rootScope', function ($rootScope) {
|
||||
_.each(pre_boot_modules, function (module) {
|
||||
_.extend(module, register_fns);
|
||||
});
|
||||
pre_boot_modules = false;
|
||||
|
||||
$rootScope.requireContext = appLevelRequire;
|
||||
$rootScope.require = function (deps, fn) {
|
||||
var $scope = this;
|
||||
$scope.requireContext(deps, function () {
|
||||
var deps = _.toArray(arguments);
|
||||
// Check that this is a valid scope.
|
||||
if($scope.$id) {
|
||||
$scope.$apply(function () {
|
||||
fn.apply($scope, deps);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return app;
|
||||
});
|
||||
|
||||
@@ -10,18 +10,6 @@ function ($) {
|
||||
$.fn.place_tt = (function () {
|
||||
var defaults = {
|
||||
offset: 5,
|
||||
css: {
|
||||
position : 'absolute',
|
||||
top : -1000,
|
||||
left : 0,
|
||||
color : "#c8c8c8",
|
||||
padding : '10px',
|
||||
'font-size': '11pt',
|
||||
'font-weight' : 200,
|
||||
'background-color': '#1f1f1f',
|
||||
'border-radius': '5px',
|
||||
'z-index': 9999
|
||||
}
|
||||
};
|
||||
|
||||
return function (x, y, opts) {
|
||||
@@ -29,10 +17,10 @@ function ($) {
|
||||
return this.each(function () {
|
||||
var $tooltip = $(this), width, height;
|
||||
|
||||
$tooltip.css(opts.css);
|
||||
if (!$.contains(document.body, $tooltip[0])) {
|
||||
$tooltip.appendTo(document.body);
|
||||
}
|
||||
$tooltip.addClass('grafana-tooltip');
|
||||
|
||||
$("#tooltip").remove();
|
||||
$tooltip.appendTo(document.body);
|
||||
|
||||
width = $tooltip.outerWidth(true);
|
||||
height = $tooltip.outerHeight(true);
|
||||
@@ -44,4 +32,4 @@ function ($) {
|
||||
})();
|
||||
|
||||
return $;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,27 +1,13 @@
|
||||
define(['jquery','underscore','moment'],
|
||||
define([
|
||||
'jquery',
|
||||
'lodash',
|
||||
'moment'
|
||||
],
|
||||
function($, _, moment) {
|
||||
'use strict';
|
||||
|
||||
var kbn = {};
|
||||
|
||||
/**
|
||||
* Calculate a graph interval
|
||||
*
|
||||
* from:: Date object containing the start time
|
||||
* to:: Date object containing the finish time
|
||||
* size:: Calculate to approximately this many bars
|
||||
* user_interval:: User specified histogram interval
|
||||
*
|
||||
*/
|
||||
kbn.calculate_interval = function(from,to,size,user_interval) {
|
||||
if(_.isObject(from)) {
|
||||
from = from.valueOf();
|
||||
}
|
||||
if(_.isObject(to)) {
|
||||
to = to.valueOf();
|
||||
}
|
||||
return user_interval === 0 ? kbn.round_interval((to - from)/size) : user_interval;
|
||||
};
|
||||
kbn.valueFormats = {};
|
||||
|
||||
kbn.round_interval = function(interval) {
|
||||
switch (true) {
|
||||
@@ -127,6 +113,28 @@ function($, _, moment) {
|
||||
s: 1
|
||||
};
|
||||
|
||||
kbn.calculateInterval = function(range, resolution, userInterval) {
|
||||
var lowLimitMs = 1; // 1 millisecond default low limit
|
||||
var intervalMs, lowLimitInterval;
|
||||
|
||||
if (userInterval) {
|
||||
if (userInterval[0] === '>') {
|
||||
lowLimitInterval = userInterval.slice(1);
|
||||
lowLimitMs = kbn.interval_to_ms(lowLimitInterval);
|
||||
}
|
||||
else {
|
||||
return userInterval;
|
||||
}
|
||||
}
|
||||
|
||||
intervalMs = kbn.round_interval((range.to.valueOf() - range.from.valueOf()) / resolution);
|
||||
if (lowLimitMs > intervalMs) {
|
||||
intervalMs = lowLimitMs;
|
||||
}
|
||||
|
||||
return kbn.secondsToHms(intervalMs / 1000);
|
||||
};
|
||||
|
||||
kbn.describe_interval = function (string) {
|
||||
var matches = string.match(kbn.interval_regex);
|
||||
if (!matches || !_.has(kbn.intervals_in_seconds, matches[2])) {
|
||||
@@ -227,36 +235,36 @@ function($, _, moment) {
|
||||
if (type === 0) {
|
||||
roundUp ? dateTime.endOf('year') : dateTime.startOf('year');
|
||||
} else if (type === 1) {
|
||||
dateTime.add('years',num);
|
||||
dateTime.add(num, 'years');
|
||||
} else if (type === 2) {
|
||||
dateTime.subtract('years',num);
|
||||
dateTime.subtract(num, 'years');
|
||||
}
|
||||
break;
|
||||
case 'M':
|
||||
if (type === 0) {
|
||||
roundUp ? dateTime.endOf('month') : dateTime.startOf('month');
|
||||
} else if (type === 1) {
|
||||
dateTime.add('months',num);
|
||||
dateTime.add(num, 'months');
|
||||
} else if (type === 2) {
|
||||
dateTime.subtract('months',num);
|
||||
dateTime.subtract(num, 'months');
|
||||
}
|
||||
break;
|
||||
case 'w':
|
||||
if (type === 0) {
|
||||
roundUp ? dateTime.endOf('week') : dateTime.startOf('week');
|
||||
} else if (type === 1) {
|
||||
dateTime.add('weeks',num);
|
||||
dateTime.add(num, 'weeks');
|
||||
} else if (type === 2) {
|
||||
dateTime.subtract('weeks',num);
|
||||
dateTime.subtract(num, 'weeks');
|
||||
}
|
||||
break;
|
||||
case 'd':
|
||||
if (type === 0) {
|
||||
roundUp ? dateTime.endOf('day') : dateTime.startOf('day');
|
||||
} else if (type === 1) {
|
||||
dateTime.add('days',num);
|
||||
dateTime.add(num, 'days');
|
||||
} else if (type === 2) {
|
||||
dateTime.subtract('days',num);
|
||||
dateTime.subtract(num, 'days');
|
||||
}
|
||||
break;
|
||||
case 'h':
|
||||
@@ -264,27 +272,27 @@ function($, _, moment) {
|
||||
if (type === 0) {
|
||||
roundUp ? dateTime.endOf('hour') : dateTime.startOf('hour');
|
||||
} else if (type === 1) {
|
||||
dateTime.add('hours',num);
|
||||
dateTime.add(num, 'hours');
|
||||
} else if (type === 2) {
|
||||
dateTime.subtract('hours',num);
|
||||
dateTime.subtract(num,'hours');
|
||||
}
|
||||
break;
|
||||
case 'm':
|
||||
if (type === 0) {
|
||||
roundUp ? dateTime.endOf('minute') : dateTime.startOf('minute');
|
||||
} else if (type === 1) {
|
||||
dateTime.add('minutes',num);
|
||||
dateTime.add(num, 'minutes');
|
||||
} else if (type === 2) {
|
||||
dateTime.subtract('minutes',num);
|
||||
dateTime.subtract(num, 'minutes');
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
if (type === 0) {
|
||||
roundUp ? dateTime.endOf('second') : dateTime.startOf('second');
|
||||
} else if (type === 1) {
|
||||
dateTime.add('seconds',num);
|
||||
dateTime.add(num, 'seconds');
|
||||
} else if (type === 2) {
|
||||
dateTime.subtract('seconds',num);
|
||||
dateTime.subtract(num, 'seconds');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -302,262 +310,174 @@ function($, _, moment) {
|
||||
].join(';') + '"></div>';
|
||||
};
|
||||
|
||||
kbn.byteFormat = function(size, decimals) {
|
||||
var ext, steps = 0;
|
||||
|
||||
if(_.isUndefined(decimals)) {
|
||||
decimals = 2;
|
||||
} else if (decimals === 0) {
|
||||
decimals = undefined;
|
||||
}
|
||||
|
||||
while (Math.abs(size) >= 1024) {
|
||||
steps++;
|
||||
size /= 1024;
|
||||
}
|
||||
|
||||
switch (steps) {
|
||||
case 0:
|
||||
ext = " B";
|
||||
break;
|
||||
case 1:
|
||||
ext = " KiB";
|
||||
break;
|
||||
case 2:
|
||||
ext = " MiB";
|
||||
break;
|
||||
case 3:
|
||||
ext = " GiB";
|
||||
break;
|
||||
case 4:
|
||||
ext = " TiB";
|
||||
break;
|
||||
case 5:
|
||||
ext = " PiB";
|
||||
break;
|
||||
case 6:
|
||||
ext = " EiB";
|
||||
break;
|
||||
case 7:
|
||||
ext = " ZiB";
|
||||
break;
|
||||
case 8:
|
||||
ext = " YiB";
|
||||
break;
|
||||
}
|
||||
|
||||
return (size.toFixed(decimals) + ext);
|
||||
kbn.valueFormats.percent = function(size, decimals) {
|
||||
return kbn.toFixed(size, decimals) + '%';
|
||||
};
|
||||
|
||||
kbn.bitFormat = function(size, decimals) {
|
||||
var ext, steps = 0;
|
||||
kbn.formatFuncCreator = function(factor, extArray) {
|
||||
return function(size, decimals, scaledDecimals) {
|
||||
if (size === null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if(_.isUndefined(decimals)) {
|
||||
decimals = 2;
|
||||
} else if (decimals === 0) {
|
||||
decimals = undefined;
|
||||
}
|
||||
var steps = 0;
|
||||
|
||||
while (Math.abs(size) >= 1024) {
|
||||
steps++;
|
||||
size /= 1024;
|
||||
}
|
||||
while (Math.abs(size) >= factor) {
|
||||
steps++;
|
||||
size /= factor;
|
||||
}
|
||||
if (steps > 0) {
|
||||
decimals = scaledDecimals + (3 * steps);
|
||||
}
|
||||
|
||||
switch (steps) {
|
||||
case 0:
|
||||
ext = " b";
|
||||
break;
|
||||
case 1:
|
||||
ext = " Kib";
|
||||
break;
|
||||
case 2:
|
||||
ext = " Mib";
|
||||
break;
|
||||
case 3:
|
||||
ext = " Gib";
|
||||
break;
|
||||
case 4:
|
||||
ext = " Tib";
|
||||
break;
|
||||
case 5:
|
||||
ext = " Pib";
|
||||
break;
|
||||
case 6:
|
||||
ext = " Eib";
|
||||
break;
|
||||
case 7:
|
||||
ext = " Zib";
|
||||
break;
|
||||
case 8:
|
||||
ext = " Yib";
|
||||
break;
|
||||
}
|
||||
|
||||
return (size.toFixed(decimals) + ext);
|
||||
return kbn.toFixed(size, decimals) + extArray[steps];
|
||||
};
|
||||
};
|
||||
|
||||
kbn.shortFormat = function(size, decimals) {
|
||||
var ext, steps = 0;
|
||||
|
||||
if(_.isUndefined(decimals)) {
|
||||
decimals = 2;
|
||||
} else if (decimals === 0) {
|
||||
decimals = undefined;
|
||||
kbn.toFixed = function(value, decimals) {
|
||||
if (value === null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
while (Math.abs(size) >= 1000) {
|
||||
steps++;
|
||||
size /= 1000;
|
||||
var factor = decimals ? Math.pow(10, Math.max(0, decimals)) : 1;
|
||||
var formatted = String(Math.round(value * factor) / factor);
|
||||
|
||||
// if exponent return directly
|
||||
if (formatted.indexOf('e') !== -1 || value === 0) {
|
||||
return formatted;
|
||||
}
|
||||
|
||||
switch (steps) {
|
||||
case 0:
|
||||
ext = "";
|
||||
break;
|
||||
case 1:
|
||||
ext = " K";
|
||||
break;
|
||||
case 2:
|
||||
ext = " Mil";
|
||||
break;
|
||||
case 3:
|
||||
ext = " Bil";
|
||||
break;
|
||||
case 4:
|
||||
ext = " Tri";
|
||||
break;
|
||||
case 5:
|
||||
ext = " Quadr";
|
||||
break;
|
||||
case 6:
|
||||
ext = " Quint";
|
||||
break;
|
||||
case 7:
|
||||
ext = " Sext";
|
||||
break;
|
||||
case 8:
|
||||
ext = " Sept";
|
||||
break;
|
||||
// If tickDecimals was specified, ensure that we have exactly that
|
||||
// much precision; otherwise default to the value's own precision.
|
||||
if (decimals != null) {
|
||||
var decimalPos = formatted.indexOf(".");
|
||||
var precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1;
|
||||
if (precision < decimals) {
|
||||
return (precision ? formatted : formatted + ".") + (String(factor)).substr(1, decimals - precision);
|
||||
}
|
||||
}
|
||||
|
||||
return (size.toFixed(decimals) + ext);
|
||||
return formatted;
|
||||
};
|
||||
|
||||
kbn.getFormatFunction = function(formatName, decimals) {
|
||||
switch(formatName) {
|
||||
case 'short':
|
||||
return function(val) {
|
||||
return kbn.shortFormat(val, decimals);
|
||||
};
|
||||
case 'bytes':
|
||||
return function(val) {
|
||||
return kbn.byteFormat(val, decimals);
|
||||
};
|
||||
case 'bits':
|
||||
return function(val) {
|
||||
return kbn.bitFormat(val, decimals);
|
||||
};
|
||||
case 's':
|
||||
return function(val) {
|
||||
return kbn.sFormat(val, decimals);
|
||||
};
|
||||
case 'ms':
|
||||
return function(val) {
|
||||
return kbn.msFormat(val, decimals);
|
||||
};
|
||||
case 'µs':
|
||||
return function(val) {
|
||||
return kbn.microsFormat(val, decimals);
|
||||
};
|
||||
case 'ns':
|
||||
return function(val) {
|
||||
return kbn.nanosFormat(val, decimals);
|
||||
};
|
||||
default:
|
||||
return function(val) {
|
||||
return val % 1 === 0 ? val : val.toFixed(decimals);
|
||||
};
|
||||
}
|
||||
};
|
||||
kbn.valueFormats.bits = kbn.formatFuncCreator(1024, [' b', ' Kib', ' Mib', ' Gib', ' Tib', ' Pib', ' Eib', ' Zib', ' Yib']);
|
||||
kbn.valueFormats.bytes = kbn.formatFuncCreator(1024, [' B', ' KiB', ' MiB', ' GiB', ' TiB', ' PiB', ' EiB', ' ZiB', ' YiB']);
|
||||
kbn.valueFormats.bps = kbn.formatFuncCreator(1000, [' bps', ' Kbps', ' Mbps', ' Gbps', ' Tbps', ' Pbps', ' Ebps', ' Zbps', ' Ybps']);
|
||||
kbn.valueFormats.short = kbn.formatFuncCreator(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Qaudr', ' Quint', ' Sext', ' Sept']);
|
||||
kbn.valueFormats.none = kbn.toFixed;
|
||||
|
||||
kbn.msFormat = function(size, decimals) {
|
||||
if (size < 1000) {
|
||||
return size.toFixed(0) + " ms";
|
||||
kbn.valueFormats.ms = function(size, decimals, scaledDecimals) {
|
||||
if (size === null) { return ""; }
|
||||
|
||||
if (Math.abs(size) < 1000) {
|
||||
return kbn.toFixed(size, decimals) + " ms";
|
||||
}
|
||||
// Less than 1 min
|
||||
else if (size < 60000) {
|
||||
return (size / 1000).toFixed(decimals) + " s";
|
||||
else if (Math.abs(size) < 60000) {
|
||||
return kbn.toFixed(size / 1000, scaledDecimals + 3) + " s";
|
||||
}
|
||||
// Less than 1 hour, devide in minutes
|
||||
else if (size < 3600000) {
|
||||
return (size / 60000).toFixed(decimals) + " min";
|
||||
else if (Math.abs(size) < 3600000) {
|
||||
return kbn.toFixed(size / 60000, scaledDecimals + 5) + " min";
|
||||
}
|
||||
// Less than one day, devide in hours
|
||||
else if (size < 86400000) {
|
||||
return (size / 3600000).toFixed(decimals) + " hour";
|
||||
else if (Math.abs(size) < 86400000) {
|
||||
return kbn.toFixed(size / 3600000, scaledDecimals + 7) + " hour";
|
||||
}
|
||||
// Less than one year, devide in days
|
||||
else if (size < 31536000000) {
|
||||
return (size / 86400000).toFixed(decimals) + " day";
|
||||
else if (Math.abs(size) < 31536000000) {
|
||||
return kbn.toFixed(size / 86400000, scaledDecimals + 8) + " day";
|
||||
}
|
||||
|
||||
return (size / 31536000000).toFixed(decimals) + " year";
|
||||
return kbn.toFixed(size / 31536000000, scaledDecimals + 10) + " year";
|
||||
};
|
||||
|
||||
kbn.sFormat = function(size, decimals) {
|
||||
// Less than 10 min, use seconds
|
||||
if (size < 600) {
|
||||
return size.toFixed(decimals) + " s";
|
||||
kbn.valueFormats.s = function(size, decimals, scaledDecimals) {
|
||||
if (size === null) { return ""; }
|
||||
|
||||
if (Math.abs(size) < 600) {
|
||||
return kbn.toFixed(size, decimals) + " s";
|
||||
}
|
||||
// Less than 1 hour, devide in minutes
|
||||
else if (size < 3600) {
|
||||
return (size / 60).toFixed(decimals) + " min";
|
||||
else if (Math.abs(size) < 3600) {
|
||||
return kbn.toFixed(size / 60, scaledDecimals + 1) + " min";
|
||||
}
|
||||
// Less than one day, devide in hours
|
||||
else if (size < 86400) {
|
||||
return (size / 3600).toFixed(decimals) + " hour";
|
||||
else if (Math.abs(size) < 86400) {
|
||||
return kbn.toFixed(size / 3600, scaledDecimals + 4) + " hour";
|
||||
}
|
||||
// Less than one week, devide in days
|
||||
else if (size < 604800) {
|
||||
return (size / 86400).toFixed(decimals) + " day";
|
||||
else if (Math.abs(size) < 604800) {
|
||||
return kbn.toFixed(size / 86400, scaledDecimals + 5) + " day";
|
||||
}
|
||||
// Less than one year, devide in week
|
||||
else if (size < 31536000) {
|
||||
return (size / 604800).toFixed(decimals) + " week";
|
||||
else if (Math.abs(size) < 31536000) {
|
||||
return kbn.toFixed(size / 604800, scaledDecimals + 6) + " week";
|
||||
}
|
||||
|
||||
return (size / 3.15569e7).toFixed(decimals) + " year";
|
||||
return kbn.toFixed(size / 3.15569e7, scaledDecimals + 7) + " year";
|
||||
};
|
||||
|
||||
kbn.microsFormat = function(size, decimals) {
|
||||
if (size < 1000) {
|
||||
return size.toFixed(0) + " µs";
|
||||
kbn.valueFormats['µs'] = function(size, decimals, scaledDecimals) {
|
||||
if (size === null) { return ""; }
|
||||
|
||||
if (Math.abs(size) < 1000) {
|
||||
return kbn.toFixed(size, decimals) + " µs";
|
||||
}
|
||||
else if (size < 1000000) {
|
||||
return (size / 1000).toFixed(decimals) + " ms";
|
||||
else if (Math.abs(size) < 1000000) {
|
||||
return kbn.toFixed(size / 1000, scaledDecimals + 3) + " ms";
|
||||
}
|
||||
else {
|
||||
return (size / 1000000).toFixed(decimals) + " s";
|
||||
return kbn.toFixed(size / 1000000, scaledDecimals + 6) + " s";
|
||||
}
|
||||
};
|
||||
|
||||
kbn.nanosFormat = function(size, decimals) {
|
||||
if (size < 1000) {
|
||||
return size.toFixed(0) + " ns";
|
||||
kbn.valueFormats.ns = function(size, decimals, scaledDecimals) {
|
||||
if (size === null) { return ""; }
|
||||
|
||||
if (Math.abs(size) < 1000) {
|
||||
return kbn.toFixed(size, decimals) + " ns";
|
||||
}
|
||||
else if (size < 1000000) {
|
||||
return (size / 1000).toFixed(decimals) + " µs";
|
||||
else if (Math.abs(size) < 1000000) {
|
||||
return kbn.toFixed(size / 1000, scaledDecimals + 3) + " µs";
|
||||
}
|
||||
else if (size < 1000000000) {
|
||||
return (size / 1000000).toFixed(decimals) + " ms";
|
||||
else if (Math.abs(size) < 1000000000) {
|
||||
return kbn.toFixed(size / 1000000, scaledDecimals + 6) + " ms";
|
||||
}
|
||||
else if (size < 60000000000){
|
||||
return (size / 1000000000).toFixed(decimals) + " s";
|
||||
else if (Math.abs(size) < 60000000000){
|
||||
return kbn.toFixed(size / 1000000000, scaledDecimals + 9) + " s";
|
||||
}
|
||||
else {
|
||||
return (size / 60000000000).toFixed(decimals) + " m";
|
||||
return kbn.toFixed(size / 60000000000, scaledDecimals + 12) + " m";
|
||||
}
|
||||
};
|
||||
|
||||
kbn.slugifyForUrl = function(str) {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.replace(/[^\w ]+/g,'')
|
||||
.replace(/ +/g,'-');
|
||||
};
|
||||
|
||||
kbn.exportSeriesListToCsv = function(seriesList) {
|
||||
var text = 'Series;Time;Value\n';
|
||||
_.each(seriesList, function(series) {
|
||||
_.each(series.datapoints, function(dp) {
|
||||
text += series.alias + ';' + new Date(dp[1]).toISOString() + ';' + dp[0] + '\n';
|
||||
});
|
||||
});
|
||||
var blob = new Blob([text], { type: "text/csv;charset=utf-8" });
|
||||
window.saveAs(blob, 'grafana_data_export.csv');
|
||||
};
|
||||
|
||||
kbn.stringToJsRegex = function(str) {
|
||||
if (str[0] !== '/') {
|
||||
return new RegExp(str);
|
||||
}
|
||||
|
||||
var match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$'));
|
||||
return new RegExp(match[1], match[2]);
|
||||
};
|
||||
|
||||
return kbn;
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
define([
|
||||
'underscore-src'
|
||||
'lodash-src'
|
||||
],
|
||||
function () {
|
||||
'use strict';
|
||||
@@ -33,4 +33,4 @@ function () {
|
||||
});
|
||||
|
||||
return _;
|
||||
});
|
||||
});
|
||||
44
src/app/components/panelmeta.js
Normal file
44
src/app/components/panelmeta.js
Normal file
@@ -0,0 +1,44 @@
|
||||
define([
|
||||
],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
function PanelMeta(options) {
|
||||
this.description = options.description;
|
||||
this.fullscreen = options.fullscreen;
|
||||
this.menu = [];
|
||||
this.editorTabs = [];
|
||||
this.extendedMenu = [];
|
||||
|
||||
if (options.fullscreen) {
|
||||
this.addMenuItem('view', 'icon-eye-open', 'toggleFullscreen(false); dismiss();');
|
||||
}
|
||||
|
||||
this.addMenuItem('edit', 'icon-cog', 'editPanel(); dismiss();');
|
||||
this.addMenuItem('duplicate', 'icon-copy', 'duplicatePanel()');
|
||||
this.addMenuItem('share', 'icon-share', 'sharePanel(); dismiss();');
|
||||
|
||||
this.addEditorTab('General', 'app/partials/panelgeneral.html');
|
||||
|
||||
if (options.metricsEditor) {
|
||||
this.addEditorTab('Metrics', 'app/partials/metrics.html');
|
||||
}
|
||||
|
||||
this.addExtendedMenuItem('Panel JSON', '', 'editPanelJson(); dismiss();');
|
||||
}
|
||||
|
||||
PanelMeta.prototype.addMenuItem = function(text, icon, click) {
|
||||
this.menu.push({text: text, icon: icon, click: click});
|
||||
};
|
||||
|
||||
PanelMeta.prototype.addExtendedMenuItem = function(text, icon, click) {
|
||||
this.extendedMenu.push({text: text, icon: icon, click: click});
|
||||
};
|
||||
|
||||
PanelMeta.prototype.addEditorTab = function(title, src) {
|
||||
this.editorTabs.push({title: title, src: src});
|
||||
};
|
||||
|
||||
return PanelMeta;
|
||||
|
||||
});
|
||||
@@ -3,32 +3,34 @@
|
||||
*/
|
||||
require.config({
|
||||
baseUrl: 'app',
|
||||
urlArgs: 'bust=' + (new Date().getTime()),
|
||||
|
||||
paths: {
|
||||
config: ['../config', '../config.sample'],
|
||||
settings: 'components/settings',
|
||||
kbn: 'components/kbn',
|
||||
store: 'components/store',
|
||||
|
||||
css: '../vendor/require/css',
|
||||
text: '../vendor/require/text',
|
||||
moment: '../vendor/moment',
|
||||
filesaver: '../vendor/filesaver',
|
||||
angular: '../vendor/angular/angular',
|
||||
'angular-route': '../vendor/angular/angular-route',
|
||||
'angular-sanitize': '../vendor/angular/angular-sanitize',
|
||||
'angular-dragdrop': '../vendor/angular/angular-dragdrop',
|
||||
'angular-strap': '../vendor/angular/angular-strap',
|
||||
'angular-sanitize': '../vendor/angular/angular-sanitize',
|
||||
timepicker: '../vendor/angular/timepicker',
|
||||
datepicker: '../vendor/angular/datepicker',
|
||||
bindonce: '../vendor/angular/bindonce',
|
||||
crypto: '../vendor/crypto.min',
|
||||
spectrum: '../vendor/spectrum',
|
||||
|
||||
underscore: 'components/underscore.extended',
|
||||
'underscore-src': '../vendor/underscore',
|
||||
lodash: 'components/lodash.extended',
|
||||
'lodash-src': '../vendor/lodash',
|
||||
bootstrap: '../vendor/bootstrap/bootstrap',
|
||||
|
||||
jquery: '../vendor/jquery/jquery-1.8.0',
|
||||
'jquery-ui': '../vendor/jquery/jquery-ui-1.10.3',
|
||||
jquery: '../vendor/jquery/jquery-2.1.1.min',
|
||||
|
||||
'extend-jquery': 'components/extend-jquery',
|
||||
|
||||
@@ -39,18 +41,15 @@ require.config({
|
||||
'jquery.flot.stack': '../vendor/jquery/jquery.flot.stack',
|
||||
'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
|
||||
'jquery.flot.time': '../vendor/jquery/jquery.flot.time',
|
||||
'jquery.flot.byte': '../vendor/jquery/jquery.flot.byte',
|
||||
'jquery.flot.crosshair': '../vendor/jquery/jquery.flot.crosshair',
|
||||
'jquery.flot.fillbelow': '../vendor/jquery/jquery.flot.fillbelow',
|
||||
|
||||
modernizr: '../vendor/modernizr-2.6.1',
|
||||
elasticjs: '../vendor/elasticjs/elastic-angular-client',
|
||||
|
||||
'bootstrap-tagsinput': '../vendor/tagsinput/bootstrap-tagsinput',
|
||||
|
||||
},
|
||||
shim: {
|
||||
underscore: {
|
||||
exports: '_'
|
||||
},
|
||||
|
||||
spectrum: {
|
||||
deps: ['jquery']
|
||||
@@ -79,32 +78,25 @@ require.config({
|
||||
|
||||
// simple dependency declaration
|
||||
//
|
||||
'jquery-ui': ['jquery'],
|
||||
'jquery.flot': ['jquery'],
|
||||
'jquery.flot.byte': ['jquery', 'jquery.flot'],
|
||||
'jquery.flot.pie': ['jquery', 'jquery.flot'],
|
||||
'jquery.flot.events': ['jquery', 'jquery.flot'],
|
||||
'jquery.flot.selection':['jquery', 'jquery.flot'],
|
||||
'jquery.flot.stack': ['jquery', 'jquery.flot'],
|
||||
'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
|
||||
'jquery.flot.time': ['jquery', 'jquery.flot'],
|
||||
|
||||
'angular-sanitize': ['angular'],
|
||||
'angular-cookies': ['angular'],
|
||||
'angular-dragdrop': ['jquery','jquery-ui','angular'],
|
||||
'angular-loader': ['angular'],
|
||||
'jquery.flot.crosshair':['jquery', 'jquery.flot'],
|
||||
'jquery.flot.fillbelow':['jquery', 'jquery.flot'],
|
||||
'angular-dragdrop': ['jquery', 'angular'],
|
||||
'angular-mocks': ['angular'],
|
||||
'angular-resource': ['angular'],
|
||||
'angular-sanitize': ['angular'],
|
||||
'angular-route': ['angular'],
|
||||
'angular-touch': ['angular'],
|
||||
'bindonce': ['angular'],
|
||||
'angular-strap': ['angular', 'bootstrap','timepicker', 'datepicker'],
|
||||
'bindonce': ['angular'],
|
||||
|
||||
timepicker: ['jquery', 'bootstrap'],
|
||||
datepicker: ['jquery', 'bootstrap'],
|
||||
|
||||
elasticjs: ['angular', '../vendor/elasticjs/elastic'],
|
||||
|
||||
'bootstrap-tagsinput': ['jquery'],
|
||||
},
|
||||
waitSeconds: 60,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
define([
|
||||
'underscore',
|
||||
'lodash',
|
||||
'crypto',
|
||||
],
|
||||
function (_, crypto) {
|
||||
@@ -13,21 +13,19 @@ function (_, crypto) {
|
||||
* @type {Object}
|
||||
*/
|
||||
var defaults = {
|
||||
elasticsearch : "http://"+window.location.hostname+":9200",
|
||||
datasources : {
|
||||
default: {
|
||||
url: "http://"+window.location.hostname+":8080",
|
||||
default: true
|
||||
}
|
||||
datasources : {},
|
||||
window_title_prefix : 'Grafana - ',
|
||||
panels : {
|
||||
'graph': { path: 'panels/graph' },
|
||||
'singlestat': { path: 'panels/singlestat' },
|
||||
'text': { path: 'panels/text' }
|
||||
},
|
||||
panels : ['graph', 'text'],
|
||||
plugins : {},
|
||||
default_route : '/dashboard/file/default.json',
|
||||
grafana_index : 'grafana-dash',
|
||||
elasticsearch_all_disabled : false,
|
||||
timezoneOffset : null,
|
||||
playlist_timespan : "1m",
|
||||
unsaved_changes_warning : true
|
||||
unsaved_changes_warning : true,
|
||||
search : { max_results: 100 },
|
||||
admin : {}
|
||||
};
|
||||
|
||||
// This initializes a new hash on purpose, to avoid adding parameters to
|
||||
@@ -57,28 +55,36 @@ function (_, crypto) {
|
||||
return datasource;
|
||||
};
|
||||
|
||||
// backward compatible with old config
|
||||
if (options.graphiteUrl) {
|
||||
settings.datasources = {
|
||||
graphite: {
|
||||
type: 'graphite',
|
||||
url: options.graphiteUrl,
|
||||
default: true
|
||||
}
|
||||
settings.datasources.graphite = {
|
||||
type: 'graphite',
|
||||
url: options.graphiteUrl,
|
||||
default: true
|
||||
};
|
||||
}
|
||||
|
||||
if (options.elasticsearch) {
|
||||
settings.datasources.elasticsearch = {
|
||||
type: 'elasticsearch',
|
||||
url: options.elasticsearch,
|
||||
index: options.grafana_index,
|
||||
grafanaDB: true
|
||||
};
|
||||
}
|
||||
|
||||
_.each(settings.datasources, function(datasource, key) {
|
||||
datasource.name = key;
|
||||
parseBasicAuth(datasource);
|
||||
if (datasource.url) { parseBasicAuth(datasource); }
|
||||
if (datasource.type === 'influxdb') { parseMultipleHosts(datasource); }
|
||||
});
|
||||
|
||||
var elasticParsed = parseBasicAuth({ url: settings.elasticsearch });
|
||||
settings.elasticsearchBasicAuth = elasticParsed.basicAuth;
|
||||
settings.elasticsearch = elasticParsed.url;
|
||||
|
||||
if (settings.plugins.panels) {
|
||||
settings.panels = _.union(settings.panels, settings.plugins.panels);
|
||||
_.extend(settings.panels, settings.plugins.panels);
|
||||
}
|
||||
|
||||
if (!settings.plugins.dependencies) {
|
||||
settings.plugins.dependencies = [];
|
||||
}
|
||||
|
||||
return settings;
|
||||
|
||||
20
src/app/components/store.js
Normal file
20
src/app/components/store.js
Normal file
@@ -0,0 +1,20 @@
|
||||
define([], function() {
|
||||
'use strict';
|
||||
|
||||
return {
|
||||
get: function(key) {
|
||||
return window.localStorage[key];
|
||||
},
|
||||
set: function(key, value) {
|
||||
window.localStorage[key] = value;
|
||||
},
|
||||
getBool: function(key) {
|
||||
return window.localStorage[key] === 'true' ? true : false;
|
||||
},
|
||||
delete: function(key) {
|
||||
window.localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
135
src/app/components/timeSeries.js
Normal file
135
src/app/components/timeSeries.js
Normal file
@@ -0,0 +1,135 @@
|
||||
define([
|
||||
'lodash',
|
||||
'kbn'
|
||||
],
|
||||
function (_, kbn) {
|
||||
'use strict';
|
||||
|
||||
function TimeSeries(opts) {
|
||||
this.datapoints = opts.datapoints;
|
||||
this.label = opts.alias;
|
||||
this.id = opts.alias;
|
||||
this.alias = opts.alias;
|
||||
this.color = opts.color;
|
||||
this.valueFormater = kbn.valueFormats.none;
|
||||
this.stats = {};
|
||||
}
|
||||
|
||||
function matchSeriesOverride(aliasOrRegex, seriesAlias) {
|
||||
if (!aliasOrRegex) { return false; }
|
||||
|
||||
if (aliasOrRegex[0] === '/') {
|
||||
var regex = kbn.stringToJsRegex(aliasOrRegex);
|
||||
return seriesAlias.match(regex) != null;
|
||||
}
|
||||
|
||||
return aliasOrRegex === seriesAlias;
|
||||
}
|
||||
|
||||
function translateFillOption(fill) {
|
||||
return fill === 0 ? 0.001 : fill/10;
|
||||
}
|
||||
|
||||
TimeSeries.prototype.applySeriesOverrides = function(overrides) {
|
||||
this.lines = {};
|
||||
this.points = {};
|
||||
this.bars = {};
|
||||
this.yaxis = 1;
|
||||
this.zindex = 0;
|
||||
delete this.stack;
|
||||
|
||||
for (var i = 0; i < overrides.length; i++) {
|
||||
var override = overrides[i];
|
||||
if (!matchSeriesOverride(override.alias, this.alias)) {
|
||||
continue;
|
||||
}
|
||||
if (override.lines !== void 0) { this.lines.show = override.lines; }
|
||||
if (override.points !== void 0) { this.points.show = override.points; }
|
||||
if (override.bars !== void 0) { this.bars.show = override.bars; }
|
||||
if (override.fill !== void 0) { this.lines.fill = translateFillOption(override.fill); }
|
||||
if (override.stack !== void 0) { this.stack = override.stack; }
|
||||
if (override.linewidth !== void 0) { this.lines.lineWidth = override.linewidth; }
|
||||
if (override.pointradius !== void 0) { this.points.radius = override.pointradius; }
|
||||
if (override.steppedLine !== void 0) { this.lines.steps = override.steppedLine; }
|
||||
if (override.zindex !== void 0) { this.zindex = override.zindex; }
|
||||
if (override.fillBelowTo !== void 0) { this.fillBelowTo = override.fillBelowTo; }
|
||||
|
||||
if (override.yaxis !== void 0) {
|
||||
this.yaxis = override.yaxis;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TimeSeries.prototype.getFlotPairs = function (fillStyle) {
|
||||
var result = [];
|
||||
|
||||
this.stats.total = 0;
|
||||
this.stats.max = -Number.MAX_VALUE;
|
||||
this.stats.min = Number.MAX_VALUE;
|
||||
this.stats.avg = null;
|
||||
this.stats.current = null;
|
||||
this.allIsNull = true;
|
||||
|
||||
var ignoreNulls = fillStyle === 'connected';
|
||||
var nullAsZero = fillStyle === 'null as zero';
|
||||
var currentTime;
|
||||
var currentValue;
|
||||
|
||||
for (var i = 0; i < this.datapoints.length; i++) {
|
||||
currentValue = this.datapoints[i][0];
|
||||
currentTime = this.datapoints[i][1];
|
||||
|
||||
if (currentValue === null) {
|
||||
if (ignoreNulls) { continue; }
|
||||
if (nullAsZero) {
|
||||
currentValue = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (_.isNumber(currentValue)) {
|
||||
this.stats.total += currentValue;
|
||||
this.allIsNull = false;
|
||||
}
|
||||
|
||||
if (currentValue > this.stats.max) {
|
||||
this.stats.max = currentValue;
|
||||
}
|
||||
|
||||
if (currentValue < this.stats.min) {
|
||||
this.stats.min = currentValue;
|
||||
}
|
||||
|
||||
result.push([currentTime, currentValue]);
|
||||
}
|
||||
|
||||
if (this.datapoints.length >= 2) {
|
||||
this.stats.timeStep = this.datapoints[1][1] - this.datapoints[0][1];
|
||||
}
|
||||
|
||||
if (this.stats.max === -Number.MAX_VALUE) { this.stats.max = null; }
|
||||
if (this.stats.min === Number.MAX_VALUE) { this.stats.min = null; }
|
||||
|
||||
if (result.length) {
|
||||
this.stats.avg = (this.stats.total / result.length);
|
||||
this.stats.current = result[result.length-1][1];
|
||||
if (this.stats.current === null && result.length > 1) {
|
||||
this.stats.current = result[result.length-2][1];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
TimeSeries.prototype.updateLegendValues = function(formater, decimals, scaledDecimals) {
|
||||
this.valueFormater = formater;
|
||||
this.decimals = decimals;
|
||||
this.scaledDecimals = scaledDecimals;
|
||||
};
|
||||
|
||||
TimeSeries.prototype.formatValue = function(value) {
|
||||
return this.valueFormater(value, this.decimals, this.scaledDecimals);
|
||||
};
|
||||
|
||||
return TimeSeries;
|
||||
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
define([
|
||||
'./dash',
|
||||
'./dashLoader',
|
||||
'./grafanaCtrl',
|
||||
'./dashboardCtrl',
|
||||
'./dashboardNavCtrl',
|
||||
'./row',
|
||||
'./submenuCtrl',
|
||||
'./pulldown',
|
||||
@@ -12,4 +13,8 @@ define([
|
||||
'./playlistCtrl',
|
||||
'./inspectCtrl',
|
||||
'./opentsdbTargetCtrl',
|
||||
'./annotationsEditorCtrl',
|
||||
'./templateEditorCtrl',
|
||||
'./sharePanelCtrl',
|
||||
'./jsonEditorCtrl',
|
||||
], function () {});
|
||||
|
||||
76
src/app/controllers/annotationsEditorCtrl.js
Normal file
76
src/app/controllers/annotationsEditorCtrl.js
Normal file
@@ -0,0 +1,76 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'jquery'
|
||||
],
|
||||
function (angular, _, $) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('AnnotationsEditorCtrl', function($scope, datasourceSrv) {
|
||||
var annotationDefaults = {
|
||||
name: '',
|
||||
datasource: null,
|
||||
showLine: true,
|
||||
iconColor: '#C0C6BE',
|
||||
lineColor: 'rgba(255, 96, 96, 0.592157)',
|
||||
iconSize: 13,
|
||||
enable: true
|
||||
};
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.editor = { index: 0 };
|
||||
$scope.datasources = datasourceSrv.getAnnotationSources();
|
||||
$scope.annotations = $scope.dashboard.annotations.list;
|
||||
$scope.reset();
|
||||
|
||||
$scope.$watch('editor.index', function(newVal) {
|
||||
if (newVal !== 2) {
|
||||
$scope.reset();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.datasourceChanged = function() {
|
||||
$scope.currentDatasource = _.findWhere($scope.datasources, { name: $scope.currentAnnotation.datasource });
|
||||
if (!$scope.currentDatasource) {
|
||||
$scope.currentDatasource = $scope.datasources[0];
|
||||
}
|
||||
};
|
||||
|
||||
$scope.edit = function(annotation) {
|
||||
$scope.currentAnnotation = annotation;
|
||||
$scope.currentIsNew = false;
|
||||
$scope.datasourceChanged();
|
||||
|
||||
$scope.editor.index = 2;
|
||||
$(".tooltip.in").remove();
|
||||
};
|
||||
|
||||
$scope.reset = function() {
|
||||
$scope.currentAnnotation = angular.copy(annotationDefaults);
|
||||
$scope.currentIsNew = true;
|
||||
$scope.datasourceChanged();
|
||||
$scope.currentAnnotation.datasource = $scope.currentDatasource.name;
|
||||
};
|
||||
|
||||
$scope.update = function() {
|
||||
$scope.reset();
|
||||
$scope.editor.index = 0;
|
||||
};
|
||||
|
||||
$scope.add = function() {
|
||||
$scope.annotations.push($scope.currentAnnotation);
|
||||
$scope.reset();
|
||||
$scope.editor.index = 0;
|
||||
};
|
||||
|
||||
$scope.removeAnnotation = function(annotation) {
|
||||
var index = _.indexOf($scope.annotations, annotation);
|
||||
$scope.annotations.splice(index, 1);
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
108
src/app/controllers/console-ctrl.js
Normal file
108
src/app/controllers/console-ctrl.js
Normal file
@@ -0,0 +1,108 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'moment',
|
||||
'store'
|
||||
],
|
||||
function (angular, _, moment, store) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
var consoleEnabled = store.getBool('grafanaConsole');
|
||||
|
||||
if (!consoleEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
var events = [];
|
||||
|
||||
function ConsoleEvent(type, title, data) {
|
||||
this.type = type;
|
||||
this.title = title;
|
||||
this.data = data;
|
||||
this.time = moment().format('hh:mm:ss');
|
||||
|
||||
if (data.config) {
|
||||
this.method = data.config.method;
|
||||
this.elapsed = (new Date().getTime() - data.config.$grafana_timestamp) + ' ms';
|
||||
if (data.config.params && data.config.params.q) {
|
||||
this.field2 = data.config.params.q;
|
||||
}
|
||||
if (_.isString(data.config.data)) {
|
||||
this.field2 = data.config.data;
|
||||
}
|
||||
if (data.status !== 200) {
|
||||
this.error = true;
|
||||
this.field3 = data.data;
|
||||
}
|
||||
|
||||
if (_.isArray(data.data)) {
|
||||
this.extractTimeseriesInfo(data.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConsoleEvent.prototype.extractTimeseriesInfo = function(series) {
|
||||
if (series.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var points = 0;
|
||||
var ok = false;
|
||||
|
||||
if (series[0].datapoints) {
|
||||
points = _.reduce(series, function(memo, val) {
|
||||
return memo + val.datapoints.length;
|
||||
}, 0);
|
||||
ok = true;
|
||||
}
|
||||
if (series[0].columns) {
|
||||
points = _.reduce(series, function(memo, val) {
|
||||
return memo + val.points.length;
|
||||
}, 0);
|
||||
ok = true;
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
this.field1 = '(' + series.length + ' series';
|
||||
this.field1 += ', ' + points + ' points)';
|
||||
}
|
||||
};
|
||||
|
||||
module.config(function($provide, $httpProvider) {
|
||||
$provide.factory('mupp', function($q) {
|
||||
return {
|
||||
'request': function(config) {
|
||||
if (config.inspect) {
|
||||
config.$grafana_timestamp = new Date().getTime();
|
||||
}
|
||||
return config;
|
||||
},
|
||||
'response': function(response) {
|
||||
if (response.config.inspect) {
|
||||
events.push(new ConsoleEvent(response.config.inspect.type, response.config.url, response));
|
||||
}
|
||||
return response;
|
||||
},
|
||||
'requestError': function(rejection) {
|
||||
console.log('requestError', rejection);
|
||||
return $q.reject(rejection);
|
||||
},
|
||||
'responseError': function (rejection) {
|
||||
var inspect = rejection.config.inspect || { type: 'error' };
|
||||
events.push(new ConsoleEvent(inspect.type, rejection.config.url, rejection));
|
||||
return $q.reject(rejection);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
$httpProvider.interceptors.push('mupp');
|
||||
});
|
||||
|
||||
module.controller('ConsoleCtrl', function($scope) {
|
||||
|
||||
$scope.events = events;
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,152 +0,0 @@
|
||||
/** @scratch /index/0
|
||||
* = Kibana
|
||||
*
|
||||
* // Why can't I have a preamble here?
|
||||
*
|
||||
* == Introduction
|
||||
*
|
||||
* Kibana is an open source (Apache Licensed), browser based analytics and search dashboard for
|
||||
* ElasticSearch. Kibana is a snap to setup and start using. Written entirely in HTML and Javascript
|
||||
* it requires only a plain webserver, Kibana requires no fancy server side components.
|
||||
* Kibana strives to be easy to get started with, while also being flexible and powerful, just like
|
||||
* Elasticsearch.
|
||||
*
|
||||
* include::configuration/config.js.asciidoc[]
|
||||
*
|
||||
* include::panels.asciidoc[]
|
||||
*
|
||||
*/
|
||||
|
||||
define([
|
||||
'angular',
|
||||
'jquery',
|
||||
'config',
|
||||
'underscore',
|
||||
'services/all',
|
||||
'services/dashboard/all'
|
||||
],
|
||||
function (angular, $, config, _) {
|
||||
"use strict";
|
||||
|
||||
var module = angular.module('kibana.controllers');
|
||||
|
||||
module.controller('DashCtrl', function(
|
||||
$scope, $rootScope, $timeout, ejsResource, dashboard, filterSrv, dashboardKeybindings,
|
||||
alertSrv, panelMove, keyboardManager, grafanaVersion) {
|
||||
|
||||
$scope.requiredElasticSearchVersion = ">=0.90.3";
|
||||
|
||||
$scope.editor = {
|
||||
index: 0
|
||||
};
|
||||
|
||||
$scope.grafanaVersion = grafanaVersion[0] === '@' ? 'master' : grafanaVersion;
|
||||
|
||||
// For moving stuff around the dashboard.
|
||||
$scope.panelMoveDrop = panelMove.onDrop;
|
||||
$scope.panelMoveStart = panelMove.onStart;
|
||||
$scope.panelMoveStop = panelMove.onStop;
|
||||
$scope.panelMoveOver = panelMove.onOver;
|
||||
$scope.panelMoveOut = panelMove.onOut;
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.config = config;
|
||||
|
||||
// Make stuff, including underscore.js available to views
|
||||
$scope._ = _;
|
||||
$scope.dashboard = dashboard;
|
||||
$scope.dashAlerts = alertSrv;
|
||||
|
||||
$scope.filter = filterSrv;
|
||||
$scope.filter.init(dashboard.current);
|
||||
|
||||
$rootScope.$on("dashboard-loaded", function(event, dashboard) {
|
||||
$scope.filter.init(dashboard);
|
||||
});
|
||||
|
||||
// Clear existing alerts
|
||||
alertSrv.clearAll();
|
||||
|
||||
$scope.reset_row();
|
||||
|
||||
$scope.ejs = ejsResource(config.elasticsearch, config.elasticsearchBasicAuth);
|
||||
|
||||
$scope.bindKeyboardShortcuts();
|
||||
};
|
||||
|
||||
$scope.bindKeyboardShortcuts = dashboardKeybindings.shortcuts;
|
||||
|
||||
$scope.isPanel = function(obj) {
|
||||
if(!_.isNull(obj) && !_.isUndefined(obj) && !_.isUndefined(obj.type)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.add_row = function(dash, row) {
|
||||
dash.rows.push(row);
|
||||
};
|
||||
|
||||
$scope.add_row_default = function() {
|
||||
$scope.reset_row();
|
||||
$scope.row.title = 'New row';
|
||||
$scope.add_row(dashboard.current, $scope.row);
|
||||
};
|
||||
|
||||
$scope.reset_row = function() {
|
||||
$scope.row = {
|
||||
title: '',
|
||||
height: '250px',
|
||||
editable: true,
|
||||
};
|
||||
};
|
||||
|
||||
$scope.row_style = function(row) {
|
||||
return { 'min-height': row.collapse ? '5px' : row.height };
|
||||
};
|
||||
|
||||
$scope.panel_path =function(type) {
|
||||
if(type) {
|
||||
return 'app/panels/'+type.replace(".","/");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.edit_path = function(type) {
|
||||
var p = $scope.panel_path(type);
|
||||
if(p) {
|
||||
return p+'/editor.html';
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setEditorTabs = function(panelMeta) {
|
||||
$scope.editorTabs = ['General','Panel'];
|
||||
if(!_.isUndefined(panelMeta.editorTabs)) {
|
||||
$scope.editorTabs = _.union($scope.editorTabs,_.pluck(panelMeta.editorTabs,'title'));
|
||||
}
|
||||
return $scope.editorTabs;
|
||||
};
|
||||
|
||||
// This is whoafully incomplete, but will do for now
|
||||
$scope.parse_error = function(data) {
|
||||
var _error = data.match("nested: (.*?);");
|
||||
return _.isNull(_error) ? data : _error[1];
|
||||
};
|
||||
|
||||
$scope.colors = [
|
||||
"#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", //1
|
||||
"#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", //2
|
||||
"#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0", //3
|
||||
"#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93", //4
|
||||
"#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7", //5
|
||||
"#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B", //6
|
||||
"#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7" //7
|
||||
];
|
||||
|
||||
$scope.init();
|
||||
});
|
||||
});
|
||||
@@ -1,172 +0,0 @@
|
||||
define([
|
||||
'angular',
|
||||
'underscore',
|
||||
'moment'
|
||||
],
|
||||
function (angular, _, moment) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.controllers');
|
||||
|
||||
module.controller('dashLoader', function($scope, $rootScope, $http, dashboard, alertSrv, $location, playlistSrv) {
|
||||
$scope.loader = dashboard.current.loader;
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/;
|
||||
$scope.gist = $scope.gist || {};
|
||||
$scope.elasticsearch = $scope.elasticsearch || {};
|
||||
|
||||
$rootScope.$on('save-dashboard', function() {
|
||||
$scope.elasticsearch_save('dashboard', false);
|
||||
});
|
||||
|
||||
$rootScope.$on('zoom-out', function() {
|
||||
$scope.zoom(2);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.exitFullscreen = function() {
|
||||
$rootScope.$emit('panel-fullscreen-exit');
|
||||
};
|
||||
|
||||
$scope.showDropdown = function(type) {
|
||||
if(_.isUndefined(dashboard.current.loader)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var _l = dashboard.current.loader;
|
||||
if(type === 'load') {
|
||||
return (_l.load_elasticsearch || _l.load_gist || _l.load_local);
|
||||
}
|
||||
if(type === 'save') {
|
||||
return (_l.save_elasticsearch || _l.save_gist || _l.save_local || _l.save_default);
|
||||
}
|
||||
if(type === 'share') {
|
||||
return (_l.save_temp);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.set_default = function() {
|
||||
if(dashboard.set_default($location.path())) {
|
||||
alertSrv.set('Home Set','This page has been set as your default dashboard','success',5000);
|
||||
} else {
|
||||
alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.purge_default = function() {
|
||||
if(dashboard.purge_default()) {
|
||||
alertSrv.set('Local Default Clear','Your default dashboard has been reset to the default',
|
||||
'success',5000);
|
||||
} else {
|
||||
alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.elasticsearch_save = function(type,ttl) {
|
||||
dashboard.elasticsearch_save(type, dashboard.current.title, ttl)
|
||||
.then(function(result) {
|
||||
if(_.isUndefined(result._id)) {
|
||||
alertSrv.set('Save failed','Dashboard could not be saved to Elasticsearch','error',5000);
|
||||
return;
|
||||
}
|
||||
|
||||
alertSrv.set('Dashboard Saved', 'Dashboard has been saved to Elasticsearch as "' + result._id + '"','success', 5000);
|
||||
if(type === 'temp') {
|
||||
$scope.share = dashboard.share_link(dashboard.current.title,'temp',result._id);
|
||||
}
|
||||
|
||||
$rootScope.$emit('dashboard-saved', dashboard.current);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.elasticsearch_delete = function(id) {
|
||||
if (!confirm('Are you sure you want to delete dashboard?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
dashboard.elasticsearch_delete(id).then(
|
||||
function(result) {
|
||||
if(!_.isUndefined(result)) {
|
||||
if(result.found) {
|
||||
alertSrv.set('Dashboard Deleted',id+' has been deleted','success',5000);
|
||||
// Find the deleted dashboard in the cached list and remove it
|
||||
var toDelete = _.where($scope.elasticsearch.dashboards,{_id:id})[0];
|
||||
$scope.elasticsearch.dashboards = _.without($scope.elasticsearch.dashboards,toDelete);
|
||||
} else {
|
||||
alertSrv.set('Dashboard Not Found','Could not find '+id+' in Elasticsearch','warning',5000);
|
||||
}
|
||||
} else {
|
||||
alertSrv.set('Dashboard Not Deleted','An error occurred deleting the dashboard','error',5000);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.save_gist = function() {
|
||||
dashboard.save_gist($scope.gist.title).then(function(link) {
|
||||
if (!_.isUndefined(link)) {
|
||||
$scope.gist.last = link;
|
||||
alertSrv.set('Gist saved','You will be able to access your exported dashboard file at '+
|
||||
'<a href="'+link+'">'+link+'</a> in a moment','success');
|
||||
} else {
|
||||
alertSrv.set('Save failed','Gist could not be saved','error',5000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.gist_dblist = function(id) {
|
||||
dashboard.gist_list(id).then(function(files) {
|
||||
if (files && files.length > 0) {
|
||||
$scope.gist.files = files;
|
||||
} else {
|
||||
alertSrv.set('Gist Failed','Could not retrieve dashboard list from gist','error',5000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// function $scope.zoom
|
||||
// factor :: Zoom factor, so 0.5 = cuts timespan in half, 2 doubles timespan
|
||||
$scope.zoom = function(factor) {
|
||||
var _range = this.filter.timeRange();
|
||||
var _timespan = (_range.to.valueOf() - _range.from.valueOf());
|
||||
var _center = _range.to.valueOf() - _timespan/2;
|
||||
|
||||
var _to = (_center + (_timespan*factor)/2);
|
||||
var _from = (_center - (_timespan*factor)/2);
|
||||
|
||||
// If we're not already looking into the future, don't.
|
||||
if(_to > Date.now() && _range.to < Date.now()) {
|
||||
var _offset = _to - Date.now();
|
||||
_from = _from - _offset;
|
||||
_to = Date.now();
|
||||
}
|
||||
|
||||
this.filter.setTime({
|
||||
from:moment.utc(_from).toDate(),
|
||||
to:moment.utc(_to).toDate(),
|
||||
});
|
||||
};
|
||||
|
||||
$scope.openSaveDropdown = function() {
|
||||
$scope.isFavorite = playlistSrv.isCurrentFavorite();
|
||||
};
|
||||
|
||||
$scope.markAsFavorite = function() {
|
||||
playlistSrv.markAsFavorite();
|
||||
$scope.isFavorite = true;
|
||||
};
|
||||
|
||||
$scope.removeAsFavorite = function() {
|
||||
playlistSrv.removeAsFavorite(dashboard.current);
|
||||
$scope.isFavorite = false;
|
||||
};
|
||||
|
||||
$scope.stopPlaylist = function() {
|
||||
playlistSrv.stop(1);
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
131
src/app/controllers/dashboardCtrl.js
Normal file
131
src/app/controllers/dashboardCtrl.js
Normal file
@@ -0,0 +1,131 @@
|
||||
define([
|
||||
'angular',
|
||||
'jquery',
|
||||
'config',
|
||||
'lodash',
|
||||
],
|
||||
function (angular, $, config, _) {
|
||||
"use strict";
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('DashboardCtrl', function(
|
||||
$scope,
|
||||
$rootScope,
|
||||
dashboardKeybindings,
|
||||
timeSrv,
|
||||
templateValuesSrv,
|
||||
dashboardSrv,
|
||||
dashboardViewStateSrv,
|
||||
$timeout) {
|
||||
|
||||
$scope.editor = { index: 0 };
|
||||
$scope.panelNames = _.map(config.panels, function(value, key) { return key; });
|
||||
var resizeEventTimeout;
|
||||
|
||||
this.init = function(dashboardData) {
|
||||
$scope.availablePanels = config.panels;
|
||||
$scope.reset_row();
|
||||
$scope.registerWindowResizeEvent();
|
||||
$scope.onAppEvent('show-json-editor', $scope.showJsonEditor);
|
||||
$scope.setupDashboard(dashboardData);
|
||||
};
|
||||
|
||||
$scope.registerWindowResizeEvent = function() {
|
||||
angular.element(window).bind('resize', function() {
|
||||
$timeout.cancel(resizeEventTimeout);
|
||||
resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setupDashboard = function(dashboardData) {
|
||||
$rootScope.performance.dashboardLoadStart = new Date().getTime();
|
||||
$rootScope.performance.panelsInitialized = 0;
|
||||
$rootScope.performance.panelsRendered = 0;
|
||||
|
||||
$scope.dashboard = dashboardSrv.create(dashboardData);
|
||||
$scope.dashboardViewState = dashboardViewStateSrv.create($scope);
|
||||
|
||||
// init services
|
||||
timeSrv.init($scope.dashboard);
|
||||
templateValuesSrv.init($scope.dashboard, $scope.dashboardViewState);
|
||||
|
||||
$scope.checkFeatureToggles();
|
||||
dashboardKeybindings.shortcuts($scope);
|
||||
|
||||
$scope.setWindowTitleAndTheme();
|
||||
|
||||
$scope.appEvent("dashboard-loaded", $scope.dashboard);
|
||||
};
|
||||
|
||||
$scope.setWindowTitleAndTheme = function() {
|
||||
window.document.title = config.window_title_prefix + $scope.dashboard.title;
|
||||
$scope.grafana.style = $scope.dashboard.style;
|
||||
};
|
||||
|
||||
$scope.isPanel = function(obj) {
|
||||
if(!_.isNull(obj) && !_.isUndefined(obj) && !_.isUndefined(obj.type)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.add_row = function(dash, row) {
|
||||
dash.rows.push(row);
|
||||
};
|
||||
|
||||
$scope.add_row_default = function() {
|
||||
$scope.reset_row();
|
||||
$scope.row.title = 'New row';
|
||||
$scope.add_row($scope.dashboard, $scope.row);
|
||||
};
|
||||
|
||||
$scope.reset_row = function() {
|
||||
$scope.row = {
|
||||
title: '',
|
||||
height: '250px',
|
||||
editable: true,
|
||||
};
|
||||
};
|
||||
|
||||
$scope.panelEditorPath = function(type) {
|
||||
return 'app/' + config.panels[type].path + '/editor.html';
|
||||
};
|
||||
|
||||
$scope.pulldownEditorPath = function(type) {
|
||||
return 'app/panels/'+type+'/editor.html';
|
||||
};
|
||||
|
||||
$scope.showJsonEditor = function(evt, options) {
|
||||
var editScope = $rootScope.$new();
|
||||
editScope.object = options.object;
|
||||
editScope.updateHandler = options.updateHandler;
|
||||
$scope.appEvent('show-dash-editor', { src: 'app/partials/edit_json.html', scope: editScope });
|
||||
};
|
||||
|
||||
$scope.checkFeatureToggles = function() {
|
||||
$scope.submenuEnabled = $scope.dashboard.templating.enable || $scope.dashboard.annotations.enable;
|
||||
};
|
||||
|
||||
$scope.onDrop = function(panelId, row, dropTarget) {
|
||||
var info = $scope.dashboard.getPanelInfoById(panelId);
|
||||
if (dropTarget) {
|
||||
var dropInfo = $scope.dashboard.getPanelInfoById(dropTarget.id);
|
||||
dropInfo.row.panels[dropInfo.index] = info.panel;
|
||||
info.row.panels[info.index] = dropTarget;
|
||||
var dragSpan = info.panel.span;
|
||||
info.panel.span = dropTarget.span;
|
||||
dropTarget.span = dragSpan;
|
||||
}
|
||||
else {
|
||||
info.row.panels.splice(info.index, 1);
|
||||
info.panel.span = 12 - $scope.dashboard.rowSpan(row);
|
||||
row.panels.push(info.panel);
|
||||
}
|
||||
|
||||
$rootScope.$broadcast('render');
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
172
src/app/controllers/dashboardNavCtrl.js
Normal file
172
src/app/controllers/dashboardNavCtrl.js
Normal file
@@ -0,0 +1,172 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'moment',
|
||||
'config',
|
||||
'store',
|
||||
'filesaver'
|
||||
],
|
||||
function (angular, _, moment, config, store) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('DashboardNavCtrl', function($scope, $rootScope, alertSrv, $location, playlistSrv, datasourceSrv, timeSrv) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.db = datasourceSrv.getGrafanaDB();
|
||||
|
||||
$scope.onAppEvent('save-dashboard', $scope.saveDashboard);
|
||||
$scope.onAppEvent('delete-dashboard', $scope.deleteDashboard);
|
||||
|
||||
$scope.onAppEvent('zoom-out', function() {
|
||||
$scope.zoom(2);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
$scope.set_default = function() {
|
||||
store.set('grafanaDashboardDefault', $location.path());
|
||||
alertSrv.set('Home Set','This page has been set as your default dashboard','success',5000);
|
||||
};
|
||||
|
||||
$scope.purge_default = function() {
|
||||
store.delete('grafanaDashboardDefault');
|
||||
alertSrv.set('Local Default Clear','Your default dashboard has been reset to the default','success', 5000);
|
||||
};
|
||||
|
||||
$scope.saveForSharing = function() {
|
||||
var clone = angular.copy($scope.dashboard);
|
||||
clone.temp = true;
|
||||
$scope.db.saveDashboard(clone)
|
||||
.then(function(result) {
|
||||
|
||||
$scope.share = { url: result.url, title: result.title };
|
||||
|
||||
}, function(err) {
|
||||
alertSrv.set('Save for sharing failed', err, 'error',5000);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.passwordCache = function(pwd) {
|
||||
if (!window.sessionStorage) { return null; }
|
||||
if (!pwd) { return window.sessionStorage["grafanaAdminPassword"]; }
|
||||
window.sessionStorage["grafanaAdminPassword"] = pwd;
|
||||
};
|
||||
|
||||
$scope.isAdmin = function() {
|
||||
if (!config.admin || !config.admin.password) { return true; }
|
||||
if ($scope.passwordCache() === config.admin.password) { return true; }
|
||||
|
||||
var password = window.prompt("Admin password", "");
|
||||
$scope.passwordCache(password);
|
||||
|
||||
if (password === config.admin.password) { return true; }
|
||||
|
||||
alertSrv.set('Save failed', 'Password incorrect', 'error');
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.openSearch = function() {
|
||||
$scope.appEvent('show-dash-editor', { src: 'app/partials/search.html' });
|
||||
};
|
||||
|
||||
$scope.saveDashboard = function() {
|
||||
if (!$scope.isAdmin()) { return false; }
|
||||
|
||||
var clone = angular.copy($scope.dashboard);
|
||||
$scope.db.saveDashboard(clone)
|
||||
.then(function(result) {
|
||||
$scope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + result.title]);
|
||||
|
||||
if (result.url !== $location.path()) {
|
||||
$location.search({});
|
||||
$location.path(result.url);
|
||||
}
|
||||
|
||||
$rootScope.$emit('dashboard-saved', $scope.dashboard);
|
||||
|
||||
}, function(err) {
|
||||
$scope.appEvent('alert-error', ['Save failed', err]);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteDashboard = function(evt, options) {
|
||||
if (!$scope.isAdmin()) { return false; }
|
||||
|
||||
$scope.appEvent('confirm-modal', {
|
||||
title: 'Delete dashboard',
|
||||
text: 'Do you want to delete dashboard ' + options.title + '?',
|
||||
onConfirm: function() {
|
||||
$scope.deleteDashboardConfirmed(options);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteDashboardConfirmed = function(options) {
|
||||
var id = options.id;
|
||||
$scope.db.deleteDashboard(id).then(function(id) {
|
||||
$scope.appEvent('dashboard-deleted', id);
|
||||
$scope.appEvent('alert-success', ['Dashboard Deleted', id + ' has been deleted']);
|
||||
}, function(err) {
|
||||
$scope.appEvent('alert-error', ['Deleted failed', err]);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.exportDashboard = function() {
|
||||
var blob = new Blob([angular.toJson($scope.dashboard, true)], { type: "application/json;charset=utf-8" });
|
||||
window.saveAs(blob, $scope.dashboard.title + '-' + new Date().getTime());
|
||||
};
|
||||
|
||||
$scope.zoom = function(factor) {
|
||||
var range = timeSrv.timeRange();
|
||||
|
||||
var timespan = (range.to.valueOf() - range.from.valueOf());
|
||||
var center = range.to.valueOf() - timespan/2;
|
||||
|
||||
var to = (center + (timespan*factor)/2);
|
||||
var from = (center - (timespan*factor)/2);
|
||||
|
||||
if(to > Date.now() && range.to <= Date.now()) {
|
||||
var offset = to - Date.now();
|
||||
from = from - offset;
|
||||
to = Date.now();
|
||||
}
|
||||
|
||||
timeSrv.setTime({
|
||||
from: moment.utc(from).toDate(),
|
||||
to: moment.utc(to).toDate(),
|
||||
});
|
||||
};
|
||||
|
||||
$scope.styleUpdated = function() {
|
||||
$scope.grafana.style = $scope.dashboard.style;
|
||||
};
|
||||
|
||||
$scope.editJson = function() {
|
||||
$scope.appEvent('show-json-editor', { object: $scope.dashboard });
|
||||
};
|
||||
|
||||
$scope.openSaveDropdown = function() {
|
||||
$scope.isFavorite = playlistSrv.isCurrentFavorite($scope.dashboard);
|
||||
$scope.saveDropdownOpened = true;
|
||||
};
|
||||
|
||||
$scope.markAsFavorite = function() {
|
||||
playlistSrv.markAsFavorite($scope.dashboard);
|
||||
$scope.isFavorite = true;
|
||||
};
|
||||
|
||||
$scope.removeAsFavorite = function() {
|
||||
playlistSrv.removeAsFavorite($scope.dashboard);
|
||||
$scope.isFavorite = false;
|
||||
};
|
||||
|
||||
$scope.stopPlaylist = function() {
|
||||
playlistSrv.stop(1);
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
112
src/app/controllers/grafanaCtrl.js
Normal file
112
src/app/controllers/grafanaCtrl.js
Normal file
@@ -0,0 +1,112 @@
|
||||
define([
|
||||
'angular',
|
||||
'config',
|
||||
'lodash',
|
||||
'jquery',
|
||||
'store',
|
||||
],
|
||||
function (angular, config, _, $, store) {
|
||||
"use strict";
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('GrafanaCtrl', function($scope, alertSrv, utilSrv, grafanaVersion, $rootScope, $controller) {
|
||||
|
||||
$scope.grafanaVersion = grafanaVersion[0] === '@' ? 'master' : grafanaVersion;
|
||||
$scope._ = _;
|
||||
$rootScope.profilingEnabled = store.getBool('profilingEnabled');
|
||||
$rootScope.performance = { loadStart: new Date().getTime() };
|
||||
|
||||
$scope.init = function() {
|
||||
if ($rootScope.profilingEnabled) { $scope.initProfiling(); }
|
||||
|
||||
alertSrv.init();
|
||||
utilSrv.init();
|
||||
|
||||
$scope.dashAlerts = alertSrv;
|
||||
$scope.grafana = { style: 'dark' };
|
||||
};
|
||||
|
||||
$scope.toggleConsole = function() {
|
||||
$scope.consoleEnabled = !$scope.consoleEnabled;
|
||||
store.set('grafanaConsole', $scope.consoleEnabled);
|
||||
};
|
||||
|
||||
$scope.initDashboard = function(dashboardData, viewScope) {
|
||||
$controller('DashboardCtrl', { $scope: viewScope }).init(dashboardData);
|
||||
};
|
||||
|
||||
$rootScope.onAppEvent = function(name, callback) {
|
||||
var unbind = $rootScope.$on(name, callback);
|
||||
this.$on('$destroy', unbind);
|
||||
};
|
||||
|
||||
$rootScope.appEvent = function(name, payload) {
|
||||
$rootScope.$emit(name, payload);
|
||||
};
|
||||
|
||||
$rootScope.colors = [
|
||||
"#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", //1
|
||||
"#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", //2
|
||||
"#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0", //3
|
||||
"#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93", //4
|
||||
"#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7", //5
|
||||
"#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B", //6
|
||||
"#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7" //7
|
||||
];
|
||||
|
||||
$scope.getTotalWatcherCount = function() {
|
||||
var count = 0;
|
||||
var scopes = 0;
|
||||
var root = $(document.getElementsByTagName('body'));
|
||||
|
||||
var f = function (element) {
|
||||
if (element.data().hasOwnProperty('$scope')) {
|
||||
scopes++;
|
||||
angular.forEach(element.data().$scope.$$watchers, function () {
|
||||
count++;
|
||||
});
|
||||
}
|
||||
|
||||
angular.forEach(element.children(), function (childElement) {
|
||||
f($(childElement));
|
||||
});
|
||||
};
|
||||
|
||||
f(root);
|
||||
$rootScope.performance.scopeCount = scopes;
|
||||
return count;
|
||||
};
|
||||
|
||||
$scope.initProfiling = function() {
|
||||
var count = 0;
|
||||
|
||||
$scope.$watch(function digestCounter() { count++; }, function() { });
|
||||
$scope.onAppEvent('dashboard-loaded', function() {
|
||||
count = 0;
|
||||
|
||||
setTimeout(function() {
|
||||
console.log("Dashboard::Performance Total Digests: " + count);
|
||||
console.log("Dashboard::Performance Total Watchers: " + $scope.getTotalWatcherCount());
|
||||
console.log("Dashboard::Performance Total ScopeCount: " + $rootScope.performance.scopeCount);
|
||||
|
||||
var timeTaken = $rootScope.performance.allPanelsInitialized - $rootScope.performance.dashboardLoadStart;
|
||||
console.log("Dashboard::Performance - All panels initialized in " + timeTaken + " ms");
|
||||
|
||||
// measure digest performance
|
||||
var rootDigestStart = window.performance.now();
|
||||
for (var i = 0; i < 30; i++) {
|
||||
$rootScope.$apply();
|
||||
}
|
||||
console.log("Dashboard::Performance Root Digest " + ((window.performance.now() - rootDigestStart) / 30));
|
||||
|
||||
}, 3000);
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,18 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore'
|
||||
'lodash',
|
||||
'kbn'
|
||||
],
|
||||
function (angular, app, _) {
|
||||
function (angular, app, _, kbn) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.controllers');
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('GraphiteImportCtrl', function($scope, $rootScope, $timeout, datasourceSrv, dashboard) {
|
||||
module.controller('GraphiteImportCtrl', function($scope, $rootScope, $timeout, datasourceSrv, $location) {
|
||||
|
||||
$scope.init = function() {
|
||||
console.log('hej!');
|
||||
$scope.datasources = datasourceSrv.listOptions();
|
||||
$scope.datasources = datasourceSrv.getMetricSources();
|
||||
$scope.setDatasource(null);
|
||||
};
|
||||
|
||||
@@ -68,23 +68,24 @@ function (angular, app, _) {
|
||||
|
||||
currentRow = angular.copy(rowTemplate);
|
||||
|
||||
var newDashboard = angular.copy(dashboard.current);
|
||||
var newDashboard = angular.copy($scope.dashboard);
|
||||
newDashboard.rows = [];
|
||||
newDashboard.title = state.name;
|
||||
newDashboard.rows.push(currentRow);
|
||||
|
||||
_.each(state.graphs, function(graph) {
|
||||
_.each(state.graphs, function(graph, index) {
|
||||
if (currentRow.panels.length === graphsPerRow) {
|
||||
currentRow = angular.copy(rowTemplate);
|
||||
newDashboard.rows.push(currentRow);
|
||||
}
|
||||
|
||||
panel = {
|
||||
type: 'graphite',
|
||||
type: 'graph',
|
||||
span: 12 / graphsPerRow,
|
||||
title: graph[1].title,
|
||||
targets: [],
|
||||
datasource: datasource
|
||||
datasource: datasource,
|
||||
id: index + 1
|
||||
};
|
||||
|
||||
_.each(graph[1].target, function(target) {
|
||||
@@ -96,7 +97,10 @@ function (angular, app, _) {
|
||||
currentRow.panels.push(panel);
|
||||
});
|
||||
|
||||
dashboard.dash_load(newDashboard);
|
||||
window.grafanaImportDashboard = newDashboard;
|
||||
$location.path('/dashboard/import/' + kbn.slugifyForUrl(newDashboard.title));
|
||||
|
||||
$scope.dismiss();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
define([
|
||||
'angular',
|
||||
'underscore',
|
||||
'lodash',
|
||||
'config',
|
||||
'../services/graphite/gfunc',
|
||||
'../services/graphite/parser'
|
||||
@@ -8,12 +8,14 @@ define([
|
||||
function (angular, _, config, gfunc, Parser) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.controllers');
|
||||
var module = angular.module('grafana.controllers');
|
||||
var targetLetters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'];
|
||||
|
||||
module.controller('GraphiteTargetCtrl', function($scope) {
|
||||
module.controller('GraphiteTargetCtrl', function($scope, $sce, templateSrv) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.target.target = $scope.target.target || '';
|
||||
$scope.targetLetters = targetLetters;
|
||||
|
||||
parseTarget();
|
||||
};
|
||||
@@ -52,6 +54,13 @@ function (angular, _, config, gfunc, Parser) {
|
||||
checkOtherSegments($scope.segments.length - 1);
|
||||
}
|
||||
|
||||
function addFunctionParameter(func, value, index, shiftBack) {
|
||||
if (shiftBack) {
|
||||
index = Math.max(index - 1, 0);
|
||||
}
|
||||
func.params[index] = value;
|
||||
}
|
||||
|
||||
function parseTargeRecursive(astNode, func, index) {
|
||||
if (astNode === null) {
|
||||
return null;
|
||||
@@ -59,7 +68,7 @@ function (angular, _, config, gfunc, Parser) {
|
||||
|
||||
switch(astNode.type) {
|
||||
case 'function':
|
||||
var innerFunc = gfunc.createFuncInstance(astNode.name);
|
||||
var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
|
||||
|
||||
_.each(astNode.params, function(param, index) {
|
||||
parseTargeRecursive(param, innerFunc, index);
|
||||
@@ -69,40 +78,27 @@ function (angular, _, config, gfunc, Parser) {
|
||||
$scope.functions.push(innerFunc);
|
||||
break;
|
||||
|
||||
case 'series-ref':
|
||||
addFunctionParameter(func, astNode.value, index, $scope.segments.length > 0);
|
||||
break;
|
||||
case 'string':
|
||||
case 'number':
|
||||
if ((index-1) >= func.def.params.length) {
|
||||
throw { message: 'invalid number of parameters to method ' + func.def.name };
|
||||
}
|
||||
|
||||
if (index === 0) {
|
||||
func.params[index] = astNode.value;
|
||||
}
|
||||
else {
|
||||
func.params[index - 1] = astNode.value;
|
||||
}
|
||||
|
||||
addFunctionParameter(func, astNode.value, index, true);
|
||||
break;
|
||||
|
||||
case 'metric':
|
||||
if ($scope.segments.length > 0) {
|
||||
throw { message: 'Multiple metric params not supported, use text editor.' };
|
||||
if (astNode.segments.length !== 1) {
|
||||
throw { message: 'Multiple metric params not supported, use text editor.' };
|
||||
}
|
||||
addFunctionParameter(func, astNode.segments[0].value, index, true);
|
||||
break;
|
||||
}
|
||||
|
||||
$scope.segments = _.map(astNode.segments, function(segment) {
|
||||
var node = {
|
||||
type: segment.type,
|
||||
val: segment.value,
|
||||
html: segment.value
|
||||
};
|
||||
if (segment.value === '*') {
|
||||
node.html = '<i class="icon-asterisk"><i>';
|
||||
}
|
||||
if (segment.type === 'template') {
|
||||
node.val = node.html = '[[' + segment.value + ']]';
|
||||
node.html = "<span style='color: #ECEC09'>" + node.html + "</span>";
|
||||
}
|
||||
return node;
|
||||
return new MetricSegment(segment);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -111,27 +107,29 @@ function (angular, _, config, gfunc, Parser) {
|
||||
var arr = $scope.segments.slice(0, index);
|
||||
|
||||
return _.reduce(arr, function(result, segment) {
|
||||
return result ? (result + "." + segment.val) : segment.val;
|
||||
return result ? (result + "." + segment.value) : segment.value;
|
||||
}, "");
|
||||
}
|
||||
|
||||
function checkOtherSegments(fromIndex) {
|
||||
if (fromIndex === 0) {
|
||||
$scope.segments.push({html: 'select metric'});
|
||||
$scope.segments.push(new MetricSegment('select metric'));
|
||||
return;
|
||||
}
|
||||
|
||||
var path = getSegmentPathUpTo(fromIndex + 1);
|
||||
return $scope.datasource.metricFindQuery($scope.filter, path)
|
||||
return $scope.datasource.metricFindQuery(path)
|
||||
.then(function(segments) {
|
||||
if (segments.length === 0) {
|
||||
$scope.segments = $scope.segments.splice(0, fromIndex);
|
||||
$scope.segments.push({html: 'select metric'});
|
||||
if (path !== '') {
|
||||
$scope.segments = $scope.segments.splice(0, fromIndex);
|
||||
$scope.segments.push(new MetricSegment('select metric'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (segments[0].expandable) {
|
||||
if ($scope.segments.length === fromIndex) {
|
||||
$scope.segments.push({html: 'select metric'});
|
||||
$scope.segments.push(new MetricSegment('select metric'));
|
||||
}
|
||||
else {
|
||||
return checkOtherSegments(fromIndex + 1);
|
||||
@@ -156,39 +154,43 @@ function (angular, _, config, gfunc, Parser) {
|
||||
$scope.getAltSegments = function (index) {
|
||||
$scope.altSegments = [];
|
||||
|
||||
var query = index === 0 ?
|
||||
'*' : getSegmentPathUpTo(index) + '.*';
|
||||
var query = index === 0 ? '*' : getSegmentPathUpTo(index) + '.*';
|
||||
|
||||
return $scope.datasource.metricFindQuery($scope.filter, query)
|
||||
return $scope.datasource.metricFindQuery(query)
|
||||
.then(function(segments) {
|
||||
_.each(segments, function(segment) {
|
||||
segment.html = segment.val = segment.text;
|
||||
$scope.altSegments = _.map(segments, function(segment) {
|
||||
return new MetricSegment({ value: segment.text, expandable: segment.expandable });
|
||||
});
|
||||
|
||||
_.each($scope.filter.templateParameters, function(templateParameter) {
|
||||
segments.unshift({
|
||||
if ($scope.altSegments.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// add template variables
|
||||
_.each(templateSrv.variables, function(variable) {
|
||||
$scope.altSegments.unshift(new MetricSegment({
|
||||
type: 'template',
|
||||
html: '[[' + templateParameter.name + ']]',
|
||||
val: '[[' + templateParameter.name + ']]',
|
||||
value: '$' + variable.name,
|
||||
expandable: true,
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
segments.unshift({val: '*', html: '<i class="icon-asterisk"></i>', expandable: true });
|
||||
$scope.altSegments = segments;
|
||||
// add wildcard option
|
||||
$scope.altSegments.unshift(new MetricSegment('*'));
|
||||
})
|
||||
.then(null, function(err) {
|
||||
$scope.parserError = err.message || 'Failed to issue metric query';
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setSegment = function (altIndex, segmentIndex) {
|
||||
$scope.segmentValueChanged = function (segment, segmentIndex) {
|
||||
delete $scope.parserError;
|
||||
|
||||
$scope.segments[segmentIndex].val = $scope.altSegments[altIndex].val;
|
||||
$scope.segments[segmentIndex].html = $scope.altSegments[altIndex].html;
|
||||
if ($scope.functions.length > 0 && $scope.functions[0].def.fake) {
|
||||
$scope.functions = [];
|
||||
}
|
||||
|
||||
if ($scope.altSegments[altIndex].expandable) {
|
||||
if (segment.expandable) {
|
||||
return checkOtherSegments(segmentIndex + 1)
|
||||
.then(function () {
|
||||
setSegmentFocus(segmentIndex + 1);
|
||||
@@ -205,7 +207,7 @@ function (angular, _, config, gfunc, Parser) {
|
||||
|
||||
$scope.targetTextChanged = function() {
|
||||
parseTarget();
|
||||
$scope.$parent.get_data();
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.targetChanged = function() {
|
||||
@@ -229,13 +231,17 @@ function (angular, _, config, gfunc, Parser) {
|
||||
};
|
||||
|
||||
$scope.addFunction = function(funcDef) {
|
||||
var newFunc = gfunc.createFuncInstance(funcDef);
|
||||
var newFunc = gfunc.createFuncInstance(funcDef, { withDefaultParams: true });
|
||||
newFunc.added = true;
|
||||
$scope.functions.push(newFunc);
|
||||
|
||||
$scope.moveAliasFuncLast();
|
||||
$scope.smartlyHandleNewAliasByNode(newFunc);
|
||||
|
||||
if ($scope.segments.length === 1 && $scope.segments[0].value === 'select metric') {
|
||||
$scope.segments = [];
|
||||
}
|
||||
|
||||
if (!newFunc.params.length && newFunc.added) {
|
||||
$scope.targetChanged();
|
||||
}
|
||||
@@ -259,7 +265,7 @@ function (angular, _, config, gfunc, Parser) {
|
||||
return;
|
||||
}
|
||||
for(var i = 0; i < $scope.segments.length; i++) {
|
||||
if ($scope.segments[i].val.indexOf('*') >= 0) {
|
||||
if ($scope.segments[i].value.indexOf('*') >= 0) {
|
||||
func.params[0] = i;
|
||||
func.added = false;
|
||||
$scope.targetChanged();
|
||||
@@ -268,11 +274,42 @@ function (angular, _, config, gfunc, Parser) {
|
||||
}
|
||||
};
|
||||
|
||||
$scope.toggleMetricOptions = function() {
|
||||
$scope.panel.metricOptionsEnabled = !$scope.panel.metricOptionsEnabled;
|
||||
if (!$scope.panel.metricOptionsEnabled) {
|
||||
delete $scope.panel.cacheTimeout;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.moveMetricQuery = function(fromIndex, toIndex) {
|
||||
_.move($scope.panel.targets, fromIndex, toIndex);
|
||||
};
|
||||
|
||||
$scope.duplicate = function() {
|
||||
var clone = angular.copy($scope.target);
|
||||
$scope.panel.targets.push(clone);
|
||||
};
|
||||
|
||||
function MetricSegment(options) {
|
||||
if (options === '*' || options.value === '*') {
|
||||
this.value = '*';
|
||||
this.html = $sce.trustAsHtml('<i class="icon-asterisk"><i>');
|
||||
this.expandable = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_.isString(options)) {
|
||||
this.value = options;
|
||||
this.html = $sce.trustAsHtml(this.value);
|
||||
return;
|
||||
}
|
||||
|
||||
this.value = options.value;
|
||||
this.type = options.type;
|
||||
this.expandable = options.expandable;
|
||||
this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.directive('focusMe', function($timeout, $parse) {
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
define([
|
||||
'angular'
|
||||
'angular',
|
||||
'lodash'
|
||||
],
|
||||
function (angular) {
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.controllers');
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
var seriesList = null;
|
||||
|
||||
module.controller('InfluxTargetCtrl', function($scope, $timeout) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.target.function = $scope.target.function || 'mean';
|
||||
$scope.target.column = $scope.target.column || 'value';
|
||||
var target = $scope.target;
|
||||
|
||||
target.function = target.function || 'mean';
|
||||
target.column = target.column || 'value';
|
||||
|
||||
// backward compatible correction of schema
|
||||
if (target.condition_value) {
|
||||
target.condition = target.condition_key + ' ' + target.condition_op + ' ' + target.condition_value;
|
||||
delete target.condition_key;
|
||||
delete target.condition_op;
|
||||
delete target.condition_value;
|
||||
}
|
||||
|
||||
if (target.groupby_field_add === false) {
|
||||
target.groupby_field = '';
|
||||
delete target.groupby_field_add;
|
||||
}
|
||||
|
||||
$scope.rawQuery = false;
|
||||
|
||||
@@ -24,7 +40,7 @@ function (angular) {
|
||||
];
|
||||
|
||||
$scope.operators = ['=', '=~', '>', '<', '!~', '<>'];
|
||||
$scope.oldSeries = $scope.target.series;
|
||||
$scope.oldSeries = target.series;
|
||||
$scope.$on('typeahead-updated', function() {
|
||||
$timeout($scope.get_data);
|
||||
});
|
||||
@@ -42,6 +58,7 @@ function (angular) {
|
||||
$scope.seriesBlur = function() {
|
||||
if ($scope.oldSeries !== $scope.target.series) {
|
||||
$scope.oldSeries = $scope.target.series;
|
||||
$scope.columnList = null;
|
||||
$scope.get_data();
|
||||
}
|
||||
};
|
||||
@@ -67,10 +84,11 @@ function (angular) {
|
||||
};
|
||||
|
||||
$scope.listSeries = function(query, callback) {
|
||||
if (!seriesList) {
|
||||
if (query !== '') {
|
||||
seriesList = [];
|
||||
$scope.datasource.listSeries().then(function(series) {
|
||||
$scope.datasource.listSeries(query).then(function(series) {
|
||||
seriesList = series;
|
||||
console.log(series);
|
||||
callback(seriesList);
|
||||
});
|
||||
}
|
||||
@@ -79,6 +97,10 @@ function (angular) {
|
||||
}
|
||||
};
|
||||
|
||||
$scope.moveMetricQuery = function(fromIndex, toIndex) {
|
||||
_.move($scope.panel.targets, fromIndex, toIndex);
|
||||
};
|
||||
|
||||
$scope.duplicate = function() {
|
||||
var clone = angular.copy($scope.target);
|
||||
$scope.panel.targets.push(clone);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
define([
|
||||
'angular'
|
||||
'angular',
|
||||
'lodash'
|
||||
],
|
||||
function (angular) {
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.controllers');
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('InspectCtrl', function($scope) {
|
||||
var model = $scope.inspector;
|
||||
@@ -28,6 +29,16 @@ function (angular) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_.isString(model.error.data)) {
|
||||
$scope.response = model.error.data;
|
||||
}
|
||||
|
||||
if (model.error.config && model.error.config.params) {
|
||||
$scope.request_parameters = _.map(model.error.config.params, function(value, key) {
|
||||
return { key: key, value: value};
|
||||
});
|
||||
}
|
||||
|
||||
if (model.error.stack) {
|
||||
$scope.editor.index = 2;
|
||||
$scope.stack_trace = model.error.stack;
|
||||
@@ -47,7 +58,7 @@ function (angular) {
|
||||
});
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.module('grafana.directives')
|
||||
.directive('iframeContent', function($parse) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
@@ -72,4 +83,4 @@ function (angular) {
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
22
src/app/controllers/jsonEditorCtrl.js
Normal file
22
src/app/controllers/jsonEditorCtrl.js
Normal file
@@ -0,0 +1,22 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash'
|
||||
],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('JsonEditorCtrl', function($scope) {
|
||||
|
||||
$scope.json = angular.toJson($scope.object, true);
|
||||
$scope.canUpdate = $scope.updateHandler !== void 0;
|
||||
|
||||
$scope.update = function () {
|
||||
var newObject = angular.fromJson($scope.json);
|
||||
$scope.updateHandler(newObject, $scope.object);
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
define([
|
||||
'angular',
|
||||
'underscore',
|
||||
'lodash',
|
||||
'config'
|
||||
],
|
||||
function (angular, _, config) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.controllers');
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('MetricKeysCtrl', function($scope, $http, $q) {
|
||||
var elasticSearchUrlForMetricIndex = config.elasticsearch + '/' + config.grafana_metrics_index + '/';
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
define([
|
||||
'angular',
|
||||
'underscore',
|
||||
'lodash',
|
||||
'kbn'
|
||||
],
|
||||
function (angular, _, kbn) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.controllers');
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('OpenTSDBTargetCtrl', function($scope, $timeout) {
|
||||
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
define([
|
||||
'angular',
|
||||
'underscore',
|
||||
'jquery'
|
||||
],
|
||||
function (angular, _, $) {
|
||||
'use strict';
|
||||
|
||||
// This function needs $inject annotations, update below
|
||||
// when changing arguments to this function
|
||||
function PanelBaseCtrl($scope, $rootScope, $timeout) {
|
||||
|
||||
var menu = [
|
||||
{
|
||||
text: 'Edit',
|
||||
configModal: "app/partials/paneleditor.html",
|
||||
condition: !$scope.panelMeta.fullscreenEdit
|
||||
},
|
||||
{
|
||||
text: 'Edit',
|
||||
click: "toggleFullscreenEdit()",
|
||||
condition: $scope.panelMeta.fullscreenEdit
|
||||
},
|
||||
{
|
||||
text: "Fullscreen",
|
||||
click: 'toggleFullscreen()',
|
||||
condition: $scope.panelMeta.fullscreenView
|
||||
},
|
||||
{
|
||||
text: 'Duplicate',
|
||||
click: 'duplicatePanel(panel)',
|
||||
condition: true
|
||||
},
|
||||
{
|
||||
text: 'Span',
|
||||
submenu: [
|
||||
{ text: '1', click: 'updateColumnSpan(1)' },
|
||||
{ text: '2', click: 'updateColumnSpan(2)' },
|
||||
{ text: '3', click: 'updateColumnSpan(3)' },
|
||||
{ text: '4', click: 'updateColumnSpan(4)' },
|
||||
{ text: '5', click: 'updateColumnSpan(5)' },
|
||||
{ text: '6', click: 'updateColumnSpan(6)' },
|
||||
{ text: '7', click: 'updateColumnSpan(7)' },
|
||||
{ text: '8', click: 'updateColumnSpan(8)' },
|
||||
{ text: '9', click: 'updateColumnSpan(9)' },
|
||||
{ text: '10', click: 'updateColumnSpan(10)' },
|
||||
{ text: '11', click: 'updateColumnSpan(11)' },
|
||||
{ text: '12', click: 'updateColumnSpan(12)' },
|
||||
],
|
||||
condition: true
|
||||
},
|
||||
{
|
||||
text: 'Remove',
|
||||
click: 'remove_panel_from_row(row, panel)',
|
||||
condition: true
|
||||
}
|
||||
];
|
||||
|
||||
$scope.inspector = {};
|
||||
$scope.panelMeta.menu = _.where(menu, { condition: true });
|
||||
|
||||
$scope.updateColumnSpan = function(span) {
|
||||
$scope.panel.span = span;
|
||||
|
||||
$timeout(function() {
|
||||
$scope.$emit('render');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.enterFullscreenMode = function(options) {
|
||||
var docHeight = $(window).height();
|
||||
var editHeight = Math.floor(docHeight * 0.3);
|
||||
var fullscreenHeight = Math.floor(docHeight * 0.7);
|
||||
var oldTimeRange = $scope.range;
|
||||
|
||||
$scope.height = options.edit ? editHeight : fullscreenHeight;
|
||||
$scope.editMode = options.edit;
|
||||
|
||||
if (!$scope.fullscreen) {
|
||||
var closeEditMode = $rootScope.$on('panel-fullscreen-exit', function() {
|
||||
$scope.editMode = false;
|
||||
$scope.fullscreen = false;
|
||||
delete $scope.height;
|
||||
|
||||
closeEditMode();
|
||||
|
||||
$timeout(function() {
|
||||
if (oldTimeRange !== $scope.range) {
|
||||
$scope.dashboard.refresh();
|
||||
}
|
||||
else {
|
||||
$scope.$emit('render');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(window).scrollTop(0);
|
||||
|
||||
$scope.fullscreen = true;
|
||||
|
||||
$rootScope.$emit('panel-fullscreen-enter');
|
||||
|
||||
$timeout(function() {
|
||||
$scope.$emit('render');
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
$scope.toggleFullscreenEdit = function() {
|
||||
if ($scope.editMode) {
|
||||
$rootScope.$emit('panel-fullscreen-exit');
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.enterFullscreenMode({edit: true});
|
||||
};
|
||||
|
||||
$scope.toggleFullscreen = function() {
|
||||
if ($scope.fullscreen && !$scope.editMode) {
|
||||
$rootScope.$emit('panel-fullscreen-exit');
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.enterFullscreenMode({ edit: false });
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
PanelBaseCtrl['$inject'] = ['$scope', '$rootScope', '$timeout'];
|
||||
|
||||
return PanelBaseCtrl;
|
||||
|
||||
});
|
||||
@@ -1,19 +1,18 @@
|
||||
define([
|
||||
'angular',
|
||||
'underscore',
|
||||
'lodash',
|
||||
'config'
|
||||
],
|
||||
function (angular, _, config) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.controllers');
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('PlaylistCtrl', function($scope, playlistSrv) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.timespan = config.playlist_timespan;
|
||||
$scope.loadFavorites();
|
||||
$scope.$on('modal-opened', $scope.loadFavorites);
|
||||
};
|
||||
|
||||
$scope.loadFavorites = function() {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore'
|
||||
'lodash'
|
||||
],
|
||||
function (angular, app, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.controllers');
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('PulldownCtrl', function($scope, $rootScope, $timeout) {
|
||||
var _d = {
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore'
|
||||
'lodash'
|
||||
],
|
||||
function (angular, app, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.controllers');
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('RowCtrl', function($scope, $rootScope, $timeout) {
|
||||
var _d = {
|
||||
title: "Row",
|
||||
height: "150px",
|
||||
collapse: false,
|
||||
collapsable: true,
|
||||
editable: true,
|
||||
panels: [],
|
||||
notice: false
|
||||
};
|
||||
|
||||
_.defaults($scope.row,_d);
|
||||
@@ -25,63 +23,41 @@ function (angular, app, _) {
|
||||
$scope.reset_panel();
|
||||
};
|
||||
|
||||
$scope.togglePanelMenu = function(posX) {
|
||||
$scope.showPanelMenu = !$scope.showPanelMenu;
|
||||
$scope.panelMenuPos = posX;
|
||||
};
|
||||
|
||||
$scope.toggle_row = function(row) {
|
||||
if(!row.collapsable) {
|
||||
return;
|
||||
}
|
||||
row.collapse = row.collapse ? false : true;
|
||||
if (!row.collapse) {
|
||||
$timeout(function() {
|
||||
$scope.$broadcast('render');
|
||||
});
|
||||
} else {
|
||||
row.notice = false;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.rowSpan = function(row) {
|
||||
var panels = _.filter(row.panels, function(p) {
|
||||
return $scope.isPanel(p);
|
||||
});
|
||||
return _.reduce(_.pluck(panels,'span'), function(p,v) {
|
||||
return p+v;
|
||||
},0);
|
||||
};
|
||||
|
||||
// This can be overridden by individual panels
|
||||
$scope.close_edit = function() {
|
||||
$scope.$broadcast('render');
|
||||
};
|
||||
|
||||
$scope.add_panel = function(panel) {
|
||||
var rowSpan = $scope.rowSpan($scope.row);
|
||||
var panelCount = $scope.row.panels.length;
|
||||
var space = (12 - rowSpan) - panel.span;
|
||||
|
||||
// try to make room of there is no space left
|
||||
if (space <= 0) {
|
||||
if (panelCount === 1) {
|
||||
$scope.row.panels[0].span = 6;
|
||||
panel.span = 6;
|
||||
}
|
||||
else if (panelCount === 2) {
|
||||
$scope.row.panels[0].span = 4;
|
||||
$scope.row.panels[1].span = 4;
|
||||
panel.span = 4;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.row.panels.push(panel);
|
||||
$scope.dashboard.add_panel(panel, $scope.row);
|
||||
};
|
||||
|
||||
$scope.delete_row = function() {
|
||||
if (confirm("Are you sure you want to delete this row?")) {
|
||||
$scope.dashboard.current.rows = _.without($scope.dashboard.current.rows, $scope.row);
|
||||
}
|
||||
$scope.appEvent('confirm-modal', {
|
||||
title: 'Delete row',
|
||||
text: 'Are you sure you want to delete this row?',
|
||||
onConfirm: function() {
|
||||
$scope.dashboard.rows = _.without($scope.dashboard.rows, $scope.row);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.move_row = function(direction) {
|
||||
var rowsList = $scope.dashboard.current.rows;
|
||||
var rowsList = $scope.dashboard.rows;
|
||||
var rowIndex = _.indexOf(rowsList, $scope.row);
|
||||
var newIndex = rowIndex + direction;
|
||||
if (newIndex >= 0 && newIndex <= (rowsList.length - 1)) {
|
||||
@@ -104,70 +80,37 @@ function (angular, app, _) {
|
||||
};
|
||||
|
||||
$scope.remove_panel_from_row = function(row, panel) {
|
||||
if (confirm('Are you sure you want to remove this ' + panel.type + ' panel?')) {
|
||||
row.panels = _.without(row.panels,panel);
|
||||
}
|
||||
$scope.appEvent('confirm-modal', {
|
||||
title: 'Remove panel',
|
||||
text: 'Are you sure you want to remove this panel?',
|
||||
onConfirm: function() {
|
||||
row.panels = _.without(row.panels, panel);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.duplicatePanel = function(panel, row) {
|
||||
row = row || $scope.row;
|
||||
var currentRowSpan = $scope.rowSpan(row);
|
||||
if (currentRowSpan <= 9) {
|
||||
row.panels.push(angular.copy(panel));
|
||||
}
|
||||
else {
|
||||
var rowsList = $scope.dashboard.current.rows;
|
||||
var rowIndex = _.indexOf(rowsList, row);
|
||||
if (rowIndex === rowsList.length - 1) {
|
||||
var newRow = angular.copy($scope.row);
|
||||
newRow.panels = [];
|
||||
$scope.dashboard.current.rows.push(newRow);
|
||||
$scope.duplicatePanel(panel, newRow);
|
||||
}
|
||||
else {
|
||||
$scope.duplicatePanel(panel, rowsList[rowIndex+1]);
|
||||
}
|
||||
}
|
||||
};
|
||||
$scope.replacePanel = function(newPanel, oldPanel) {
|
||||
var row = $scope.row;
|
||||
var index = _.indexOf(row.panels, oldPanel);
|
||||
row.panels.splice(index, 1);
|
||||
|
||||
/** @scratch /panels/0
|
||||
* [[panels]]
|
||||
* = Panels
|
||||
*
|
||||
* [partintro]
|
||||
* --
|
||||
* *Kibana* dashboards are made up of blocks called +panels+. Panels are organized into rows
|
||||
* and can serve many purposes, though most are designed to provide the results of a query or
|
||||
* multiple queries as a visualization. Other panels may show collections of documents or
|
||||
* allow you to insert instructions for your users.
|
||||
*
|
||||
* Panels can be configured easily via the Kibana web interface. For more advanced usage, such
|
||||
* as templated or scripted dashboards, documentation of panel properties is available in this
|
||||
* section. You may find settings here which are not exposed via the web interface.
|
||||
*
|
||||
* Each panel type has its own properties, hover there are several that are shared.
|
||||
*
|
||||
*/
|
||||
// adding it back needs to be done in next digest
|
||||
$timeout(function() {
|
||||
newPanel.id = oldPanel.id;
|
||||
newPanel.span = oldPanel.span;
|
||||
row.panels.splice(index, 0, newPanel);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.reset_panel = function(type) {
|
||||
var
|
||||
defaultSpan = 4,
|
||||
_as = 12-$scope.rowSpan($scope.row);
|
||||
var defaultSpan = 12;
|
||||
var _as = 12 - $scope.dashboard.rowSpan($scope.row);
|
||||
|
||||
$scope.panel = {
|
||||
title: 'no title (click here)',
|
||||
error : false,
|
||||
/** @scratch /panels/1
|
||||
* span:: A number, 1-12, that describes the width of the panel.
|
||||
*/
|
||||
span : _as < defaultSpan && _as > 0 ? _as : defaultSpan,
|
||||
/** @scratch /panels/1
|
||||
* editable:: Enable or disable the edit button the the panel
|
||||
*/
|
||||
editable: true,
|
||||
/** @scratch /panels/1
|
||||
* type:: The type of panel this object contains. Each panel type will require additional
|
||||
* properties. See the panel types list to the right.
|
||||
*/
|
||||
type : type
|
||||
};
|
||||
|
||||
@@ -184,12 +127,42 @@ function (angular, app, _) {
|
||||
$scope.row.height = fixRowHeight($scope.row.height);
|
||||
};
|
||||
|
||||
/** @scratch /panels/2
|
||||
* --
|
||||
*/
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
module.directive('rowHeight', function() {
|
||||
return function(scope, element) {
|
||||
scope.$watchGroup(['row.collapse', 'row.height'], function() {
|
||||
element[0].style.minHeight = scope.row.collapse ? '5px' : scope.row.height;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
module.directive('panelWidth', function() {
|
||||
return function(scope, element) {
|
||||
scope.$watch('panel.span', function() {
|
||||
element[0].style.width = ((scope.panel.span / 1.2) * 10) + '%';
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
module.directive('panelDropZone', function() {
|
||||
return function(scope, element) {
|
||||
scope.$on("ANGULAR_DRAG_START", function() {
|
||||
var dropZoneSpan = 12 - scope.dashboard.rowSpan(scope.row);
|
||||
|
||||
if (dropZoneSpan > 0) {
|
||||
element.find('.panel-container').css('height', scope.row.height);
|
||||
element[0].style.width = ((dropZoneSpan / 1.2) * 10) + '%';
|
||||
element.show();
|
||||
}
|
||||
});
|
||||
|
||||
scope.$on("ANGULAR_DRAG_END", function() {
|
||||
element.hide();
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,33 +1,44 @@
|
||||
define([
|
||||
'angular',
|
||||
'underscore',
|
||||
'lodash',
|
||||
'config',
|
||||
'jquery'
|
||||
],
|
||||
function (angular, _, config, $) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.controllers');
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('SearchCtrl', function($scope, $rootScope, dashboard, $element, $location) {
|
||||
module.controller('SearchCtrl', function($scope, $rootScope, $element, $location, datasourceSrv, $timeout) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.giveSearchFocus = 0;
|
||||
$scope.selectedIndex = -1;
|
||||
$scope.results = {dashboards: [], tags: [], metrics: []};
|
||||
$scope.query = { query: 'title:' };
|
||||
$rootScope.$on('open-search', $scope.openSearch);
|
||||
$scope.db = datasourceSrv.getGrafanaDB();
|
||||
$scope.currentSearchId = 0;
|
||||
|
||||
// events
|
||||
$scope.onAppEvent('dashboard-deleted', $scope.dashboardDeleted);
|
||||
|
||||
$timeout(function() {
|
||||
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
|
||||
$scope.query.query = 'title:';
|
||||
$scope.search();
|
||||
}, 100);
|
||||
|
||||
};
|
||||
|
||||
$scope.keyDown = function (evt) {
|
||||
if (evt.keyCode === 27) {
|
||||
$element.find('.dropdown-toggle').dropdown('toggle');
|
||||
$scope.appEvent('hide-dash-editor');
|
||||
}
|
||||
if (evt.keyCode === 40) {
|
||||
$scope.selectedIndex++;
|
||||
$scope.moveSelection(1);
|
||||
}
|
||||
if (evt.keyCode === 38) {
|
||||
$scope.selectedIndex--;
|
||||
$scope.moveSelection(-1);
|
||||
}
|
||||
if (evt.keyCode === 13) {
|
||||
if ($scope.tagsOnly) {
|
||||
@@ -40,7 +51,8 @@ function (angular, _, config, $) {
|
||||
|
||||
var selectedDash = $scope.results.dashboards[$scope.selectedIndex];
|
||||
if (selectedDash) {
|
||||
$location.path("/dashboard/elasticsearch/" + encodeURIComponent(selectedDash._id));
|
||||
$location.search({});
|
||||
$location.path("/dashboard/db/" + selectedDash.id);
|
||||
setTimeout(function() {
|
||||
$('body').click(); // hack to force dropdown to close;
|
||||
});
|
||||
@@ -48,39 +60,42 @@ function (angular, _, config, $) {
|
||||
}
|
||||
};
|
||||
|
||||
$scope.searchDasboards = function(query) {
|
||||
var request = $scope.ejs.Request().indices(config.grafana_index).types('dashboard');
|
||||
var tagsOnly = query.indexOf('tags!:') === 0;
|
||||
if (tagsOnly) {
|
||||
var tagsQuery = query.substring(6, query.length);
|
||||
query = 'tags:' + tagsQuery + '*';
|
||||
}
|
||||
else {
|
||||
if (query.length === 0) {
|
||||
query = 'title:';
|
||||
}
|
||||
$scope.moveSelection = function(direction) {
|
||||
$scope.selectedIndex = Math.max(Math.min($scope.selectedIndex + direction, $scope.resultCount - 1), 0);
|
||||
};
|
||||
|
||||
if (query[query.length - 1] !== '*') {
|
||||
query += '*';
|
||||
}
|
||||
}
|
||||
$scope.goToDashboard = function(id) {
|
||||
$location.search({});
|
||||
$location.path("/dashboard/db/" + id);
|
||||
};
|
||||
|
||||
return request
|
||||
.query($scope.ejs.QueryStringQuery(query))
|
||||
.sort('_uid')
|
||||
.facet($scope.ejs.TermsFacet("tags").field("tags").order('term').size(50))
|
||||
.size(20).doSearch()
|
||||
$scope.shareDashboard = function(title, id, $event) {
|
||||
$event.stopPropagation();
|
||||
var baseUrl = window.location.href.replace(window.location.hash,'');
|
||||
|
||||
$scope.share = {
|
||||
title: title,
|
||||
url: baseUrl + '#dashboard/db/' + encodeURIComponent(id)
|
||||
};
|
||||
};
|
||||
|
||||
$scope.searchDashboards = function(queryString) {
|
||||
// bookeeping for determining stale search requests
|
||||
var searchId = $scope.currentSearchId + 1;
|
||||
$scope.currentSearchId = searchId > $scope.currentSearchId ? searchId : $scope.currentSearchId;
|
||||
|
||||
return $scope.db.searchDashboards(queryString)
|
||||
.then(function(results) {
|
||||
|
||||
if(_.isUndefined(results.hits)) {
|
||||
$scope.results.dashboards = [];
|
||||
$scope.results.tags = [];
|
||||
// since searches are async, it's possible that these results are not for the latest search. throw
|
||||
// them away if so
|
||||
if (searchId < $scope.currentSearchId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.tagsOnly = tagsOnly;
|
||||
$scope.results.dashboards = results.hits.hits;
|
||||
$scope.results.tags = results.facets.tags.terms;
|
||||
$scope.tagsOnly = results.tagsOnly;
|
||||
$scope.results.dashboards = results.dashboards;
|
||||
$scope.results.tags = results.tags;
|
||||
$scope.resultCount = results.tagsOnly ? results.tags.length : results.dashboards.length;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -94,8 +109,7 @@ function (angular, _, config, $) {
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showTags = function(evt) {
|
||||
evt.stopPropagation();
|
||||
$scope.showTags = function() {
|
||||
$scope.tagsOnly = !$scope.tagsOnly;
|
||||
$scope.query.query = $scope.tagsOnly ? "tags!:" : "";
|
||||
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
|
||||
@@ -105,55 +119,22 @@ function (angular, _, config, $) {
|
||||
|
||||
$scope.search = function() {
|
||||
$scope.showImport = false;
|
||||
$scope.selectedIndex = -1;
|
||||
|
||||
var queryStr = $scope.query.query.toLowerCase();
|
||||
|
||||
if (queryStr.indexOf('m:') !== 0) {
|
||||
queryStr = queryStr.replace(' and ', ' AND ');
|
||||
$scope.searchDasboards(queryStr);
|
||||
return;
|
||||
}
|
||||
|
||||
queryStr = queryStr.substring(2, queryStr.length);
|
||||
|
||||
var words = queryStr.split(' ');
|
||||
var query = $scope.ejs.BoolQuery();
|
||||
var terms = _.map(words, function(word) {
|
||||
return $scope.ejs.MatchQuery('metricPath_ng', word).boost(1.2);
|
||||
});
|
||||
|
||||
var ngramQuery = $scope.ejs.BoolQuery();
|
||||
ngramQuery.must(terms);
|
||||
|
||||
var fieldMatchQuery = $scope.ejs.FieldQuery('metricPath', queryStr + "*").boost(1.2);
|
||||
query.should([ngramQuery, fieldMatchQuery]);
|
||||
|
||||
var request = $scope.ejs.Request().indices(config.grafana_index).types('metricKey');
|
||||
var results = request.query(query).size(20).doSearch();
|
||||
|
||||
results.then(function(results) {
|
||||
if (results && results.hits && results.hits.hits.length > 0) {
|
||||
$scope.results.metrics = { metrics: results.hits.hits };
|
||||
}
|
||||
else {
|
||||
$scope.results.metrics = { metric: [] };
|
||||
}
|
||||
});
|
||||
$scope.selectedIndex = 0;
|
||||
$scope.searchDashboards($scope.query.query);
|
||||
};
|
||||
|
||||
$scope.openSearch = function (evt) {
|
||||
if (evt) {
|
||||
$element.find('.dropdown-toggle').dropdown('toggle');
|
||||
}
|
||||
$scope.deleteDashboard = function(dash, evt) {
|
||||
evt.stopPropagation();
|
||||
$scope.appEvent('delete-dashboard', { id: dash.id, title: dash.title });
|
||||
};
|
||||
|
||||
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
|
||||
$scope.query.query = 'title:';
|
||||
$scope.search();
|
||||
$scope.dashboardDeleted = function(evt, id) {
|
||||
var dash = _.findWhere($scope.results.dashboards, {id: id});
|
||||
$scope.results.dashboards = _.without($scope.results.dashboards, dash);
|
||||
};
|
||||
|
||||
$scope.addMetricToCurrentDashboard = function (metricId) {
|
||||
dashboard.current.rows.push({
|
||||
$scope.dashboard.rows.push({
|
||||
title: '',
|
||||
height: '250px',
|
||||
editable: true,
|
||||
@@ -168,8 +149,7 @@ function (angular, _, config, $) {
|
||||
});
|
||||
};
|
||||
|
||||
$scope.toggleImport = function ($event) {
|
||||
$event.stopPropagation();
|
||||
$scope.toggleImport = function () {
|
||||
$scope.showImport = !$scope.showImport;
|
||||
};
|
||||
|
||||
@@ -181,16 +161,48 @@ function (angular, _, config, $) {
|
||||
|
||||
module.directive('xngFocus', function() {
|
||||
return function(scope, element, attrs) {
|
||||
$(element).click(function(e) {
|
||||
element.click(function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
scope.$watch(attrs.xngFocus,function (newValue) {
|
||||
if (!newValue) {
|
||||
return;
|
||||
}
|
||||
setTimeout(function() {
|
||||
newValue && element.focus();
|
||||
element.focus();
|
||||
var pos = element.val().length * 2;
|
||||
element[0].setSelectionRange(pos, pos);
|
||||
}, 200);
|
||||
},true);
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
module.directive('tagColorFromName', function() {
|
||||
|
||||
function djb2(str) {
|
||||
var hash = 5381;
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
hash = ((hash << 5) + hash) + str.charCodeAt(i); /* hash * 33 + c */
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
return function (scope, element) {
|
||||
var name = _.isString(scope.tag) ? scope.tag : scope.tag.term;
|
||||
var hash = djb2(name.toLowerCase());
|
||||
var colors = [
|
||||
"#E24D42","#1F78C1","#BA43A9","#705DA0","#466803",
|
||||
"#508642","#447EBC","#C15C17","#890F02","#757575",
|
||||
"#0A437C","#6D1F62","#584477","#629E51","#2F4F4F",
|
||||
"#BF1B00","#806EB7","#8a2eb8", "#699e00","#000000",
|
||||
"#3F6833","#2F575E","#99440A","#E0752D","#0E4AB4",
|
||||
"#58140C","#052B51","#511749","#3F2B5B",
|
||||
];
|
||||
var color = colors[Math.abs(hash % colors.length)];
|
||||
element.css("background-color", color);
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
85
src/app/controllers/sharePanelCtrl.js
Normal file
85
src/app/controllers/sharePanelCtrl.js
Normal file
@@ -0,0 +1,85 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash'
|
||||
],
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('SharePanelCtrl', function($scope, $location, $timeout, timeSrv, $element, templateSrv) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.editor = { index: 0 };
|
||||
$scope.forCurrent = true;
|
||||
$scope.toPanel = true;
|
||||
$scope.includeTemplateVars = true;
|
||||
|
||||
$scope.buildUrl();
|
||||
};
|
||||
|
||||
$scope.buildUrl = function() {
|
||||
var baseUrl = $location.absUrl();
|
||||
var queryStart = baseUrl.indexOf('?');
|
||||
|
||||
if (queryStart !== -1) {
|
||||
baseUrl = baseUrl.substring(0, queryStart);
|
||||
}
|
||||
|
||||
var panelId = $scope.panel.id;
|
||||
var params = angular.copy($location.search());
|
||||
|
||||
var range = timeSrv.timeRangeForUrl();
|
||||
params.from = range.from;
|
||||
params.to = range.to;
|
||||
|
||||
if ($scope.includeTemplateVars) {
|
||||
_.each(templateSrv.variables, function(variable) {
|
||||
params['var-' + variable.name] = variable.current.text;
|
||||
});
|
||||
}
|
||||
else {
|
||||
_.each(templateSrv.variables, function(variable) {
|
||||
delete params['var-' + variable.name];
|
||||
});
|
||||
}
|
||||
|
||||
if (!$scope.forCurrent) {
|
||||
delete params.from;
|
||||
delete params.to;
|
||||
}
|
||||
|
||||
if ($scope.toPanel) {
|
||||
params.panelId = panelId;
|
||||
params.fullscreen = true;
|
||||
} else {
|
||||
delete params.panelId;
|
||||
delete params.fullscreen;
|
||||
}
|
||||
|
||||
var paramsArray = [];
|
||||
_.each(params, function(value, key) {
|
||||
if (value === null) { return; }
|
||||
if (value === true) {
|
||||
paramsArray.push(key);
|
||||
} else {
|
||||
key += '=' + encodeURIComponent(value);
|
||||
paramsArray.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.shareUrl = baseUrl + "?" + paramsArray.join('&') ;
|
||||
|
||||
$timeout(function() {
|
||||
var input = $element.find('[data-share-panel-url]');
|
||||
input.focus();
|
||||
input.select();
|
||||
}, 10);
|
||||
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,14 +1,13 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore'
|
||||
'lodash'
|
||||
],
|
||||
function (angular, app, _) {
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.controllers');
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('SubmenuCtrl', function($scope) {
|
||||
module.controller('SubmenuCtrl', function($scope, $q, $rootScope, templateValuesSrv) {
|
||||
var _d = {
|
||||
enable: true
|
||||
};
|
||||
@@ -18,10 +17,20 @@ function (angular, app, _) {
|
||||
$scope.init = function() {
|
||||
$scope.panel = $scope.pulldown;
|
||||
$scope.row = $scope.pulldown;
|
||||
$scope.variables = $scope.dashboard.templating.list;
|
||||
};
|
||||
|
||||
$scope.disableAnnotation = function (annotation) {
|
||||
annotation.enable = !annotation.enable;
|
||||
$rootScope.$broadcast('refresh');
|
||||
};
|
||||
|
||||
$scope.setVariableValue = function(param, option) {
|
||||
templateValuesSrv.setVariableValue(param, option);
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
111
src/app/controllers/templateEditorCtrl.js
Normal file
111
src/app/controllers/templateEditorCtrl.js
Normal file
@@ -0,0 +1,111 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
],
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('TemplateEditorCtrl', function($scope, datasourceSrv, templateSrv, templateValuesSrv, alertSrv) {
|
||||
|
||||
var replacementDefaults = {
|
||||
type: 'query',
|
||||
datasource: null,
|
||||
refresh_on_load: false,
|
||||
name: '',
|
||||
options: [],
|
||||
includeAll: false,
|
||||
allFormat: 'glob',
|
||||
};
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.editor = { index: 0 };
|
||||
$scope.datasources = datasourceSrv.getMetricSources();
|
||||
$scope.variables = templateSrv.variables;
|
||||
$scope.reset();
|
||||
|
||||
$scope.$watch('editor.index', function(index) {
|
||||
if ($scope.currentIsNew === false && index === 1) {
|
||||
$scope.reset();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.add = function() {
|
||||
if ($scope.isValid()) {
|
||||
$scope.variables.push($scope.current);
|
||||
$scope.update();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.isValid = function() {
|
||||
if (!$scope.current.name) {
|
||||
$scope.appEvent('alert-warning', ['Validation', 'Template variable requires a name']);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$scope.current.name.match(/^\w+$/)) {
|
||||
$scope.appEvent('alert-warning', ['Validation', 'Only word and digit characters are allowed in variable names']);
|
||||
return false;
|
||||
}
|
||||
|
||||
var sameName = _.findWhere($scope.variables, { name: $scope.current.name });
|
||||
if (sameName && sameName !== $scope.current) {
|
||||
$scope.appEvent('alert-warning', ['Validation', 'Variable with the same name already exists']);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.runQuery = function() {
|
||||
return templateValuesSrv.updateOptions($scope.current).then(function() {
|
||||
}, function(err) {
|
||||
alertSrv.set('Templating', 'Failed to run query for variable values: ' + err.message, 'error');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.edit = function(variable) {
|
||||
$scope.current = variable;
|
||||
$scope.currentIsNew = false;
|
||||
$scope.editor.index = 2;
|
||||
|
||||
if ($scope.current.datasource === void 0) {
|
||||
$scope.current.datasource = null;
|
||||
$scope.current.type = 'query';
|
||||
$scope.current.allFormat = 'Glob';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.update = function() {
|
||||
if ($scope.isValid()) {
|
||||
$scope.runQuery().then(function() {
|
||||
$scope.reset();
|
||||
$scope.editor.index = 0;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.reset = function() {
|
||||
$scope.currentIsNew = true;
|
||||
$scope.current = angular.copy(replacementDefaults);
|
||||
};
|
||||
|
||||
$scope.typeChanged = function () {
|
||||
if ($scope.current.type === 'interval') {
|
||||
$scope.current.query = '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d';
|
||||
}
|
||||
if ($scope.current.type === 'query') {
|
||||
$scope.current.query = '';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removeVariable = function(variable) {
|
||||
var index = _.indexOf($scope.variables, variable);
|
||||
$scope.variables.splice(index, 1);
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,54 +1,82 @@
|
||||
{
|
||||
"title": "Welcome to Grafana!",
|
||||
"services": {
|
||||
"filter": {
|
||||
"list": [],
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Grafana",
|
||||
"tags": [],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"rows": [
|
||||
{
|
||||
"title": "Welcome to Grafana",
|
||||
"title": "New row",
|
||||
"height": "150px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": true,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"error": false,
|
||||
"id": 1,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"type": "text",
|
||||
"loadingEditor": false,
|
||||
"mode": "markdown",
|
||||
"content": "####Thank you for trying out Grafana! \n\nGeneral documentation is found in the readme and in the wiki section of the [Github Project](https://github.com/torkelo/grafana). If you encounter any problem or have an idea for an improvement do not hesitate to open a github issue. \n\nTips: \n\n- Ctrl+S saves the current dashboard\n- Ctrl+F Opens the dashboard finder (searches elastic search)\n- Ctrl+H Hide/show row controls \n- Click and drag graph title to move panel (only works when row controls are enabled)\n\nIf you do not see a graph in the panel bellow the browser cannot access your graphite installation. Please make sure that the graphiteUrl property in config.js is correctly set and accessible.",
|
||||
"mode": "html",
|
||||
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
|
||||
"style": {},
|
||||
"title": "Welcome to Grafana"
|
||||
"title": "Welcome to"
|
||||
}
|
||||
],
|
||||
"notice": false
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Welcome to Grafana",
|
||||
"height": "210px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 2,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
|
||||
"style": {},
|
||||
"title": "Documentation Links"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
|
||||
"style": {},
|
||||
"title": "Tips & Shortcuts"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "test",
|
||||
"height": "250px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 4,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"type": "graphite",
|
||||
"type": "graph",
|
||||
"x-axis": true,
|
||||
"y-axis": true,
|
||||
"scale": 1,
|
||||
"y_formats": ["short", "short"],
|
||||
"y_formats": [
|
||||
"short",
|
||||
"short"
|
||||
],
|
||||
"grid": {
|
||||
"max": null,
|
||||
"min": null
|
||||
"min": null,
|
||||
"leftMax": null,
|
||||
"rightMax": null,
|
||||
"leftMin": null,
|
||||
"rightMin": null,
|
||||
"threshold1": null,
|
||||
"threshold2": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||
},
|
||||
"resolution": 100,
|
||||
"lines": true,
|
||||
@@ -60,53 +88,48 @@
|
||||
"stack": true,
|
||||
"spyable": true,
|
||||
"options": false,
|
||||
"legend": true,
|
||||
"legend": {
|
||||
"show": true,
|
||||
"values": false,
|
||||
"min": false,
|
||||
"max": false,
|
||||
"current": false,
|
||||
"total": false,
|
||||
"avg": false
|
||||
},
|
||||
"interactive": true,
|
||||
"legend_counts": true,
|
||||
"timezone": "browser",
|
||||
"percentage": false,
|
||||
"zerofill": true,
|
||||
"nullPointMode": "connected",
|
||||
"steppedLine": false,
|
||||
"tooltip": {
|
||||
"value_type": "cumulative"
|
||||
"value_type": "cumulative",
|
||||
"query_as_alias": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"target": "randomWalk('random walk')"
|
||||
},
|
||||
{
|
||||
"target": "randomWalk('random walk2')"
|
||||
},
|
||||
{
|
||||
"target": "randomWalk('random walk3')"
|
||||
"target": "randomWalk('random walk')",
|
||||
"function": "mean",
|
||||
"column": "value"
|
||||
}
|
||||
],
|
||||
"aliasColors": {},
|
||||
"aliasYAxis": {},
|
||||
"title": "Graphite test"
|
||||
"title": "First Graph (click title to edit)",
|
||||
"datasource": "graphite",
|
||||
"renderer": "flot",
|
||||
"annotate": {
|
||||
"enable": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"notice": false
|
||||
}
|
||||
],
|
||||
"editable": true,
|
||||
"failover": false,
|
||||
"panel_hints": true,
|
||||
"style": "dark",
|
||||
"pulldowns": [
|
||||
{
|
||||
"type": "filtering",
|
||||
"collapse": false,
|
||||
"notice": false,
|
||||
"enable": false
|
||||
]
|
||||
}
|
||||
],
|
||||
"nav": [
|
||||
{
|
||||
"type": "timepicker",
|
||||
"collapse": false,
|
||||
"notice": false,
|
||||
"enable": true,
|
||||
"status": "Stable",
|
||||
"time_options": [
|
||||
@@ -135,19 +158,12 @@
|
||||
"now": true
|
||||
}
|
||||
],
|
||||
"loader": {
|
||||
"save_gist": false,
|
||||
"save_elasticsearch": true,
|
||||
"save_local": true,
|
||||
"save_default": true,
|
||||
"save_temp": true,
|
||||
"save_temp_ttl_enable": true,
|
||||
"save_temp_ttl": "30d",
|
||||
"load_gist": false,
|
||||
"load_elasticsearch": true,
|
||||
"load_elasticsearch_size": 20,
|
||||
"load_local": false,
|
||||
"hide": false
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"refresh": false
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"version": 5
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
{
|
||||
"title": "New Dashboard",
|
||||
"services": {
|
||||
"filter": {
|
||||
"list": [],
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
}
|
||||
}
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
@@ -15,28 +13,14 @@
|
||||
"height": "250px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": true,
|
||||
"panels": [],
|
||||
"notice": false
|
||||
"panels": []
|
||||
}
|
||||
],
|
||||
"editable": true,
|
||||
"failover": false,
|
||||
"panel_hints": true,
|
||||
"style": "dark",
|
||||
"pulldowns": [
|
||||
{
|
||||
"type": "filtering",
|
||||
"collapse": false,
|
||||
"notice": false,
|
||||
"enable": false
|
||||
}
|
||||
],
|
||||
"nav": [
|
||||
{
|
||||
"type": "timepicker",
|
||||
"collapse": false,
|
||||
"notice": false,
|
||||
"enable": true,
|
||||
"status": "Stable",
|
||||
"time_options": [
|
||||
@@ -65,19 +49,5 @@
|
||||
"now": true
|
||||
}
|
||||
],
|
||||
"loader": {
|
||||
"save_gist": false,
|
||||
"save_elasticsearch": true,
|
||||
"save_local": true,
|
||||
"save_default": true,
|
||||
"save_temp": true,
|
||||
"save_temp_ttl_enable": true,
|
||||
"save_temp_ttl": "30d",
|
||||
"load_gist": false,
|
||||
"load_elasticsearch": true,
|
||||
"load_elasticsearch_size": 20,
|
||||
"load_local": false,
|
||||
"hide": false
|
||||
},
|
||||
"refresh": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,27 +17,25 @@
|
||||
var window, document, ARGS, $, jQuery, moment, kbn;
|
||||
|
||||
// Setup some variables
|
||||
var dashboard, timspan;
|
||||
var dashboard;
|
||||
|
||||
// All url parameters are available via the ARGS object
|
||||
var ARGS;
|
||||
|
||||
// Set a default timespan if one isn't specified
|
||||
timspan = '1d';
|
||||
|
||||
// Intialize a skeleton with nothing but a rows array and service object
|
||||
dashboard = {
|
||||
rows : [],
|
||||
services : {}
|
||||
};
|
||||
|
||||
// Set a title
|
||||
dashboard.title = 'Scripted dash';
|
||||
dashboard.services.filter = {
|
||||
time: {
|
||||
from: "now-" + (ARGS.from || timspan),
|
||||
to: "now"
|
||||
}
|
||||
|
||||
// Set default time
|
||||
// time can be overriden in the url using from/to parameteres, but this is
|
||||
// handled automatically in grafana core during dashboard initialization
|
||||
dashboard.time = {
|
||||
from: "now-6h",
|
||||
to: "now"
|
||||
};
|
||||
|
||||
var rows = 1;
|
||||
@@ -59,7 +57,7 @@ for (var i = 0; i < rows; i++) {
|
||||
panels: [
|
||||
{
|
||||
title: 'Events',
|
||||
type: 'graphite',
|
||||
type: 'graph',
|
||||
span: 12,
|
||||
fill: 1,
|
||||
linewidth: 2,
|
||||
@@ -71,10 +69,21 @@ for (var i = 0; i < rows; i++) {
|
||||
'target': "randomWalk('random walk2')"
|
||||
}
|
||||
],
|
||||
seriesOverrides: [
|
||||
{
|
||||
alias: '/random/',
|
||||
yaxis: 2,
|
||||
fill: 0,
|
||||
linewidth: 5
|
||||
}
|
||||
],
|
||||
tooltip: {
|
||||
shared: true
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return dashboard;
|
||||
return dashboard;
|
||||
|
||||
@@ -22,10 +22,7 @@ var window, document, ARGS, $, jQuery, moment, kbn;
|
||||
return function(callback) {
|
||||
|
||||
// Setup some variables
|
||||
var dashboard, timspan;
|
||||
|
||||
// Set a default timespan if one isn't specified
|
||||
timspan = '1d';
|
||||
var dashboard;
|
||||
|
||||
// Intialize a skeleton with nothing but a rows array and service object
|
||||
dashboard = {
|
||||
@@ -35,11 +32,13 @@ return function(callback) {
|
||||
|
||||
// Set a title
|
||||
dashboard.title = 'Scripted dash';
|
||||
dashboard.services.filter = {
|
||||
time: {
|
||||
from: "now-" + (ARGS.from || timspan),
|
||||
|
||||
// Set default time
|
||||
// time can be overriden in the url using from/to parameteres, but this is
|
||||
// handled automatically in grafana core during dashboard initialization
|
||||
dashboard.time = {
|
||||
from: "now-6h",
|
||||
to: "now"
|
||||
}
|
||||
};
|
||||
|
||||
var rows = 1;
|
||||
@@ -78,4 +77,4 @@ return function(callback) {
|
||||
callback(dashboard);
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
95
src/app/dashboards/scripted_gen_and_save.js
Normal file
95
src/app/dashboards/scripted_gen_and_save.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/* global _ */
|
||||
|
||||
/*
|
||||
* Complex scripted dashboard
|
||||
* This script generates a dashboard object that Grafana can load. It also takes a number of user
|
||||
* supplied URL parameters (int ARGS variable)
|
||||
*
|
||||
* Return a dashboard object, or a function
|
||||
*
|
||||
* For async scripts, return a function, this function must take a single callback function as argument,
|
||||
* call this callback function with the dashboard object (look at scripted_async.js for an example)
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// accessable variables in this scope
|
||||
var window, document, ARGS, $, jQuery, moment, kbn, services, _;
|
||||
|
||||
// default datasource
|
||||
var datasource = services.datasourceSrv.default;
|
||||
// get datasource used for saving dashboards
|
||||
var dashboardDB = services.datasourceSrv.getGrafanaDB();
|
||||
|
||||
var targets = [];
|
||||
|
||||
function getTargets(path) {
|
||||
return datasource.metricFindQuery(path + '.*').then(function(result) {
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (targets.length === 10) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var promises = _.map(result, function(metric) {
|
||||
if (metric.expandable) {
|
||||
return getTargets(path + "." + metric.text);
|
||||
}
|
||||
else {
|
||||
targets.push(path + '.' + metric.text);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
return services.$q.all(promises);
|
||||
});
|
||||
}
|
||||
|
||||
function createDashboard(target, index) {
|
||||
// Intialize a skeleton with nothing but a rows array and service object
|
||||
var dashboard = { rows : [] };
|
||||
dashboard.title = 'Scripted dash ' + index;
|
||||
dashboard.time = {
|
||||
from: "now-6h",
|
||||
to: "now"
|
||||
};
|
||||
|
||||
dashboard.rows.push({
|
||||
title: 'Chart',
|
||||
height: '300px',
|
||||
panels: [
|
||||
{
|
||||
title: 'Events',
|
||||
type: 'graph',
|
||||
span: 12,
|
||||
targets: [ {target: target} ]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
function saveDashboard(dashboard) {
|
||||
var model = services.dashboardSrv.create(dashboard);
|
||||
dashboardDB.saveDashboard(model);
|
||||
}
|
||||
|
||||
return function(callback) {
|
||||
|
||||
getTargets('apps').then(function() {
|
||||
console.log('targets: ', targets);
|
||||
_.each(targets, function(target, index) {
|
||||
var dashboard = createDashboard(target, index);
|
||||
saveDashboard(dashboard);
|
||||
|
||||
if (index === targets.length - 1) {
|
||||
callback(dashboard);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
98
src/app/dashboards/scripted_templated.js
Normal file
98
src/app/dashboards/scripted_templated.js
Normal file
@@ -0,0 +1,98 @@
|
||||
/* global _ */
|
||||
|
||||
/*
|
||||
* Complex scripted dashboard
|
||||
* This script generates a dashboard object that Grafana can load. It also takes a number of user
|
||||
* supplied URL parameters (int ARGS variable)
|
||||
*
|
||||
* Return a dashboard object, or a function
|
||||
*
|
||||
* For async scripts, return a function, this function must take a single callback function as argument,
|
||||
* call this callback function with the dashboard object (look at scripted_async.js for an example)
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// accessable variables in this scope
|
||||
var window, document, ARGS, $, jQuery, moment, kbn;
|
||||
|
||||
// Setup some variables
|
||||
var dashboard;
|
||||
|
||||
// All url parameters are available via the ARGS object
|
||||
var ARGS;
|
||||
|
||||
// Intialize a skeleton with nothing but a rows array and service object
|
||||
dashboard = {
|
||||
rows : [],
|
||||
};
|
||||
|
||||
// Set a title
|
||||
dashboard.title = 'Scripted and templated dash';
|
||||
|
||||
// Set default time
|
||||
// time can be overriden in the url using from/to parameteres, but this is
|
||||
// handled automatically in grafana core during dashboard initialization
|
||||
dashboard.time = {
|
||||
from: "now-6h",
|
||||
to: "now"
|
||||
};
|
||||
|
||||
dashboard.templating = {
|
||||
enable: true,
|
||||
list: [
|
||||
{
|
||||
name: 'test',
|
||||
query: 'apps.backend.*',
|
||||
refresh: true,
|
||||
options: [],
|
||||
current: null,
|
||||
},
|
||||
{
|
||||
name: 'test2',
|
||||
query: '*',
|
||||
refresh: true,
|
||||
options: [],
|
||||
current: null,
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var rows = 1;
|
||||
var seriesName = 'argName';
|
||||
|
||||
if(!_.isUndefined(ARGS.rows)) {
|
||||
rows = parseInt(ARGS.rows, 10);
|
||||
}
|
||||
|
||||
if(!_.isUndefined(ARGS.name)) {
|
||||
seriesName = ARGS.name;
|
||||
}
|
||||
|
||||
for (var i = 0; i < rows; i++) {
|
||||
|
||||
dashboard.rows.push({
|
||||
title: 'Chart',
|
||||
height: '300px',
|
||||
panels: [
|
||||
{
|
||||
title: 'Events',
|
||||
type: 'graph',
|
||||
span: 12,
|
||||
fill: 1,
|
||||
linewidth: 2,
|
||||
targets: [
|
||||
{
|
||||
'target': "randomWalk('" + seriesName + "')"
|
||||
},
|
||||
{
|
||||
'target': "randomWalk('[[test2]]')"
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return dashboard;
|
||||
263
src/app/dashboards/template_vars.json
Normal file
263
src/app/dashboards/template_vars.json
Normal file
@@ -0,0 +1,263 @@
|
||||
{
|
||||
"id": null,
|
||||
"title": "Templated Graphs Nested",
|
||||
"originalTitle": "Templated Graphs Nested",
|
||||
"tags": [
|
||||
"showcase",
|
||||
"templated"
|
||||
],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"hideControls": false,
|
||||
"rows": [
|
||||
{
|
||||
"title": "Row1",
|
||||
"height": "350px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"type": "graph",
|
||||
"loadingEditor": false,
|
||||
"datasource": null,
|
||||
"renderer": "flot",
|
||||
"x-axis": true,
|
||||
"y-axis": true,
|
||||
"scale": 1,
|
||||
"y_formats": [
|
||||
"short",
|
||||
"short"
|
||||
],
|
||||
"grid": {
|
||||
"max": null,
|
||||
"min": 0,
|
||||
"threshold1": null,
|
||||
"threshold2": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)",
|
||||
"leftMax": null,
|
||||
"rightMax": null,
|
||||
"leftMin": null,
|
||||
"rightMin": null
|
||||
},
|
||||
"annotate": {
|
||||
"enable": false
|
||||
},
|
||||
"resolution": 100,
|
||||
"lines": true,
|
||||
"fill": 1,
|
||||
"linewidth": 1,
|
||||
"points": false,
|
||||
"pointradius": 5,
|
||||
"bars": false,
|
||||
"stack": true,
|
||||
"legend": {
|
||||
"show": true,
|
||||
"values": false,
|
||||
"min": false,
|
||||
"max": false,
|
||||
"current": false,
|
||||
"total": false,
|
||||
"avg": false
|
||||
},
|
||||
"percentage": false,
|
||||
"nullPointMode": "connected",
|
||||
"steppedLine": false,
|
||||
"tooltip": {
|
||||
"value_type": "cumulative",
|
||||
"query_as_alias": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"target": "aliasByNode(apps.$app.$server.counters.requests.count, 2)",
|
||||
"function": "mean",
|
||||
"column": "value"
|
||||
}
|
||||
],
|
||||
"aliasColors": {
|
||||
"highres.test": "#1F78C1",
|
||||
"scale(highres.test,3)": "#6ED0E0",
|
||||
"mobile": "#6ED0E0",
|
||||
"tablet": "#EAB839"
|
||||
},
|
||||
"title": "Traffic [[period]]",
|
||||
"id": 1,
|
||||
"seriesOverrides": []
|
||||
}
|
||||
],
|
||||
"notice": false
|
||||
},
|
||||
{
|
||||
"title": "Row1",
|
||||
"height": "350px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"type": "graph",
|
||||
"loadingEditor": false,
|
||||
"datasource": null,
|
||||
"renderer": "flot",
|
||||
"x-axis": true,
|
||||
"y-axis": true,
|
||||
"scale": 1,
|
||||
"y_formats": [
|
||||
"short",
|
||||
"short"
|
||||
],
|
||||
"grid": {
|
||||
"max": null,
|
||||
"min": 0,
|
||||
"threshold1": null,
|
||||
"threshold2": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)",
|
||||
"leftMax": null,
|
||||
"rightMax": null,
|
||||
"leftMin": null,
|
||||
"rightMin": null
|
||||
},
|
||||
"annotate": {
|
||||
"enable": false
|
||||
},
|
||||
"resolution": 100,
|
||||
"lines": true,
|
||||
"fill": 1,
|
||||
"linewidth": 1,
|
||||
"points": false,
|
||||
"pointradius": 5,
|
||||
"bars": false,
|
||||
"stack": true,
|
||||
"legend": {
|
||||
"show": true,
|
||||
"values": false,
|
||||
"min": false,
|
||||
"max": false,
|
||||
"current": false,
|
||||
"total": false,
|
||||
"avg": false
|
||||
},
|
||||
"percentage": false,
|
||||
"zerofill": true,
|
||||
"nullPointMode": "connected",
|
||||
"steppedLine": false,
|
||||
"tooltip": {
|
||||
"value_type": "cumulative",
|
||||
"query_as_alias": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"target": "aliasByNode(apps.$app.$server.counters.requests.count, 2)"
|
||||
}
|
||||
],
|
||||
"aliasColors": {
|
||||
"highres.test": "#1F78C1",
|
||||
"scale(highres.test,3)": "#6ED0E0",
|
||||
"mobile": "#6ED0E0",
|
||||
"tablet": "#EAB839"
|
||||
},
|
||||
"title": "Second pannel",
|
||||
"id": 2,
|
||||
"seriesOverrides": []
|
||||
}
|
||||
],
|
||||
"notice": false
|
||||
}
|
||||
],
|
||||
"nav": [
|
||||
{
|
||||
"type": "timepicker",
|
||||
"collapse": false,
|
||||
"notice": false,
|
||||
"enable": true,
|
||||
"status": "Stable",
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
],
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"now": true
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-15m",
|
||||
"to": "now"
|
||||
},
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"type": "query",
|
||||
"name": "app",
|
||||
"query": "apps.*",
|
||||
"includeAll": true,
|
||||
"options": [],
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "*"
|
||||
},
|
||||
"datasource": null,
|
||||
"allFormat": "wildcard",
|
||||
"refresh": true
|
||||
},
|
||||
{
|
||||
"type": "query",
|
||||
"name": "server",
|
||||
"query": "apps.$app.*",
|
||||
"includeAll": true,
|
||||
"options": [],
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "*"
|
||||
},
|
||||
"datasource": null,
|
||||
"allFormat": "Glob",
|
||||
"refresh": false
|
||||
},
|
||||
{
|
||||
"type": "query",
|
||||
"datasource": null,
|
||||
"refresh_on_load": false,
|
||||
"name": "metric",
|
||||
"options": [],
|
||||
"includeAll": true,
|
||||
"allFormat": "glob",
|
||||
"query": "apps.$app.$server.*",
|
||||
"current": {
|
||||
"text": "counters",
|
||||
"value": "counters"
|
||||
}
|
||||
}
|
||||
],
|
||||
"enable": true
|
||||
},
|
||||
"annotations": {
|
||||
"enable": false
|
||||
},
|
||||
"refresh": false,
|
||||
"version": 6
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore',
|
||||
'lodash',
|
||||
'jquery',
|
||||
'../services/graphite/gfunc',
|
||||
],
|
||||
@@ -9,7 +9,7 @@ function (angular, app, _, $, gfunc) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.module('grafana.directives')
|
||||
.directive('graphiteAddFunc', function($compile) {
|
||||
var inputTemplate = '<input type="text"'+
|
||||
' class="grafana-target-segment-input input-medium grafana-target-segment-input"' +
|
||||
@@ -38,6 +38,15 @@ function (angular, app, _, $, gfunc) {
|
||||
items: 10,
|
||||
updater: function (value) {
|
||||
var funcDef = gfunc.getFuncDef(value);
|
||||
if (!funcDef) {
|
||||
// try find close match
|
||||
value = value.toLowerCase();
|
||||
funcDef = _.find(allFunctions, function(funcName) {
|
||||
return funcName.toLowerCase().indexOf(value) === 0;
|
||||
});
|
||||
|
||||
if (!funcDef) { return; }
|
||||
}
|
||||
|
||||
$scope.$apply(function() {
|
||||
$scope.addFunction(funcDef);
|
||||
@@ -59,13 +68,12 @@ function (angular, app, _, $, gfunc) {
|
||||
});
|
||||
|
||||
$input.blur(function() {
|
||||
$input.hide();
|
||||
$input.val('');
|
||||
$button.show();
|
||||
$button.focus();
|
||||
// clicking the function dropdown menu wont
|
||||
// work if you remove class at once
|
||||
setTimeout(function() {
|
||||
$input.val('');
|
||||
$input.hide();
|
||||
$button.show();
|
||||
elem.removeClass('open');
|
||||
}, 200);
|
||||
});
|
||||
@@ -97,4 +105,4 @@ function (angular, app, _, $, gfunc) {
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore'
|
||||
],
|
||||
function (angular, app, _) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.directive('addPanel', function($compile) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function($scope, elem) {
|
||||
|
||||
$scope.$on("$destroy",function() {
|
||||
elem.remove();
|
||||
});
|
||||
|
||||
$scope.$watch('panel.type', function() {
|
||||
var _type = $scope.panel.type;
|
||||
$scope.reset_panel(_type);
|
||||
if(!_.isUndefined($scope.panel.type)) {
|
||||
$scope.panel.loadingEditor = true;
|
||||
$scope.require(['panels/'+$scope.panel.type.replace(".","/") +'/module'], function () {
|
||||
var template = '<div ng-controller="'+$scope.panel.type+'" ng-include="\'app/partials/paneladd.html\'"></div>';
|
||||
elem.html($compile(angular.element(template))($scope));
|
||||
$scope.panel.loadingEditor = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,22 @@
|
||||
define([
|
||||
'./addPanel',
|
||||
'./arrayJoin',
|
||||
'./dashUpload',
|
||||
'./kibanaPanel',
|
||||
'./kibanaSimplePanel',
|
||||
'./grafanaPanel',
|
||||
'./grafanaSimplePanel',
|
||||
'./ngBlur',
|
||||
'./dashEditLink',
|
||||
'./ngModelOnBlur',
|
||||
'./tip',
|
||||
'./confirmClick',
|
||||
'./configModal',
|
||||
'./spectrumPicker',
|
||||
'./grafanaGraph',
|
||||
'./bootstrap-tagsinput',
|
||||
'./bodyClass',
|
||||
'./addGraphiteFunc',
|
||||
'./graphiteFuncEditor',
|
||||
'./templateParamSelector',
|
||||
'./graphiteSegment',
|
||||
'./grafanaVersionCheck',
|
||||
'./dropdown.typeahead',
|
||||
'./influxdbFuncEditor'
|
||||
], function () {});
|
||||
], function () {});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore'
|
||||
'lodash'
|
||||
],
|
||||
function (angular, app, _) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.module('grafana.directives')
|
||||
.directive('arrayJoin', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore'
|
||||
'lodash'
|
||||
],
|
||||
function (angular, app, _) {
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.module('grafana.directives')
|
||||
.directive('bodyClass', function() {
|
||||
return {
|
||||
link: function($scope, elem) {
|
||||
|
||||
var lastPulldownVal;
|
||||
var lastHideControlsVal;
|
||||
|
||||
$scope.$watch('dashboard.current.pulldowns', function() {
|
||||
var panel = _.find($scope.dashboard.current.pulldowns, function(pulldown) { return pulldown.enable; });
|
||||
var panelEnabled = panel ? panel.enable : false;
|
||||
if (lastPulldownVal !== panelEnabled) {
|
||||
elem.toggleClass('submenu-controls-visible', panelEnabled);
|
||||
lastPulldownVal = panelEnabled;
|
||||
$scope.$watch('submenuEnabled', function() {
|
||||
if (!$scope.dashboard) {
|
||||
return;
|
||||
}
|
||||
}, true);
|
||||
|
||||
$scope.$watch('dashboard.current.hideControls', function() {
|
||||
var hideControls = $scope.dashboard.current.hideControls || $scope.playlist_active;
|
||||
elem.toggleClass('submenu-controls-visible', $scope.submenuEnabled);
|
||||
});
|
||||
|
||||
$scope.$watch('dashboard.hideControls', function() {
|
||||
if (!$scope.dashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hideControls = $scope.dashboard.hideControls || $scope.playlist_active;
|
||||
|
||||
if (lastHideControlsVal !== hideControls) {
|
||||
elem.toggleClass('hide-controls', hideControls);
|
||||
@@ -41,4 +43,4 @@ function (angular, app, _) {
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
8
src/app/directives/bootstrap-tagsinput.js
vendored
8
src/app/directives/bootstrap-tagsinput.js
vendored
@@ -7,7 +7,7 @@ function (angular, $) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.module('grafana.directives')
|
||||
.directive('bootstrapTagsinput', function() {
|
||||
|
||||
function getItemProperty(scope, property) {
|
||||
@@ -84,7 +84,7 @@ function (angular, $) {
|
||||
});
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.module('grafana.directives')
|
||||
.directive('gfDropdown', function ($parse, $compile, $timeout) {
|
||||
|
||||
function buildTemplate(items, placement) {
|
||||
@@ -102,7 +102,7 @@ function (angular, $) {
|
||||
var li = '<li' + (item.submenu && item.submenu.length ? ' class="dropdown-submenu"' : '') + '>' +
|
||||
'<a tabindex="-1" ng-href="' + (item.href || '') + '"' + (item.click ? ' ng-click="' + item.click + '"' : '') +
|
||||
(item.target ? ' target="' + item.target + '"' : '') + (item.method ? ' data-method="' + item.method + '"' : '') +
|
||||
(item.configModal ? ' config-modal="' + item.configModal + '"' : "") +
|
||||
(item.configModal ? ' dash-editor-link="' + item.configModal + '"' : "") +
|
||||
'>' + (item.text || '') + '</a>';
|
||||
|
||||
if (item.submenu && item.submenu.length) {
|
||||
@@ -131,4 +131,4 @@ function (angular, $) {
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
define([
|
||||
'angular',
|
||||
'underscore',
|
||||
'lodash',
|
||||
'jquery'
|
||||
],
|
||||
function (angular, _, $) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.module('grafana.directives')
|
||||
.directive('configModal', function($modal, $q, $timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
@@ -45,4 +45,4 @@ function (angular, _, $) {
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ define([
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.directives');
|
||||
var module = angular.module('grafana.directives');
|
||||
|
||||
module.directive('confirmClick', function() {
|
||||
return {
|
||||
|
||||
88
src/app/directives/dashEditLink.js
Normal file
88
src/app/directives/dashEditLink.js
Normal file
@@ -0,0 +1,88 @@
|
||||
define([
|
||||
'angular',
|
||||
'jquery'
|
||||
],
|
||||
function (angular, $) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('grafana.directives')
|
||||
.directive('dashEditorLink', function($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, elem, attrs) {
|
||||
var partial = attrs.dashEditorLink;
|
||||
|
||||
elem.bind('click',function() {
|
||||
$timeout(function() {
|
||||
var editorScope = attrs.editorScope === 'isolated' ? null : scope;
|
||||
scope.appEvent('show-dash-editor', { src: partial, scope: editorScope });
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
angular
|
||||
.module('grafana.directives')
|
||||
.directive('dashEditorView', function($compile) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, elem) {
|
||||
var editorScope;
|
||||
var lastEditor;
|
||||
|
||||
function hideScrollbars(value) {
|
||||
if (value) {
|
||||
window.scrollTo(0,0);
|
||||
document.documentElement.style.overflow = 'hidden'; // firefox, chrome
|
||||
document.body.scroll = "no"; // ie only
|
||||
} else {
|
||||
document.documentElement.style.overflow = 'auto';
|
||||
document.body.scroll = "yes";
|
||||
}
|
||||
}
|
||||
|
||||
function hideEditorPane() {
|
||||
hideScrollbars(false);
|
||||
if (editorScope) { editorScope.dismiss(); }
|
||||
}
|
||||
|
||||
scope.onAppEvent("dashboard-loaded", hideEditorPane);
|
||||
scope.onAppEvent('hide-dash-editor', hideEditorPane);
|
||||
|
||||
scope.onAppEvent('show-dash-editor', function(evt, payload) {
|
||||
if (lastEditor === payload.src) {
|
||||
hideEditorPane();
|
||||
return;
|
||||
}
|
||||
|
||||
hideEditorPane();
|
||||
|
||||
scope.exitFullscreen();
|
||||
|
||||
lastEditor = payload.src;
|
||||
editorScope = payload.scope ? payload.scope.$new() : scope.$new();
|
||||
|
||||
editorScope.dismiss = function() {
|
||||
editorScope.$destroy();
|
||||
elem.empty();
|
||||
lastEditor = null;
|
||||
editorScope = null;
|
||||
hideScrollbars(false);
|
||||
};
|
||||
|
||||
// hide page scrollbars while edit pane is visible
|
||||
hideScrollbars(true);
|
||||
|
||||
var src = "'" + payload.src + "'";
|
||||
var view = $('<div class="dashboard-edit-view" ng-include="' + src + '"></div>');
|
||||
elem.append(view);
|
||||
$compile(elem.contents())(editorScope);
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,12 +1,13 @@
|
||||
define([
|
||||
'angular'
|
||||
'angular',
|
||||
'kbn'
|
||||
],
|
||||
function (angular) {
|
||||
function (angular, kbn) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.directives');
|
||||
var module = angular.module('grafana.directives');
|
||||
|
||||
module.directive('dashUpload', function(timer, dashboard, alertSrv) {
|
||||
module.directive('dashUpload', function(timer, alertSrv, $location) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope) {
|
||||
@@ -14,8 +15,17 @@ function (angular) {
|
||||
var files = evt.target.files; // FileList object
|
||||
var readerOnload = function() {
|
||||
return function(e) {
|
||||
dashboard.dash_load(JSON.parse(e.target.result));
|
||||
scope.$apply();
|
||||
scope.$apply(function() {
|
||||
try {
|
||||
window.grafanaImportDashboard = JSON.parse(e.target.result);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
scope.appEvent('alert-error', ['Import failed', 'JSON -> JS Serialization failed: ' + err.message]);
|
||||
return;
|
||||
}
|
||||
var title = kbn.slugifyForUrl(window.grafanaImportDashboard.title);
|
||||
$location.path('/dashboard/import/' + title);
|
||||
});
|
||||
};
|
||||
};
|
||||
for (var i = 0, f; f = files[i]; i++) {
|
||||
@@ -34,4 +44,4 @@ function (angular) {
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
105
src/app/directives/dropdown.typeahead.js
Normal file
105
src/app/directives/dropdown.typeahead.js
Normal file
@@ -0,0 +1,105 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'lodash',
|
||||
'jquery',
|
||||
],
|
||||
function (angular, app, _, $) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('grafana.directives')
|
||||
.directive('dropdownTypeahead', function($compile) {
|
||||
|
||||
var inputTemplate = '<input type="text"'+
|
||||
' class="grafana-target-segment-input input-medium grafana-target-segment-input"' +
|
||||
' spellcheck="false" style="display:none"></input>';
|
||||
|
||||
var buttonTemplate = '<a class="grafana-target-segment grafana-target-function dropdown-toggle"' +
|
||||
' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
|
||||
' data-placement="top"><i class="icon-plus"></i></a>';
|
||||
|
||||
return {
|
||||
scope: {
|
||||
"menuItems": "=dropdownTypeahead",
|
||||
"dropdownTypeaheadOnSelect": "&dropdownTypeaheadOnSelect"
|
||||
},
|
||||
link: function($scope, elem) {
|
||||
var $input = $(inputTemplate);
|
||||
var $button = $(buttonTemplate);
|
||||
$input.appendTo(elem);
|
||||
$button.appendTo(elem);
|
||||
|
||||
var typeaheadValues = _.reduce($scope.menuItems, function(memo, value) {
|
||||
_.each(value.submenu, function(item) {
|
||||
memo.push(value.text + ' ' + item.text);
|
||||
});
|
||||
return memo;
|
||||
}, []);
|
||||
|
||||
$scope.menuItemSelected = function(optionIndex, valueIndex) {
|
||||
var option = $scope.menuItems[optionIndex];
|
||||
var result = {
|
||||
$item: option.submenu[valueIndex],
|
||||
$optionIndex: optionIndex,
|
||||
$valueIndex: valueIndex
|
||||
};
|
||||
|
||||
$scope.dropdownTypeaheadOnSelect(result);
|
||||
};
|
||||
|
||||
$input.attr('data-provide', 'typeahead');
|
||||
$input.typeahead({
|
||||
source: typeaheadValues,
|
||||
minLength: 1,
|
||||
items: 10,
|
||||
updater: function (value) {
|
||||
var result = {};
|
||||
_.each($scope.menuItems, function(menuItem, optionIndex) {
|
||||
_.each(menuItem.submenu, function(submenuItem, valueIndex) {
|
||||
if (value === (menuItem.text + ' ' + submenuItem.text)) {
|
||||
result.$item = submenuItem;
|
||||
result.$optionIndex = optionIndex;
|
||||
result.$valueIndex = valueIndex;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (result.$item) {
|
||||
$scope.$apply(function() {
|
||||
$scope.dropdownTypeaheadOnSelect(result);
|
||||
});
|
||||
}
|
||||
|
||||
$input.trigger('blur');
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
$button.click(function() {
|
||||
$button.hide();
|
||||
$input.show();
|
||||
$input.focus();
|
||||
});
|
||||
|
||||
$input.keyup(function() {
|
||||
elem.toggleClass('open', $input.val() === '');
|
||||
});
|
||||
|
||||
$input.blur(function() {
|
||||
$input.hide();
|
||||
$input.val('');
|
||||
$button.show();
|
||||
$button.focus();
|
||||
// clicking the function dropdown menu wont
|
||||
// work if you remove class at once
|
||||
setTimeout(function() {
|
||||
elem.removeClass('open');
|
||||
}, 200);
|
||||
});
|
||||
|
||||
$compile(elem.contents())($scope);
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
89
src/app/directives/grafanaPanel.js
Normal file
89
src/app/directives/grafanaPanel.js
Normal file
@@ -0,0 +1,89 @@
|
||||
define([
|
||||
'angular',
|
||||
'jquery',
|
||||
'config',
|
||||
'./panelMenu',
|
||||
],
|
||||
function (angular, $, config) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('grafana.directives')
|
||||
.directive('grafanaPanel', function($compile, $parse) {
|
||||
|
||||
var container = '<div class="panel-container"></div>';
|
||||
var content = '<div class="panel-content"></div>';
|
||||
|
||||
var panelHeader =
|
||||
'<div class="panel-header">'+
|
||||
'<span class="alert-error panel-error small pointer"' +
|
||||
'config-modal="app/partials/inspector.html" ng-if="panelMeta.error">' +
|
||||
'<span data-placement="top" bs-tooltip="panelMeta.error">' +
|
||||
'<i class="icon-exclamation-sign"></i><span class="panel-error-arrow"></span>' +
|
||||
'</span>' +
|
||||
'</span>' +
|
||||
|
||||
'<span class="panel-loading" ng-show="panelMeta.loading">' +
|
||||
'<i class="icon-spinner icon-spin icon-large"></i>' +
|
||||
'</span>' +
|
||||
|
||||
'<div class="panel-title-container drag-handle" panel-menu></div>' +
|
||||
'</div>'+
|
||||
'</div>';
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function($scope, elem, attr) {
|
||||
var getter = $parse(attr.type), panelType = getter($scope);
|
||||
var newScope = $scope.$new();
|
||||
|
||||
$scope.kbnJqUiDraggableOptions = {
|
||||
revert: 'invalid',
|
||||
helper: function() {
|
||||
return $('<div style="width:200px;height:100px;background: rgba(100,100,100,0.50);"/>');
|
||||
},
|
||||
placeholder: 'keep'
|
||||
};
|
||||
|
||||
// compile the module and uncloack. We're done
|
||||
function loadModule($module) {
|
||||
$module.appendTo(elem);
|
||||
elem.wrap(container);
|
||||
/* jshint indent:false */
|
||||
$compile(elem.contents())(newScope);
|
||||
elem.removeClass("ng-cloak");
|
||||
|
||||
var panelCtrlElem = $(elem.children()[0]);
|
||||
var panelCtrlScope = panelCtrlElem.data().$scope;
|
||||
|
||||
panelCtrlScope.$watchGroup(['fullscreen', 'panel.height', 'row.height'], function() {
|
||||
panelCtrlElem.css({ minHeight: panelCtrlScope.panel.height || panelCtrlScope.row.height });
|
||||
panelCtrlElem.toggleClass('panel-fullscreen', panelCtrlScope.fullscreen ? true : false);
|
||||
});
|
||||
}
|
||||
|
||||
newScope.$on('$destroy',function() {
|
||||
elem.unbind();
|
||||
elem.remove();
|
||||
});
|
||||
|
||||
elem.addClass('ng-cloak');
|
||||
|
||||
var panelPath = config.panels[panelType].path;
|
||||
|
||||
$scope.require([
|
||||
'jquery',
|
||||
'text!'+panelPath+'/module.html',
|
||||
panelPath + "/module",
|
||||
], function ($, moduleTemplate) {
|
||||
var $module = $(moduleTemplate);
|
||||
$module.prepend(panelHeader);
|
||||
$module.first().find('.panel-header').nextAll().wrapAll(content);
|
||||
loadModule($module);
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,13 +1,12 @@
|
||||
define([
|
||||
'angular',
|
||||
'underscore'
|
||||
],
|
||||
function (angular, _) {
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.directive('kibanaSimplePanel', function($compile) {
|
||||
.module('grafana.directives')
|
||||
.directive('grafanaSimplePanel', function($compile) {
|
||||
var panelLoading = '<span ng-show="panelMeta.loading == true">' +
|
||||
'<span style="font-size:72px;font-weight:200">'+
|
||||
'<i class="icon-spinner icon-spin"></i> loading ...' +
|
||||
@@ -60,18 +59,8 @@ function (angular, _) {
|
||||
loadController(name);
|
||||
});
|
||||
|
||||
if(attr.panel) {
|
||||
$scope.$watch(attr.panel, function (panel) {
|
||||
// If the panel attribute is specified, create a new scope. This ruins configuration
|
||||
// so don't do it with anything that needs to use editor.html
|
||||
if(!_.isUndefined(panel)) {
|
||||
$scope = $scope.$new();
|
||||
$scope.panel = angular.fromJson(panel);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ function (angular) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.module('grafana.directives')
|
||||
.directive('grafanaVersionCheck', function($http, grafanaVersion) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
@@ -14,7 +14,7 @@ function (angular) {
|
||||
return;
|
||||
}
|
||||
|
||||
$http({ method: 'GET', url: 'http://grafanarel.s3.amazonaws.com/latest.json' })
|
||||
$http({ method: 'GET', url: 'https://grafanarel.s3.amazonaws.com/latest.json' })
|
||||
.then(function(response) {
|
||||
if (!response.data || !response.data.version) {
|
||||
return;
|
||||
@@ -30,4 +30,4 @@ function (angular) {
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
define([
|
||||
'angular',
|
||||
'underscore',
|
||||
'lodash',
|
||||
'jquery',
|
||||
],
|
||||
function (angular, _, $) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.directive('graphiteFuncEditor', function($compile) {
|
||||
.module('grafana.directives')
|
||||
.directive('graphiteFuncEditor', function($compile, templateSrv) {
|
||||
|
||||
var funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
|
||||
var paramTemplate = '<input type="text" style="display:none"' +
|
||||
@@ -69,12 +69,12 @@ function (angular, _, $) {
|
||||
|
||||
function inputBlur(paramIndex) {
|
||||
/*jshint validthis:true */
|
||||
|
||||
var $input = $(this);
|
||||
var $link = $input.prev();
|
||||
var newValue = $input.val();
|
||||
|
||||
if ($input.val() !== '' || func.def.params[paramIndex].optional) {
|
||||
$link.text($input.val());
|
||||
if (newValue !== '' || func.def.params[paramIndex].optional) {
|
||||
$link.html(templateSrv.highlightVariablesAsHtml(newValue));
|
||||
|
||||
func.updateParam($input.val(), paramIndex);
|
||||
scheduledRelinkIfNeeded();
|
||||
@@ -88,7 +88,6 @@ function (angular, _, $) {
|
||||
|
||||
function inputKeyPress(paramIndex, e) {
|
||||
/*jshint validthis:true */
|
||||
|
||||
if(e.which === 13) {
|
||||
inputBlur.call(this, paramIndex);
|
||||
}
|
||||
@@ -147,7 +146,7 @@ function (angular, _, $) {
|
||||
$funcLink.appendTo(elem);
|
||||
|
||||
_.each(funcDef.params, function(param, index) {
|
||||
if (param.optional && !func.params[index]) {
|
||||
if (param.optional && func.params.length <= index) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,7 +154,8 @@ function (angular, _, $) {
|
||||
$('<span>, </span>').appendTo(elem);
|
||||
}
|
||||
|
||||
var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + func.params[index] + '</a>');
|
||||
var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
|
||||
var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + paramValue + '</a>');
|
||||
var $input = $(paramTemplate);
|
||||
|
||||
paramCountAtLink++;
|
||||
@@ -206,6 +206,7 @@ function (angular, _, $) {
|
||||
if ($target.hasClass('icon-arrow-left')) {
|
||||
$scope.$apply(function() {
|
||||
_.move($scope.functions, $scope.$index, $scope.$index - 1);
|
||||
$scope.targetChanged();
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -213,6 +214,7 @@ function (angular, _, $) {
|
||||
if ($target.hasClass('icon-arrow-right')) {
|
||||
$scope.$apply(function() {
|
||||
_.move($scope.functions, $scope.$index, $scope.$index + 1);
|
||||
$scope.targetChanged();
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -239,4 +241,4 @@ function (angular, _, $) {
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
134
src/app/directives/graphiteSegment.js
Normal file
134
src/app/directives/graphiteSegment.js
Normal file
@@ -0,0 +1,134 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'lodash',
|
||||
'jquery',
|
||||
],
|
||||
function (angular, app, _, $) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('grafana.directives')
|
||||
.directive('graphiteSegment', function($compile, $sce) {
|
||||
var inputTemplate = '<input type="text" data-provide="typeahead" ' +
|
||||
' class="grafana-target-text-input input-medium"' +
|
||||
' spellcheck="false" style="display:none"></input>';
|
||||
|
||||
var buttonTemplate = '<a class="grafana-target-segment" tabindex="1" focus-me="segment.focus" ng-bind-html="segment.html"></a>';
|
||||
|
||||
return {
|
||||
link: function($scope, elem) {
|
||||
var $input = $(inputTemplate);
|
||||
var $button = $(buttonTemplate);
|
||||
var segment = $scope.segment;
|
||||
var options = null;
|
||||
var cancelBlur = null;
|
||||
|
||||
$input.appendTo(elem);
|
||||
$button.appendTo(elem);
|
||||
|
||||
$scope.updateVariableValue = function(value) {
|
||||
if (value === '' || segment.value === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.$apply(function() {
|
||||
var selected = _.findWhere($scope.altSegments, { value: value });
|
||||
if (selected) {
|
||||
segment.value = selected.value;
|
||||
segment.html = selected.html;
|
||||
segment.expandable = selected.expandable;
|
||||
}
|
||||
else {
|
||||
segment.value = value;
|
||||
segment.html = $sce.trustAsHtml(value);
|
||||
segment.expandable = true;
|
||||
}
|
||||
$scope.segmentValueChanged(segment, $scope.$index);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.switchToLink = function(now) {
|
||||
if (now === true || cancelBlur) {
|
||||
clearTimeout(cancelBlur);
|
||||
cancelBlur = null;
|
||||
$input.hide();
|
||||
$button.show();
|
||||
$scope.updateVariableValue($input.val());
|
||||
}
|
||||
else {
|
||||
// need to have long delay because the blur
|
||||
// happens long before the click event on the typeahead options
|
||||
cancelBlur = setTimeout($scope.switchToLink, 350);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.source = function(query, callback) {
|
||||
if (options) { return options; }
|
||||
|
||||
$scope.$apply(function() {
|
||||
$scope.getAltSegments($scope.$index).then(function() {
|
||||
options = _.map($scope.altSegments, function(alt) { return alt.value; });
|
||||
|
||||
// add custom values
|
||||
if (segment.value !== 'select metric' && _.indexOf(options, segment.value) === -1) {
|
||||
options.unshift(segment.value);
|
||||
}
|
||||
|
||||
callback(options);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updater = function(value) {
|
||||
if (value === segment.value) {
|
||||
clearTimeout(cancelBlur);
|
||||
$input.focus();
|
||||
return value;
|
||||
}
|
||||
|
||||
$input.val(value);
|
||||
$scope.switchToLink(true);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
$input.attr('data-provide', 'typeahead');
|
||||
$input.typeahead({ source: $scope.source, minLength: 0, items: 10000, updater: $scope.updater });
|
||||
|
||||
var typeahead = $input.data('typeahead');
|
||||
typeahead.lookup = function () {
|
||||
this.query = this.$element.val() || '';
|
||||
var items = this.source(this.query, $.proxy(this.process, this));
|
||||
return items ? this.process(items) : items;
|
||||
};
|
||||
|
||||
$button.keydown(function(evt) {
|
||||
// trigger typeahead on down arrow or enter key
|
||||
if (evt.keyCode === 40 || evt.keyCode === 13) {
|
||||
$button.click();
|
||||
}
|
||||
});
|
||||
|
||||
$button.click(function() {
|
||||
options = null;
|
||||
$input.css('width', ($button.width() + 16) + 'px');
|
||||
|
||||
$button.hide();
|
||||
$input.show();
|
||||
$input.focus();
|
||||
|
||||
var typeahead = $input.data('typeahead');
|
||||
if (typeahead) {
|
||||
$input.val('');
|
||||
typeahead.lookup();
|
||||
}
|
||||
});
|
||||
|
||||
$input.blur($scope.switchToLink);
|
||||
|
||||
$compile(elem.contents())($scope);
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,13 @@
|
||||
define([
|
||||
'angular',
|
||||
'underscore',
|
||||
'lodash',
|
||||
'jquery',
|
||||
],
|
||||
function (angular, _, $) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.module('grafana.directives')
|
||||
.directive('influxdbFuncEditor', function($compile) {
|
||||
|
||||
var funcSpanTemplate = '<a gf-dropdown="functionMenu" class="dropdown-toggle" ' +
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
define([
|
||||
'angular',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'../controllers/panelBaseCtrl'
|
||||
],
|
||||
function (angular, $, _, PanelBaseCtrl) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.directive('kibanaPanel', function($compile, $timeout, $rootScope, $injector) {
|
||||
|
||||
var container = '<div class="panel-container"></div>';
|
||||
var content = '<div class="panel-content"></div>';
|
||||
|
||||
var panelHeader =
|
||||
'<div class="panel-header">'+
|
||||
'<div class="row-fluid">' +
|
||||
'<div class="span12 alert-error panel-error small" ng-show="panel.error">' +
|
||||
'<a class="close" ng-click="panel.error=false">×</a>' +
|
||||
'<span><i class="icon-exclamation-sign"></i> <strong>Oops!</strong> {{panel.error}} </span>' +
|
||||
'<span class="pointer panel-error-inspector-link" config-modal="app/partials/inspector.html">View details</span>' +
|
||||
'</div>' +
|
||||
'</div>\n' +
|
||||
|
||||
'<div class="row-fluid panel-extra">' +
|
||||
'<div class="panel-extra-container">' +
|
||||
|
||||
'<span class="panel-loading" ng-show="panelMeta.loading == true">' +
|
||||
'<i class="icon-spinner icon-spin icon-large"></i>' +
|
||||
'</span>' +
|
||||
|
||||
'<span class="dropdown">' +
|
||||
'<span class="panel-text panel-title pointer" gf-dropdown="panelMeta.menu" tabindex="1" ' +
|
||||
'data-drag=true data-jqyoui-options="kbnJqUiDraggableOptions"'+
|
||||
' jqyoui-draggable="'+
|
||||
'{'+
|
||||
'animate:false,'+
|
||||
'mutate:false,'+
|
||||
'index:{{$index}},'+
|
||||
'onStart:\'panelMoveStart\','+
|
||||
'onStop:\'panelMoveStop\''+
|
||||
'}" ng-model="row.panels" ' +
|
||||
'>' +
|
||||
'{{panel.title || "No title"}}' +
|
||||
'</span>' +
|
||||
'</span>'+
|
||||
|
||||
'</div>'+
|
||||
'</div>\n'+
|
||||
'</div>';
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function($scope, elem, attr) {
|
||||
// once we have the template, scan it for controllers and
|
||||
// load the module.js if we have any
|
||||
var newScope = $scope.$new();
|
||||
|
||||
$scope.kbnJqUiDraggableOptions = {
|
||||
revert: 'invalid',
|
||||
helper: function() {
|
||||
return $('<div style="width:200px;height:100px;background: rgba(100,100,100,0.50);"/>');
|
||||
},
|
||||
placeholder: 'keep'
|
||||
};
|
||||
|
||||
// compile the module and uncloack. We're done
|
||||
function loadModule($module) {
|
||||
$module.appendTo(elem);
|
||||
elem.wrap(container);
|
||||
/* jshint indent:false */
|
||||
$compile(elem.contents())(newScope);
|
||||
elem.removeClass("ng-cloak");
|
||||
}
|
||||
|
||||
newScope.$on('$destroy',function() {
|
||||
elem.unbind();
|
||||
elem.remove();
|
||||
});
|
||||
|
||||
newScope.initBaseController = function(self, scope) {
|
||||
$injector.invoke(PanelBaseCtrl, self, { $scope: scope });
|
||||
};
|
||||
|
||||
$scope.$watch(attr.type, function (name) {
|
||||
elem.addClass("ng-cloak");
|
||||
// load the panels module file, then render it in the dom.
|
||||
var nameAsPath = name.replace(".", "/");
|
||||
$scope.require([
|
||||
'jquery',
|
||||
'text!panels/'+nameAsPath+'/module.html'
|
||||
], function ($, moduleTemplate) {
|
||||
var $module = $(moduleTemplate);
|
||||
// top level controllers
|
||||
var $controllers = $module.filter('ngcontroller, [ng-controller], .ng-controller');
|
||||
// add child controllers
|
||||
$controllers = $controllers.add($module.find('ngcontroller, [ng-controller], .ng-controller'));
|
||||
|
||||
if ($controllers.length) {
|
||||
$controllers.first().prepend(panelHeader);
|
||||
$controllers.first().find('.panel-header').nextAll().wrapAll(content);
|
||||
|
||||
$scope.require(['panels/' + nameAsPath + '/module'], function() {
|
||||
loadModule($module);
|
||||
});
|
||||
} else {
|
||||
loadModule($module);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
@@ -5,7 +5,7 @@ function (angular) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.module('grafana.directives')
|
||||
.directive('ngBlur', ['$parse', function($parse) {
|
||||
return function(scope, element, attr) {
|
||||
var fn = $parse(attr['ngBlur']);
|
||||
|
||||
@@ -3,17 +3,18 @@ function (angular) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.module('grafana.directives')
|
||||
.directive('ngModelOnblur', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
priority: 1,
|
||||
require: 'ngModel',
|
||||
link: function(scope, elm, attr, ngModelCtrl) {
|
||||
if (attr.type === 'radio' || attr.type === 'checkbox') {
|
||||
return;
|
||||
}
|
||||
|
||||
elm.unbind('input').unbind('keydown').unbind('change');
|
||||
elm.off('input keydown change');
|
||||
elm.bind('blur', function() {
|
||||
scope.$apply(function() {
|
||||
ngModelCtrl.$setViewValue(elm.val());
|
||||
@@ -22,4 +23,4 @@ function (angular) {
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
155
src/app/directives/panelMenu.js
Normal file
155
src/app/directives/panelMenu.js
Normal file
@@ -0,0 +1,155 @@
|
||||
define([
|
||||
'angular',
|
||||
'jquery',
|
||||
'lodash',
|
||||
],
|
||||
function (angular, $, _) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('grafana.directives')
|
||||
.directive('panelMenu', function($compile, linkSrv) {
|
||||
var linkTemplate =
|
||||
'<span class="panel-title drag-handle pointer">' +
|
||||
'<span class="panel-title-text drag-handle">{{panel.title | interpolateTemplateVars}}</span>' +
|
||||
'<span class="panel-links-icon"></span>' +
|
||||
'</span>';
|
||||
|
||||
function createMenuTemplate($scope) {
|
||||
var template = '<div class="panel-menu small">';
|
||||
template += '<div class="panel-menu-inner">';
|
||||
template += '<div class="panel-menu-row">';
|
||||
template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(-1)"><i class="icon-minus"></i></a>';
|
||||
template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(1)"><i class="icon-plus"></i></a>';
|
||||
template += '<a class="panel-menu-icon pull-right" ng-click="remove_panel_from_row(row, panel)"><i class="icon-remove"></i></a>';
|
||||
template += '<div class="clearfix"></div>';
|
||||
template += '</div>';
|
||||
|
||||
template += '<div class="panel-menu-row">';
|
||||
template += '<a class="panel-menu-link" gf-dropdown="extendedMenu"><i class="icon-th-list"></i></a>';
|
||||
|
||||
_.each($scope.panelMeta.menu, function(item) {
|
||||
template += '<a class="panel-menu-link" ';
|
||||
if (item.click) { template += ' ng-click="' + item.click + '"'; }
|
||||
if (item.editorLink) { template += ' dash-editor-link="' + item.editorLink + '"'; }
|
||||
template += '>';
|
||||
template += item.text + '</a>';
|
||||
});
|
||||
|
||||
template += '</div>';
|
||||
template += '</div>';
|
||||
template += '</div>';
|
||||
return template;
|
||||
}
|
||||
|
||||
function getExtendedMenu($scope) {
|
||||
var menu = angular.copy($scope.panelMeta.extendedMenu);
|
||||
|
||||
if ($scope.panel.links) {
|
||||
_.each($scope.panel.links, function(link) {
|
||||
var info = linkSrv.getPanelLinkAnchorInfo(link);
|
||||
menu.push({text: info.title, href: info.href, target: info.target });
|
||||
});
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function($scope, elem) {
|
||||
var $link = $(linkTemplate);
|
||||
var $panelContainer = elem.parents(".panel-container");
|
||||
var menuWidth = $scope.panelMeta.menu.length === 4 ? 236 : 191;
|
||||
var menuScope = null;
|
||||
var timeout = null;
|
||||
var $menu = null;
|
||||
|
||||
elem.append($link);
|
||||
|
||||
$scope.$watchCollection('panel.links', function(newValue) {
|
||||
var showIcon = (newValue ? newValue.length > 0 : false) && $scope.panel.title !== '';
|
||||
$link.toggleClass('has-panel-links', showIcon);
|
||||
});
|
||||
|
||||
function dismiss(time, force) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
|
||||
if (time) {
|
||||
timeout = setTimeout(dismiss, time);
|
||||
return;
|
||||
}
|
||||
|
||||
// if hovering or draging pospone close
|
||||
if (force !== true) {
|
||||
if ($menu.is(':hover') || $scope.dashboard.$$panelDragging) {
|
||||
dismiss(2200);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (menuScope) {
|
||||
$menu.unbind();
|
||||
$menu.remove();
|
||||
menuScope.$destroy();
|
||||
menuScope = null;
|
||||
$menu = null;
|
||||
$panelContainer.removeClass('panel-highlight');
|
||||
}
|
||||
}
|
||||
|
||||
var showMenu = function(e) {
|
||||
// if menu item is clicked and menu was just removed from dom ignore this event
|
||||
if (!$.contains(document, e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($menu) {
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
var windowWidth = $(window).width();
|
||||
var panelLeftPos = $(elem).offset().left;
|
||||
var panelWidth = $(elem).width();
|
||||
var menuLeftPos = (panelWidth / 2) - (menuWidth/2);
|
||||
var stickingOut = panelLeftPos + menuLeftPos + menuWidth - windowWidth;
|
||||
if (stickingOut > 0) {
|
||||
menuLeftPos -= stickingOut + 10;
|
||||
}
|
||||
if (panelLeftPos + menuLeftPos < 0) {
|
||||
menuLeftPos = 0;
|
||||
}
|
||||
|
||||
var menuTemplate = createMenuTemplate($scope);
|
||||
$menu = $(menuTemplate);
|
||||
$menu.css('left', menuLeftPos);
|
||||
$menu.mouseleave(function() {
|
||||
dismiss(1000);
|
||||
});
|
||||
|
||||
menuScope = $scope.$new();
|
||||
menuScope.extendedMenu = getExtendedMenu($scope);
|
||||
menuScope.dismiss = function() {
|
||||
dismiss(null, true);
|
||||
};
|
||||
|
||||
$('.panel-menu').remove();
|
||||
elem.append($menu);
|
||||
$scope.$apply(function() {
|
||||
$compile($menu.contents())(menuScope);
|
||||
});
|
||||
|
||||
$(".panel-container").removeClass('panel-highlight');
|
||||
$panelContainer.toggleClass('panel-highlight');
|
||||
|
||||
dismiss(2200);
|
||||
};
|
||||
|
||||
elem.click(showMenu);
|
||||
$compile(elem.contents())($scope);
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@ function (angular) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.module('grafana.directives')
|
||||
.directive('spectrumPicker', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
@@ -32,7 +32,11 @@ function (angular) {
|
||||
};
|
||||
|
||||
input.spectrum(options);
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
input.spectrum('destroy');
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
82
src/app/directives/templateParamSelector.js
Normal file
82
src/app/directives/templateParamSelector.js
Normal file
@@ -0,0 +1,82 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'lodash',
|
||||
'jquery',
|
||||
],
|
||||
function (angular, app, _, $) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('grafana.directives')
|
||||
.directive('templateParamSelector', function($compile) {
|
||||
var inputTemplate = '<input type="text" data-provide="typeahead" ' +
|
||||
' class="grafana-target-text-input input-medium"' +
|
||||
' spellcheck="false" style="display:none"></input>';
|
||||
|
||||
var buttonTemplate = '<a class="grafana-target-segment tabindex="1">{{variable.current.text}}</a>';
|
||||
|
||||
return {
|
||||
link: function($scope, elem) {
|
||||
var $input = $(inputTemplate);
|
||||
var $button = $(buttonTemplate);
|
||||
var variable = $scope.variable;
|
||||
|
||||
$input.appendTo(elem);
|
||||
$button.appendTo(elem);
|
||||
|
||||
function updateVariableValue(value) {
|
||||
$scope.$apply(function() {
|
||||
var selected = _.findWhere(variable.options, { text: value });
|
||||
if (!selected) {
|
||||
selected = { text: value, value: value };
|
||||
}
|
||||
$scope.setVariableValue($scope.variable, selected);
|
||||
});
|
||||
}
|
||||
|
||||
$input.attr('data-provide', 'typeahead');
|
||||
$input.typeahead({
|
||||
minLength: 0,
|
||||
items: 1000,
|
||||
updater: function(value) {
|
||||
$input.val(value);
|
||||
$input.trigger('blur');
|
||||
return value;
|
||||
}
|
||||
});
|
||||
|
||||
var typeahead = $input.data('typeahead');
|
||||
typeahead.lookup = function () {
|
||||
var options = _.map(variable.options, function(option) { return option.text; });
|
||||
this.query = this.$element.val() || '';
|
||||
return this.process(options);
|
||||
};
|
||||
|
||||
$button.click(function() {
|
||||
$input.css('width', ($button.width() + 16) + 'px');
|
||||
|
||||
$button.hide();
|
||||
$input.show();
|
||||
$input.focus();
|
||||
|
||||
var typeahead = $input.data('typeahead');
|
||||
if (typeahead) {
|
||||
$input.val('');
|
||||
typeahead.lookup();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
$input.blur(function() {
|
||||
if ($input.val() !== '') { updateVariableValue($input.val()); }
|
||||
$input.hide();
|
||||
$button.show();
|
||||
$button.focus();
|
||||
});
|
||||
|
||||
$compile(elem.contents())($scope);
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -6,15 +6,38 @@ function (angular, kbn) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.module('grafana.directives')
|
||||
.directive('tip', function($compile) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function(scope, elem, attrs) {
|
||||
var _t = '<i class="icon-'+(attrs.icon||'question-sign')+'" bs-tooltip="\''+
|
||||
var _t = '<i class="grafana-tip icon-'+(attrs.icon||'question-sign')+'" bs-tooltip="\''+
|
||||
kbn.addslashes(elem.text())+'\'"></i>';
|
||||
elem.replaceWith($compile(angular.element(_t))(scope));
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
angular
|
||||
.module('grafana.directives')
|
||||
.directive('editorOptBool', function($compile) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function(scope, elem, attrs) {
|
||||
var ngchange = attrs.change ? (' ng-change="' + attrs.change + '"') : '';
|
||||
var tip = attrs.tip ? (' <tip>' + attrs.tip + '</tip>') : '';
|
||||
var showIf = attrs.showIf ? (' ng-show="' + attrs.showIf + '" ') : '';
|
||||
|
||||
var template = '<div class="editor-option text-center"' + showIf + '>' +
|
||||
' <label for="' + attrs.model + '" class="small">' +
|
||||
attrs.text + tip + '</label>' +
|
||||
'<input class="cr1" id="' + attrs.model + '" type="checkbox" ' +
|
||||
' ng-model="' + attrs.model + '"' + ngchange +
|
||||
' ng-checked="' + attrs.model + '"></input>' +
|
||||
' <label for="' + attrs.model + '" class="cr1"></label>';
|
||||
elem.replaceWith($compile(angular.element(template))(scope));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
3
src/app/features/all.js
Normal file
3
src/app/features/all.js
Normal file
@@ -0,0 +1,3 @@
|
||||
define([
|
||||
'./panellinkeditor/module',
|
||||
], function () {});
|
||||
39
src/app/features/panellinkeditor/linkSrv.js
Normal file
39
src/app/features/panellinkeditor/linkSrv.js
Normal file
@@ -0,0 +1,39 @@
|
||||
define([
|
||||
'angular',
|
||||
'kbn',
|
||||
],
|
||||
function (angular, kbn) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('grafana.services')
|
||||
.service('linkSrv', function(templateSrv, timeSrv) {
|
||||
|
||||
this.getPanelLinkAnchorInfo = function(link) {
|
||||
var info = {};
|
||||
if (link.type === 'absolute') {
|
||||
info.target = '_blank';
|
||||
info.href = templateSrv.replace(link.url || '');
|
||||
info.title = templateSrv.replace(link.title || '');
|
||||
info.href += '?';
|
||||
|
||||
}
|
||||
else {
|
||||
info.title = templateSrv.replace(link.title || '');
|
||||
var slug = kbn.slugifyForUrl(link.dashboard || '');
|
||||
info.href = '#dashboard/db/' + slug + '?';
|
||||
}
|
||||
|
||||
var range = timeSrv.timeRangeForUrl();
|
||||
info.href += 'from=' + range.from;
|
||||
info.href += '&to=' + range.to;
|
||||
|
||||
if (link.params) {
|
||||
info.href += "&" + templateSrv.replace(link.params);
|
||||
}
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
51
src/app/features/panellinkeditor/module.html
Normal file
51
src/app/features/panellinkeditor/module.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<div class="editor-row">
|
||||
<div class="section">
|
||||
<h5>Drilldown / detail link<tip>These links appear in the dropdown menu in the panel menu. </tip></h5>
|
||||
|
||||
<div class="grafana-target" ng-repeat="link in panel.links"j>
|
||||
<div class="grafana-target-inner">
|
||||
<ul class="grafana-segment-list">
|
||||
<li class="grafana-target-segment">
|
||||
<i class="icon-remove pointer" ng-click="deleteLink(link)"></i>
|
||||
</li>
|
||||
|
||||
<li class="grafana-target-segment">title</li>
|
||||
<li>
|
||||
<input type="text" ng-model="link.title" class="input-medium grafana-target-segment-input">
|
||||
</li>
|
||||
|
||||
<li class="grafana-target-segment">type</li>
|
||||
<li>
|
||||
<select class="input-medium grafana-target-segment-input" style="width: 101px;" ng-model="link.type" ng-options="f for f in ['dashboard','absolute']"></select>
|
||||
</li>
|
||||
|
||||
<li class="grafana-target-segment" ng-show="link.type === 'dashboard'">dashboard</li>
|
||||
<li ng-show="link.type === 'dashboard'">
|
||||
<input type="text"
|
||||
ng-model="link.dashboard"
|
||||
bs-typeahead="searchDashboards"
|
||||
class="input-large grafana-target-segment-input">
|
||||
</li>
|
||||
|
||||
<li class="grafana-target-segment" ng-show="link.type === 'absolute'">url</li>
|
||||
<li ng-show="link.type === 'absolute'">
|
||||
<input type="text" ng-model="link.url" class="input-large grafana-target-segment-input">
|
||||
</li>
|
||||
|
||||
<li class="grafana-target-segment">params
|
||||
<tip>Use var-variableName=value to pass templating variables.</tip>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" ng-model="link.params" class="input-medium grafana-target-segment-input">
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<br>
|
||||
<button class="btn btn-success" ng-click="addLink()">Add link</button>
|
||||
</div>
|
||||
51
src/app/features/panellinkeditor/module.js
Normal file
51
src/app/features/panellinkeditor/module.js
Normal file
@@ -0,0 +1,51 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'./linkSrv',
|
||||
],
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('grafana.directives')
|
||||
.directive('panelLinkEditor', function() {
|
||||
return {
|
||||
scope: {
|
||||
panel: "="
|
||||
},
|
||||
restrict: 'E',
|
||||
controller: 'PanelLinkEditorCtrl',
|
||||
templateUrl: 'app/features/panellinkeditor/module.html',
|
||||
link: function() {
|
||||
}
|
||||
};
|
||||
}).controller('PanelLinkEditorCtrl', function($scope, datasourceSrv) {
|
||||
|
||||
$scope.panel.links = $scope.panel.links || [];
|
||||
|
||||
$scope.addLink = function() {
|
||||
$scope.panel.links.push({
|
||||
type: 'dashboard',
|
||||
name: 'Drilldown dashboard'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.searchDashboards = function(query, callback) {
|
||||
var ds = datasourceSrv.getGrafanaDB();
|
||||
if (ds === null) { return; }
|
||||
|
||||
ds.searchDashboards(query).then(function(result) {
|
||||
var dashboards = _.map(result.dashboards, function(dash) {
|
||||
return dash.title;
|
||||
});
|
||||
|
||||
callback(dashboards);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteLink = function(link) {
|
||||
$scope.panel.links = _.without($scope.panel.links, link);
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
define(['angular', 'jquery', 'underscore', 'moment'], function (angular, $, _, moment) {
|
||||
define(['angular', 'jquery', 'lodash', 'moment'], function (angular, $, _, moment) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.filters');
|
||||
var module = angular.module('grafana.filters');
|
||||
|
||||
module.filter('stringSort', function() {
|
||||
return function(input) {
|
||||
@@ -9,18 +9,6 @@ define(['angular', 'jquery', 'underscore', 'moment'], function (angular, $, _, m
|
||||
};
|
||||
});
|
||||
|
||||
/*
|
||||
Filter an array of objects by elasticsearch version requirements
|
||||
*/
|
||||
module.filter('esVersion', function(esVersion) {
|
||||
return function(items, require) {
|
||||
var ret = _.filter(items,function(qt) {
|
||||
return esVersion.is(qt[require]) ? true : false;
|
||||
});
|
||||
return ret;
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('slice', function() {
|
||||
return function(arr, start, end) {
|
||||
if(!_.isUndefined(arr)) {
|
||||
@@ -67,57 +55,14 @@ define(['angular', 'jquery', 'underscore', 'moment'], function (angular, $, _, m
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('urlLink', function() {
|
||||
var //URLs starting with http://, https://, or ftp://
|
||||
r1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim,
|
||||
//URLs starting with "www." (without // before it, or it'd re-link the ones done above).
|
||||
r2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim,
|
||||
//Change email addresses to mailto:: links.
|
||||
r3 = /(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,6})/gim;
|
||||
module.filter('interpolateTemplateVars', function(templateSrv) {
|
||||
function interpolateTemplateVars(text) {
|
||||
return templateSrv.replaceWithText(text);
|
||||
}
|
||||
|
||||
var urlLink = function(text) {
|
||||
var t1,t2,t3;
|
||||
if(!_.isString(text)) {
|
||||
return text;
|
||||
} else {
|
||||
_.each(text.match(r1), function() {
|
||||
t1 = text.replace(r1, "<a href=\"$1\" target=\"_blank\">$1</a>");
|
||||
});
|
||||
text = t1 || text;
|
||||
_.each(text.match(r2), function() {
|
||||
t2 = text.replace(r2, "$1<a href=\"http://$2\" target=\"_blank\">$2</a>");
|
||||
});
|
||||
text = t2 || text;
|
||||
_.each(text.match(r3), function() {
|
||||
t3 = text.replace(r3, "<a href=\"mailto:$1\">$1</a>");
|
||||
});
|
||||
text = t3 || text;
|
||||
return text;
|
||||
}
|
||||
};
|
||||
return function(text) {
|
||||
return _.isArray(text)
|
||||
? _.map(text, urlLink)
|
||||
: urlLink(text);
|
||||
};
|
||||
interpolateTemplateVars.$stateful = true;
|
||||
|
||||
return interpolateTemplateVars;
|
||||
});
|
||||
|
||||
module.filter('gistid', function() {
|
||||
var gist_pattern = /(\d{5,})|([a-z0-9]{10,})|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/;
|
||||
return function(input) {
|
||||
if(!(_.isUndefined(input))) {
|
||||
var output = input.match(gist_pattern);
|
||||
if(!_.isNull(output) && !_.isUndefined(output)) {
|
||||
return output[0].replace(/.*\//, '');
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('urlDecode', function() {
|
||||
return function(input) {
|
||||
return decodeURIComponent(input);
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
<div bindonce class="modal-body">
|
||||
<div class="pull-right editor-title">Annotations</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<table class="table table-striped annotation-editor-table" style="width: 700px">
|
||||
<thead>
|
||||
<th width="90%">Name</th>
|
||||
<th width="1%"></th>
|
||||
<th width="1%"></th>
|
||||
<th width="1%"></th>
|
||||
</thead>
|
||||
<tr ng-repeat="annotation in panel.annotations">
|
||||
<td>
|
||||
<a ng-click="edit(annotation)" bs-tooltip="'Click to edit'">
|
||||
<i class="icon-cog"></i>
|
||||
{{annotation.name}}
|
||||
</a>
|
||||
</td>
|
||||
<td><i ng-click="_.move(panel.annotations,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
|
||||
<td><i ng-click="_.move(panel.annotations,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
|
||||
<td><i ng-click="panel.annotations = _.without(panel.annotations, annotation)" class="pointer icon-remove"></i></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<h4 ng-show="currentIsNew">Add Annotation</h4>
|
||||
<h4 ng-show="!currentIsNew">Edit Annotation</h4>
|
||||
|
||||
<div class="editor-option">
|
||||
<label class="small">Name</label>
|
||||
<input type="text" class="input-medium" ng-model='currentAnnnotation.name' placeholder="name"></input>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Type</label>
|
||||
<select ng-model="currentAnnnotation.type" ng-options="f for f in ['graphite metric', 'graphite events']"></select>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Icon color</label>
|
||||
<spectrum-picker ng-model="currentAnnnotation.iconColor"></spectrum-picker>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Icon size</label>
|
||||
<select class="input-mini" ng-model="currentAnnnotation.iconSize" ng-options="f for f in [7,8,9,10,13,15,17,20,25,30]"></select>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Grid line</label>
|
||||
<input type="checkbox" ng-model="currentAnnnotation.showLine" ng-checked="currentAnnnotation.showLine">
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Line color</label>
|
||||
<spectrum-picker ng-model="currentAnnnotation.lineColor"></spectrum-picker>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-row" ng-if="currentAnnnotation.type === 'graphite metric'">
|
||||
<div class="editor-option">
|
||||
<label class="small">Graphite target expression</label>
|
||||
<input type="text" class="span10" ng-model='currentAnnnotation.target' placeholder=""></input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-row" ng-if="currentAnnnotation.type === 'graphite events'">
|
||||
<div class="editor-option">
|
||||
<label class="small">Graphite event tags</label>
|
||||
<input type="text" ng-model='currentAnnnotation.tags' placeholder=""></input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button ng-show="currentIsNew" type="button" class="btn btn-success" ng-click="add()">Add annotation</button>
|
||||
<button ng-show="!currentIsNew" type="button" class="btn btn-success" ng-click="update()">Update</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="close_edit();dismiss();dashboard.refresh();">Close</button>
|
||||
</div>
|
||||
@@ -1,12 +0,0 @@
|
||||
<div ng-controller='AnnotationsCtrl' ng-init="init()">
|
||||
|
||||
<div class="submenu-toggle" ng-repeat="annotation in panel.annotations" ng-class="{'annotation-disabled': !annotation.enable }">
|
||||
<i class="annotation-color-icon icon-minus"></i>
|
||||
<a ng-click="hide(annotation)" class="small">{{annotation.name}}</a>
|
||||
</div>
|
||||
|
||||
<div class="submenu-control-edit">
|
||||
<i class="icon-cog pointer" config-modal="app/panels/annotations/editor.html" bs-tooltip="'Edit annotations'" ></i>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
|
||||
## annotations
|
||||
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore'
|
||||
],
|
||||
function (angular, app, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.annotations', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('AnnotationsCtrl', function($scope, dashboard, $rootScope) {
|
||||
|
||||
$scope.panelMeta = {
|
||||
status : "Stable",
|
||||
description : "Annotations"
|
||||
};
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
annotations: []
|
||||
};
|
||||
|
||||
var annotationDefaults = {
|
||||
name: '',
|
||||
type: 'graphite metric',
|
||||
showLine: true,
|
||||
iconColor: '#C0C6BE',
|
||||
lineColor: 'rgba(255, 96, 96, 0.592157)',
|
||||
iconSize: 13,
|
||||
enable: true
|
||||
};
|
||||
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.currentAnnnotation = angular.copy(annotationDefaults);
|
||||
$scope.currentIsNew = true;
|
||||
};
|
||||
|
||||
$scope.edit = function(annotation) {
|
||||
$scope.currentAnnnotation = annotation;
|
||||
$scope.currentIsNew = false;
|
||||
};
|
||||
|
||||
$scope.update = function() {
|
||||
$scope.currentAnnnotation = angular.copy(annotationDefaults);
|
||||
$scope.currentIsNew = true;
|
||||
};
|
||||
|
||||
$scope.add = function() {
|
||||
$scope.panel.annotations.push($scope.currentAnnnotation);
|
||||
$scope.currentAnnnotation = angular.copy(annotationDefaults);
|
||||
};
|
||||
|
||||
$scope.hide = function (annotation) {
|
||||
annotation.enable = !annotation.enable;
|
||||
$rootScope.$broadcast('refresh');
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
<div ng-controller='filtering' ng-init="init()">
|
||||
|
||||
<div class='filtering-container'>
|
||||
|
||||
<div ng-repeat="filter in filter.templateParameters" class="small filter-panel-filter">
|
||||
<div>
|
||||
<i class="filter-action pointer icon-remove" bs-tooltip="'Remove'" ng-click="remove(filter)"></i>
|
||||
<i class="filter-action pointer icon-edit" ng-hide="filter.editing" bs-tooltip="'Edit'" ng-click="filter.editing = true"></i>
|
||||
</div>
|
||||
|
||||
<div ng-hide="filter.editing" style="margin-right: 45px;">
|
||||
<ul class="unstyled">
|
||||
<li ng-if="filter.name" class="dropdown">
|
||||
{{filter.name}} :
|
||||
<a class="dropdown-toggle" data-toggle="dropdown">
|
||||
{{filter.current.text}}
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="option in filter.options">
|
||||
<a ng-click="filterOptionSelected(filter, option)">{{option.text | urlDecode}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form ng-show="filter.editing">
|
||||
<ul class="unstyled">
|
||||
<li>
|
||||
<strong>name</strong>:<br/>
|
||||
<input type='text' ng-model="filter.name">
|
||||
</li>
|
||||
<li>
|
||||
<strong>filter.query</strong>:<br/>
|
||||
<input type='text' ng-model="filter.query">
|
||||
</li>
|
||||
<li>
|
||||
<label for="includeAll">Include all:</label>
|
||||
<input id="includeAll" type='checkbox' ng-model="filter.includeAll">
|
||||
</li>
|
||||
</ul>
|
||||
<div>
|
||||
<input type="submit" value="Update" ng-click="applyFilter(filter)" class="filter-apply btn btn-success btn-mini" bs-tooltip="'Update and refresh'"/>
|
||||
<button ng-click="filter.editing=undefined" class="filter-apply btn btn-mini" bs-tooltip="'Save without refresh'">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<i class="pointer icon-plus-sign add-filter-action" ng-click="add()" bs-tooltip="'Add metric filter / param'" data-placement="right"></i>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
|
||||
## filtering
|
||||
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore'
|
||||
],
|
||||
function (angular, app, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.filtering', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('filtering', function($scope, datasourceSrv, $rootScope, $timeout, $q) {
|
||||
|
||||
$scope.panelMeta = {
|
||||
status : "Stable",
|
||||
description : "graphite target filters"
|
||||
};
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
};
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
$scope.init = function() {
|
||||
// empty. Don't know if I need the function then.
|
||||
};
|
||||
|
||||
$scope.remove = function(templateParameter) {
|
||||
$scope.filter.removeTemplateParameter(templateParameter);
|
||||
};
|
||||
|
||||
$scope.filterOptionSelected = function(templateParameter, option, recursive) {
|
||||
templateParameter.current = option;
|
||||
|
||||
$scope.filter.updateTemplateData();
|
||||
|
||||
return $scope.applyFilterToOtherFilters(templateParameter)
|
||||
.then(function() {
|
||||
// only refresh in the outermost call
|
||||
if (!recursive) {
|
||||
$scope.dashboard.refresh();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.applyFilterToOtherFilters = function(updatedTemplatedParam) {
|
||||
var promises = _.map($scope.filter.templateParameters, function(templateParam) {
|
||||
if (templateParam === updatedTemplatedParam) {
|
||||
return;
|
||||
}
|
||||
if (templateParam.query.indexOf(updatedTemplatedParam.name) !== -1) {
|
||||
return $scope.applyFilter(templateParam);
|
||||
}
|
||||
});
|
||||
|
||||
return $q.all(promises);
|
||||
};
|
||||
|
||||
$scope.applyFilter = function(templateParam) {
|
||||
return datasourceSrv.default.metricFindQuery($scope.filter, templateParam.query)
|
||||
.then(function (results) {
|
||||
templateParam.editing = undefined;
|
||||
templateParam.options = _.map(results, function(node) {
|
||||
return { text: node.text, value: node.text };
|
||||
});
|
||||
|
||||
if (templateParam.includeAll) {
|
||||
var allExpr = '{';
|
||||
_.each(templateParam.options, function(option) {
|
||||
allExpr += option.text + ',';
|
||||
});
|
||||
allExpr = allExpr.substring(0, allExpr.length - 1) + '}';
|
||||
templateParam.options.unshift({text: 'All', value: allExpr});
|
||||
}
|
||||
|
||||
// if parameter has current value
|
||||
// if it exists in options array keep value
|
||||
if (templateParam.current) {
|
||||
var currentExists = _.findWhere(templateParam.options, { value: templateParam.current.value });
|
||||
if (currentExists) {
|
||||
return $scope.filterOptionSelected(templateParam, templateParam.current, true);
|
||||
}
|
||||
}
|
||||
|
||||
return $scope.filterOptionSelected(templateParam, templateParam.options[0], true);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.add = function() {
|
||||
$scope.filter.addTemplateParameter({
|
||||
type : 'filter',
|
||||
name : 'filter name',
|
||||
editing : true,
|
||||
query : 'metric.path.query.*',
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
@@ -1,48 +1,59 @@
|
||||
|
||||
<div class="editor-row">
|
||||
|
||||
<div class="section">
|
||||
<h5>Axes</h5>
|
||||
<div class="editor-option">
|
||||
<label class="small">X-Axis</label><input type="checkbox" ng-model="panel['x-axis']" ng-checked="panel['x-axis']" ng-change="render()">
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Y-Axis</label><input type="checkbox" ng-model="panel['y-axis']" ng-checked="panel['y-axis']" ng-change="render()">
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Left Y Format <tip>Y-axis formatting</tip></label>
|
||||
<select class="input-small" ng-model="panel.y_formats[0]" ng-options="f for f in ['none','short','bytes', 'bits', 's', 'ms', 'µs', 'ns']" ng-change="render()"></select>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Right Y Format <tip>Y-axis formatting</tip></label>
|
||||
<select class="input-small" ng-model="panel.y_formats[1]" ng-options="f for f in ['none','short','bytes', 'bits', 's', 'ms', 'µs', 'ns']" ng-change="render()"></select>
|
||||
</div>
|
||||
|
||||
<div class="editor-option">
|
||||
<label class="small">Left Y-axis label</label>
|
||||
<input ng-change="get_data()" ng-model-onblur placeholder="" type="text" class="input-medium" ng-model="panel.leftYAxisLabel">
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Right Y-axis label</label>
|
||||
<input ng-change="get_data()" ng-model-onblur placeholder="" type="text" class="input-medium" ng-model="panel.rightYAxisLabel">
|
||||
</div>
|
||||
|
||||
<h5>Left Y Axis</h5>
|
||||
<div class="editor-option">
|
||||
<label class="small">Format <tip>Y-axis formatting</tip></label>
|
||||
<select class="input-small" ng-model="panel.y_formats[0]" ng-options="f for f in ['none','short','bytes', 'bits', 'bps', 's', 'ms', 'µs', 'ns', 'percent']" ng-change="render()"></select>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Min / <a ng-click="toggleGridMinMax('leftMin')">Auto <i class="icon-star" ng-show="_.isNull(panel.grid.leftMin)"></i></a></label>
|
||||
<input type="number" class="input-small" ng-model="panel.grid.leftMin" ng-change="render()" ng-model-onblur />
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Max / <a ng-click="toggleGridMinMax('leftMax')">Auto <i class="icon-star" ng-show="_.isNull(panel.grid.leftMax)"></i></a></label>
|
||||
<input type="number" class="input-small" ng-model="panel.grid.leftMax" ng-change="render()" ng-model-onblur />
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Label</label>
|
||||
<input ng-change="get_data()" ng-model-onblur placeholder="" type="text" class="input-medium" ng-model="panel.leftYAxisLabel">
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h5>Right Y Axis</h5>
|
||||
<div class="editor-option">
|
||||
<label class="small">Format <tip>Y-axis formatting</tip></label>
|
||||
<select class="input-small" ng-model="panel.y_formats[1]" ng-options="f for f in ['none','short','bytes', 'bits', 'bps', 's', 'ms', 'µs', 'ns', 'percent']" ng-change="render()"></select>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Min / <a ng-click="toggleGridMinMax('rightMin')">Auto <i class="icon-star" ng-show="_.isNull(panel.grid.rightMin)"></i></a></label>
|
||||
<input type="number" class="input-small" ng-model="panel.grid.rightMin" ng-change="render()" ng-model-onblur />
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Max / <a ng-click="toggleGridMinMax('rightMax')">Auto <i class="icon-star" ng-show="_.isNull(panel.grid.rightMax)"></i></a></label>
|
||||
<input type="number" class="input-small" ng-model="panel.grid.rightMax" ng-change="render()" ng-model-onblur />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="section">
|
||||
<h5>Grid</h5>
|
||||
<div class="editor-option">
|
||||
<label class="small">Min / <a ng-click="toggleGridMinMax('min')">Auto <i class="icon-star" ng-show="_.isNull(panel.grid.min)"></i></a></label>
|
||||
<input type="number" class="input-small" ng-model="panel.grid.min" ng-change="render()" ng-model-onblur />
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Max / <a ng-click="toggleGridMinMax('max')">Auto <i class="icon-star" ng-show="_.isNull(panel.grid.max)"></i></a></label>
|
||||
<input type="number" class="input-small" ng-model="panel.grid.max" ng-change="render()" ng-model-onblur />
|
||||
</div>
|
||||
<h5>Legend styles</h5>
|
||||
<editor-opt-bool text="Show" model="panel.legend.show" change="get_data();"></editor-opt-bool>
|
||||
<editor-opt-bool text="Values" model="panel.legend.values" change="render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Table" model="panel.legend.alignAsTable" change="render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Right side" model="panel.legend.rightSide" change="render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Hide empty" model="panel.legend.hideEmpty" tip="Hides series with only null values" change="render()"></editor-opt-bool>
|
||||
</div>
|
||||
|
||||
<div class="section" ng-if="panel.legend.values">
|
||||
<h5>Legend values</h5>
|
||||
<editor-opt-bool text="Min" model="panel.legend.min" change="render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Max" model="panel.legend.max" change="render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Current" model="panel.legend.current" change="render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Total" model="panel.legend.total" change="render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Avg" model="panel.legend.avg" change="render()"></editor-opt-bool>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
@@ -63,43 +74,13 @@
|
||||
<label class="small">Color</label>
|
||||
<spectrum-picker ng-model="panel.grid.threshold2Color" ng-change="render()" ></spectrum-picker>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Line mode</label><input type="checkbox" ng-model="panel.grid.thresholdLine" ng-checked="panel.grid.thresholdLine" ng-change="render();">
|
||||
</div>
|
||||
<editor-opt-bool text="Line mode" model="panel.grid.thresholdLine" change="render()"></editor-opt-bool>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h5>Legend</h5>
|
||||
<div class="editor-option">
|
||||
<label class="small">Show Legend</label><input type="checkbox" ng-model="panel.legend.show" ng-checked="panel.legend.show" ng-change="render();">
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Include Values</label><input type="checkbox" ng-model="panel.legend.values" ng-checked="panel.legend.values" ng-change="render();">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" ng-if="panel.legend.values">
|
||||
<h5>Legend values</h5>
|
||||
<div class="editor-option">
|
||||
<label class="small">Min</label><input type="checkbox" ng-model="panel.legend.min" ng-checked="panel.legend.min" ng-change="render();">
|
||||
</div>
|
||||
|
||||
<div class="editor-option">
|
||||
<label class="small">Max</label><input type="checkbox" ng-model="panel.legend.max" ng-checked="panel.legend.max" ng-change="render();">
|
||||
</div>
|
||||
|
||||
<div class="editor-option">
|
||||
<label class="small">Current</label><input type="checkbox" ng-model="panel.legend.current" ng-checked="panel.legend.current" ng-change="render();">
|
||||
</div>
|
||||
|
||||
<div class="editor-option">
|
||||
<label class="small">Total</label><input type="checkbox" ng-model="panel.legend.total" ng-checked="panel.legend.total" ng-change="render();">
|
||||
</div>
|
||||
|
||||
<div class="editor-option">
|
||||
<label class="small">Avg</label><input type="checkbox" ng-model="panel.legend.avg" ng-checked="panel.legend.avg" ng-change="render();">
|
||||
</div>
|
||||
|
||||
<h5>Show Axes</h5>
|
||||
<editor-opt-bool text="X-Axis" model="panel['x-axis']" change="render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Y-axis" model="panel['y-axis']" change="render()"></editor-opt-bool>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -3,46 +3,66 @@ define([
|
||||
'jquery',
|
||||
'kbn',
|
||||
'moment',
|
||||
'underscore'
|
||||
'lodash',
|
||||
'./graph.tooltip',
|
||||
'jquery.flot',
|
||||
'jquery.flot.events',
|
||||
'jquery.flot.selection',
|
||||
'jquery.flot.time',
|
||||
'jquery.flot.stack',
|
||||
'jquery.flot.stackpercent',
|
||||
'jquery.flot.fillbelow',
|
||||
'jquery.flot.crosshair'
|
||||
],
|
||||
function (angular, $, kbn, moment, _) {
|
||||
function (angular, $, kbn, moment, _, GraphTooltip) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.directives');
|
||||
var module = angular.module('grafana.directives');
|
||||
|
||||
module.directive('grafanaGraph', function($rootScope, dashboard) {
|
||||
module.directive('grafanaGraph', function($rootScope, timeSrv) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
template: '<div> </div>',
|
||||
link: function(scope, elem) {
|
||||
var data, plot, annotations;
|
||||
var hiddenData = {};
|
||||
var dashboard = scope.dashboard;
|
||||
var data, annotations;
|
||||
var sortedSeries;
|
||||
var legendSideLastValue = null;
|
||||
scope.crosshairEmiter = false;
|
||||
|
||||
scope.$on('refresh',function() {
|
||||
if (scope.otherPanelInFullscreenMode()) { return; }
|
||||
scope.get_data();
|
||||
scope.onAppEvent('setCrosshair', function(event, info) {
|
||||
// do not need to to this if event is from this panel
|
||||
if (info.scope === scope) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(dashboard.sharedCrosshair) {
|
||||
var plot = elem.data().plot;
|
||||
if (plot) {
|
||||
plot.setCrosshair({ x: info.pos.x, y: info.pos.y });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
scope.$on('toggleLegend', function(e, series) {
|
||||
_.each(series, function(serie) {
|
||||
if (hiddenData[serie.alias]) {
|
||||
data.push(hiddenData[serie.alias]);
|
||||
delete hiddenData[serie.alias];
|
||||
}
|
||||
});
|
||||
scope.onAppEvent('clearCrosshair', function() {
|
||||
var plot = elem.data().plot;
|
||||
if (plot) {
|
||||
plot.clearCrosshair();
|
||||
}
|
||||
});
|
||||
|
||||
render_panel();
|
||||
scope.$on('refresh', function() {
|
||||
scope.get_data();
|
||||
});
|
||||
|
||||
// Receive render events
|
||||
scope.$on('render',function(event, renderData) {
|
||||
data = renderData || data;
|
||||
annotations = data.annotations;
|
||||
render_panel();
|
||||
});
|
||||
|
||||
// Re-render if the window is resized
|
||||
angular.element(window).bind('resize', function() {
|
||||
if (!data) {
|
||||
scope.get_data();
|
||||
return;
|
||||
}
|
||||
annotations = data.annotations || annotations;
|
||||
render_panel();
|
||||
});
|
||||
|
||||
@@ -53,10 +73,11 @@ function (angular, $, kbn, moment, _) {
|
||||
height = parseInt(height.replace('px', ''), 10);
|
||||
}
|
||||
|
||||
height = height - 32; // subtract panel title bar
|
||||
height -= 5; // padding
|
||||
height -= scope.panel.title ? 24 : 9; // subtract panel title bar
|
||||
|
||||
if (scope.panel.legend.show) {
|
||||
height = height - 21; // subtract one line legend
|
||||
if (scope.panel.legend.show && !scope.panel.legend.rightSide) {
|
||||
height = height - 26; // subtract one line legend
|
||||
}
|
||||
|
||||
elem.css('height', height + 'px');
|
||||
@@ -82,6 +103,27 @@ function (angular, $, kbn, moment, _) {
|
||||
render_panel_as_graphite_png(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (elem.width() === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function updateLegendValues(plot) {
|
||||
var yaxis = plot.getYAxes();
|
||||
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var series = data[i];
|
||||
var axis = yaxis[series.yaxis - 1];
|
||||
var formater = kbn.valueFormats[scope.panel.y_formats[series.yaxis - 1]];
|
||||
|
||||
// legend and tooltip gets one more decimal precision
|
||||
// than graph legend ticks
|
||||
var tickDecimals = (axis.tickDecimals || -1) + 1;
|
||||
|
||||
series.updateLegendValues(formater, tickDecimals, axis.scaledDecimals + 2);
|
||||
if(!scope.$$phase) { scope.$digest(); }
|
||||
}
|
||||
}
|
||||
|
||||
// Function for rendering panel
|
||||
@@ -91,21 +133,11 @@ function (angular, $, kbn, moment, _) {
|
||||
}
|
||||
|
||||
var panel = scope.panel;
|
||||
|
||||
_.each(_.keys(scope.hiddenSeries), function(seriesAlias) {
|
||||
var dataSeries = _.find(data, function(series) {
|
||||
return series.info.alias === seriesAlias;
|
||||
});
|
||||
if (dataSeries) {
|
||||
hiddenData[dataSeries.info.alias] = dataSeries;
|
||||
data = _.without(data, dataSeries);
|
||||
}
|
||||
});
|
||||
|
||||
var stack = panel.stack ? true : null;
|
||||
|
||||
// Populate element
|
||||
var options = {
|
||||
hooks: { draw: [updateLegendValues] },
|
||||
legend: { show: false },
|
||||
series: {
|
||||
stackpercent: panel.stack ? panel.percentage : false,
|
||||
@@ -113,7 +145,7 @@ function (angular, $, kbn, moment, _) {
|
||||
lines: {
|
||||
show: panel.lines,
|
||||
zero: false,
|
||||
fill: panel.fill === 0 ? 0.001 : panel.fill/10,
|
||||
fill: translateFillOption(panel.fill),
|
||||
lineWidth: panel.linewidth,
|
||||
steps: panel.steppedLine
|
||||
},
|
||||
@@ -128,13 +160,15 @@ function (angular, $, kbn, moment, _) {
|
||||
show: panel.points,
|
||||
fill: 1,
|
||||
fillColor: false,
|
||||
radius: panel.pointradius
|
||||
radius: panel.points ? panel.pointradius : 2
|
||||
// little points when highlight points
|
||||
},
|
||||
shadowSize: 1
|
||||
},
|
||||
yaxes: [],
|
||||
xaxis: {},
|
||||
grid: {
|
||||
minBorderMargin: 0,
|
||||
markings: [],
|
||||
backgroundColor: null,
|
||||
borderWidth: 0,
|
||||
@@ -144,16 +178,26 @@ function (angular, $, kbn, moment, _) {
|
||||
selection: {
|
||||
mode: "x",
|
||||
color: '#666'
|
||||
},
|
||||
crosshair: {
|
||||
mode: panel.tooltip.shared || dashboard.sharedCrosshair ? "x" : null
|
||||
}
|
||||
};
|
||||
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var _d = data[i].getFlotPairs(panel.nullPointMode, panel.y_formats);
|
||||
data[i].data = _d;
|
||||
var series = data[i];
|
||||
series.applySeriesOverrides(panel.seriesOverrides);
|
||||
series.data = series.getFlotPairs(panel.nullPointMode, panel.y_formats);
|
||||
|
||||
// if hidden remove points and disable stack
|
||||
if (scope.hiddenSeries[series.alias]) {
|
||||
series.data = [];
|
||||
series.stack = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (panel.bars && data.length && data[0].info.timeStep) {
|
||||
options.series.bars.barWidth = data[0].info.timeStep / 1.5;
|
||||
if (data.length && data[0].stats.timeStep) {
|
||||
options.series.bars.barWidth = data[0].stats.timeStep / 1.5;
|
||||
}
|
||||
|
||||
addTimeAxis(options);
|
||||
@@ -161,9 +205,41 @@ function (angular, $, kbn, moment, _) {
|
||||
addAnnotations(options);
|
||||
configureAxisOptions(data, options);
|
||||
|
||||
plot = $.plot(elem, data, options);
|
||||
sortedSeries = _.sortBy(data, function(series) { return series.zindex; });
|
||||
|
||||
addAxisLabels();
|
||||
function callPlot() {
|
||||
try {
|
||||
$.plot(elem, sortedSeries, options);
|
||||
} catch (e) {
|
||||
console.log('flotcharts error', e);
|
||||
}
|
||||
|
||||
addAxisLabels();
|
||||
}
|
||||
|
||||
if (shouldDelayDraw(panel)) {
|
||||
// temp fix for legends on the side, need to render twice to get dimensions right
|
||||
callPlot();
|
||||
setTimeout(callPlot, 50);
|
||||
legendSideLastValue = panel.legend.rightSide;
|
||||
}
|
||||
else {
|
||||
callPlot();
|
||||
}
|
||||
}
|
||||
|
||||
function translateFillOption(fill) {
|
||||
return fill === 0 ? 0.001 : fill/10;
|
||||
}
|
||||
|
||||
function shouldDelayDraw(panel) {
|
||||
if (panel.legend.rightSide) {
|
||||
return true;
|
||||
}
|
||||
if (legendSideLastValue !== null && panel.legend.rightSide !== legendSideLastValue) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function addTimeAxis(options) {
|
||||
@@ -172,7 +248,7 @@ function (angular, $, kbn, moment, _) {
|
||||
var max = _.isUndefined(scope.range.to) ? null : scope.range.to.getTime();
|
||||
|
||||
options.xaxis = {
|
||||
timezone: dashboard.current.timezone,
|
||||
timezone: dashboard.timezone,
|
||||
show: scope.panel['x-axis'],
|
||||
mode: "time",
|
||||
min: min,
|
||||
@@ -258,8 +334,8 @@ function (angular, $, kbn, moment, _) {
|
||||
var defaults = {
|
||||
position: 'left',
|
||||
show: scope.panel['y-axis'],
|
||||
min: scope.panel.grid.min,
|
||||
max: scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.max,
|
||||
min: scope.panel.grid.leftMin,
|
||||
max: scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.leftMax,
|
||||
};
|
||||
|
||||
options.yaxes.push(defaults);
|
||||
@@ -267,6 +343,8 @@ function (angular, $, kbn, moment, _) {
|
||||
if (_.findWhere(data, {yaxis: 2})) {
|
||||
var secondY = _.clone(defaults);
|
||||
secondY.position = 'right';
|
||||
secondY.min = scope.panel.grid.rightMin;
|
||||
secondY.max = scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.rightMax;
|
||||
options.yaxes.push(secondY);
|
||||
configureAxisMode(options.yaxes[1], scope.panel.y_formats[1]);
|
||||
}
|
||||
@@ -275,9 +353,9 @@ function (angular, $, kbn, moment, _) {
|
||||
}
|
||||
|
||||
function configureAxisMode(axis, format) {
|
||||
if (format !== 'none') {
|
||||
axis.tickFormatter = kbn.getFormatFunction(format, 1);
|
||||
}
|
||||
axis.tickFormatter = function(val, axis) {
|
||||
return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
|
||||
};
|
||||
}
|
||||
|
||||
function time_format(interval, ticks, min, max) {
|
||||
@@ -302,57 +380,19 @@ function (angular, $, kbn, moment, _) {
|
||||
return "%H:%M";
|
||||
}
|
||||
|
||||
var $tooltip = $('<div>');
|
||||
|
||||
elem.bind("plothover", function (event, pos, item) {
|
||||
var group, value, timestamp, seriesInfo, format;
|
||||
|
||||
if (item) {
|
||||
seriesInfo = item.series.info;
|
||||
format = scope.panel.y_formats[seriesInfo.yaxis - 1];
|
||||
|
||||
if (seriesInfo.alias) {
|
||||
group = '<small style="font-size:0.9em;">' +
|
||||
'<i class="icon-circle" style="color:'+item.series.color+';"></i>' + ' ' +
|
||||
(decodeURIComponent(seriesInfo.alias)) +
|
||||
'</small><br>';
|
||||
} else {
|
||||
group = kbn.query_color_dot(item.series.color, 15) + ' ';
|
||||
}
|
||||
|
||||
if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
|
||||
value = item.datapoint[1] - item.datapoint[2];
|
||||
}
|
||||
else {
|
||||
value = item.datapoint[1];
|
||||
}
|
||||
|
||||
value = kbn.getFormatFunction(format, 2)(value);
|
||||
|
||||
timestamp = dashboard.current.timezone === 'browser' ?
|
||||
moment(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss') :
|
||||
moment.utc(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss');
|
||||
$tooltip
|
||||
.html(
|
||||
group + value + " @ " + timestamp
|
||||
)
|
||||
.place_tt(pos.pageX, pos.pageY);
|
||||
} else {
|
||||
$tooltip.detach();
|
||||
}
|
||||
});
|
||||
|
||||
function render_panel_as_graphite_png(url) {
|
||||
url += '&width=' + elem.width();
|
||||
url += '&height=' + elem.css('height').replace('px', '');
|
||||
url += '&bgcolor=1f1f1f'; // @grayDarker & @kibanaPanelBackground
|
||||
url += '&bgcolor=1f1f1f'; // @grayDarker & @grafanaPanelBackground
|
||||
url += '&fgcolor=BBBFC2'; // @textColor & @grayLighter
|
||||
url += scope.panel.stack ? '&areaMode=stacked' : '';
|
||||
url += scope.panel.fill !== 0 ? ('&areaAlpha=' + (scope.panel.fill/10).toFixed(1)) : '';
|
||||
url += scope.panel.linewidth !== 0 ? '&lineWidth=' + scope.panel.linewidth : '';
|
||||
url += scope.panel.legend.show ? '&hideLegend=false' : '&hideLegend=true';
|
||||
url += scope.panel.grid.min !== null ? '&yMin=' + scope.panel.grid.min : '';
|
||||
url += scope.panel.grid.max !== null ? '&yMax=' + scope.panel.grid.max : '';
|
||||
url += scope.panel.grid.leftMin !== null ? '&yMin=' + scope.panel.grid.leftMin : '';
|
||||
url += scope.panel.grid.leftMax !== null ? '&yMax=' + scope.panel.grid.leftMax : '';
|
||||
url += scope.panel.grid.rightMin !== null ? '&yMin=' + scope.panel.grid.rightMin : '';
|
||||
url += scope.panel.grid.rightMax !== null ? '&yMax=' + scope.panel.grid.rightMax : '';
|
||||
url += scope.panel['x-axis'] ? '' : '&hideAxes=true';
|
||||
url += scope.panel['y-axis'] ? '' : '&hideYAxis=true';
|
||||
|
||||
@@ -363,6 +403,9 @@ function (angular, $, kbn, moment, _) {
|
||||
case 'bits':
|
||||
url += '&yUnitSystem=binary';
|
||||
break;
|
||||
case 'bps':
|
||||
url += '&yUnitSystem=si';
|
||||
break;
|
||||
case 'short':
|
||||
url += '&yUnitSystem=si';
|
||||
break;
|
||||
@@ -387,9 +430,13 @@ function (angular, $, kbn, moment, _) {
|
||||
elem.html('<img src="' + url + '"></img>');
|
||||
}
|
||||
|
||||
new GraphTooltip(elem, dashboard, scope, function() {
|
||||
return sortedSeries;
|
||||
});
|
||||
|
||||
elem.bind("plotselected", function (event, ranges) {
|
||||
scope.$apply(function() {
|
||||
scope.filter.setTime({
|
||||
timeSrv.setTime({
|
||||
from : moment.utc(ranges.xaxis.from).toDate(),
|
||||
to : moment.utc(ranges.xaxis.to).toDate(),
|
||||
});
|
||||
200
src/app/panels/graph/graph.tooltip.js
Normal file
200
src/app/panels/graph/graph.tooltip.js
Normal file
@@ -0,0 +1,200 @@
|
||||
define([
|
||||
'jquery',
|
||||
],
|
||||
function ($) {
|
||||
'use strict';
|
||||
|
||||
function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
|
||||
var self = this;
|
||||
|
||||
var $tooltip = $('<div id="tooltip">');
|
||||
|
||||
this.findHoverIndexFromDataPoints = function(posX, series,last) {
|
||||
var ps = series.datapoints.pointsize;
|
||||
var initial = last*ps;
|
||||
var len = series.datapoints.points.length;
|
||||
for (var j = initial; j < len; j += ps) {
|
||||
if (series.datapoints.points[j] > posX) {
|
||||
return Math.max(j - ps, 0)/ps;
|
||||
}
|
||||
}
|
||||
return j/ps - 1;
|
||||
};
|
||||
|
||||
this.findHoverIndexFromData = function(posX, series) {
|
||||
var len = series.data.length;
|
||||
for (var j = 0; j < len; j++) {
|
||||
if (series.data[j][0] > posX) {
|
||||
return Math.max(j - 1, 0);
|
||||
}
|
||||
}
|
||||
return j - 1;
|
||||
};
|
||||
|
||||
this.showTooltip = function(title, innerHtml, pos) {
|
||||
var body = '<div class="graph-tooltip small"><div class="graph-tooltip-time">'+ title + '</div> ' ;
|
||||
body += innerHtml + '</div>';
|
||||
$tooltip.html(body).place_tt(pos.pageX + 20, pos.pageY);
|
||||
};
|
||||
|
||||
this.getMultiSeriesPlotHoverInfo = function(seriesList, pos) {
|
||||
var value, i, series, hoverIndex, seriesTmp;
|
||||
var results = [];
|
||||
|
||||
var pointCount;
|
||||
for (i = 0; i < seriesList.length; i++) {
|
||||
seriesTmp = seriesList[i];
|
||||
if (!seriesTmp.data.length) { continue; }
|
||||
|
||||
if (!pointCount) {
|
||||
series = seriesTmp;
|
||||
pointCount = series.data.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (seriesTmp.data.length !== pointCount) {
|
||||
results.pointCountMismatch = true;
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
hoverIndex = this.findHoverIndexFromData(pos.x, series);
|
||||
var lasthoverIndex = 0;
|
||||
if(!scope.panel.steppedLine) {
|
||||
lasthoverIndex = hoverIndex;
|
||||
}
|
||||
|
||||
//now we know the current X (j) position for X and Y values
|
||||
results.time = series.data[hoverIndex][0];
|
||||
var last_value = 0; //needed for stacked values
|
||||
|
||||
for (i = 0; i < seriesList.length; i++) {
|
||||
series = seriesList[i];
|
||||
|
||||
if (!series.data.length) {
|
||||
results.push({ hidden: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (scope.panel.stack) {
|
||||
if (scope.panel.tooltip.value_type === 'individual') {
|
||||
value = series.data[hoverIndex][1];
|
||||
} else {
|
||||
last_value += series.data[hoverIndex][1];
|
||||
value = last_value;
|
||||
}
|
||||
} else {
|
||||
value = series.data[hoverIndex][1];
|
||||
}
|
||||
|
||||
// Highlighting multiple Points depending on the plot type
|
||||
if (scope.panel.steppedLine || (scope.panel.stack && scope.panel.nullPointMode == "null")) {
|
||||
// stacked and steppedLine plots can have series with different length.
|
||||
// Stacked series can increase its length on each new stacked serie if null points found,
|
||||
// to speed the index search we begin always on the las found hoverIndex.
|
||||
var newhoverIndex = this.findHoverIndexFromDataPoints(pos.x, series,lasthoverIndex);
|
||||
// update lasthoverIndex depends also on the plot type.
|
||||
if(!scope.panel.steppedLine) {
|
||||
// on stacked graphs new will be always greater than last
|
||||
lasthoverIndex = newhoverIndex;
|
||||
} else {
|
||||
// if steppeLine, not always series increases its length, so we should begin
|
||||
// to search correct index from the original hoverIndex on each serie.
|
||||
lasthoverIndex = hoverIndex;
|
||||
}
|
||||
|
||||
results.push({ value: value, hoverIndex: newhoverIndex});
|
||||
} else {
|
||||
results.push({ value: value, hoverIndex: hoverIndex});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
elem.mouseleave(function () {
|
||||
if (scope.panel.tooltip.shared || dashboard.sharedCrosshair) {
|
||||
var plot = elem.data().plot;
|
||||
if (plot) {
|
||||
$tooltip.detach();
|
||||
plot.unhighlight();
|
||||
scope.appEvent('clearCrosshair');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
elem.bind("plothover", function (event, pos, item) {
|
||||
var plot = elem.data().plot;
|
||||
var plotData = plot.getData();
|
||||
var seriesList = getSeriesFn();
|
||||
var group, value, timestamp, hoverInfo, i, series, seriesHtml;
|
||||
|
||||
if(dashboard.sharedCrosshair){
|
||||
scope.appEvent('setCrosshair', { pos: pos, scope: scope });
|
||||
}
|
||||
|
||||
if (seriesList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (scope.panel.tooltip.shared) {
|
||||
plot.unhighlight();
|
||||
|
||||
var seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);
|
||||
if (seriesHoverInfo.pointCountMismatch) {
|
||||
self.showTooltip('Shared tooltip error', '<ul>' +
|
||||
'<li>Series point counts are not the same</li>' +
|
||||
'<li>Set null point mode to null or null as zero</li>' +
|
||||
'<li>For influxdb users set fill(0) in your query</li></ul>', pos);
|
||||
return;
|
||||
}
|
||||
|
||||
seriesHtml = '';
|
||||
timestamp = dashboard.formatDate(seriesHoverInfo.time);
|
||||
|
||||
for (i = 0; i < seriesHoverInfo.length; i++) {
|
||||
hoverInfo = seriesHoverInfo[i];
|
||||
|
||||
if (hoverInfo.hidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
series = seriesList[i];
|
||||
value = series.formatValue(hoverInfo.value);
|
||||
|
||||
seriesHtml += '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
|
||||
seriesHtml += '<i class="icon-minus" style="color:' + series.color +';"></i> ' + series.label + ':</div>';
|
||||
seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
|
||||
plot.highlight(i, hoverInfo.hoverIndex);
|
||||
}
|
||||
|
||||
self.showTooltip(timestamp, seriesHtml, pos);
|
||||
}
|
||||
// single series tooltip
|
||||
else if (item) {
|
||||
series = seriesList[item.seriesIndex];
|
||||
group = '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
|
||||
group += '<i class="icon-minus" style="color:' + item.series.color +';"></i> ' + series.label + ':</div>';
|
||||
|
||||
if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
|
||||
value = item.datapoint[1] - item.datapoint[2];
|
||||
}
|
||||
else {
|
||||
value = item.datapoint[1];
|
||||
}
|
||||
|
||||
value = series.formatValue(value);
|
||||
timestamp = dashboard.formatDate(item.datapoint[0]);
|
||||
group += '<div class="graph-tooltip-value">' + value + '</div>';
|
||||
|
||||
self.showTooltip(timestamp, group, pos);
|
||||
}
|
||||
// no hit
|
||||
else {
|
||||
$tooltip.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return GraphTooltip;
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
<span ng-show="panel.legend.show"
|
||||
ng-class="{'pull-right': series.yaxis === 2, 'hidden-series': hiddenSeries[series.alias]}"
|
||||
ng-repeat='series in legend'
|
||||
class="histogram-legend">
|
||||
<i class='icon-minus pointer'
|
||||
ng-style="{color: series.color}"
|
||||
bs-popover="'colorPopup.html'"
|
||||
>
|
||||
</i>
|
||||
<span class='small histogram-legend-item'>
|
||||
<a ng-click="toggleSeries(series, $event)" data-unique="1" data-placement="{{series.yaxis === 2 ? 'bottomRight' : 'bottomLeft'}}">
|
||||
{{series.alias | urlDecode}}
|
||||
</a>
|
||||
<span ng-if="panel.legend.values">
|
||||
<span ng-show="panel.legend.current">
|
||||
Current: {{series.current}}
|
||||
</span>
|
||||
<span ng-show="panel.legend.min">
|
||||
Min: {{series.min}}
|
||||
</span>
|
||||
<span ng-show="panel.legend.max">
|
||||
Max: {{series.max}}
|
||||
</span>
|
||||
<span ng-show="panel.legend.total">
|
||||
Total: {{series.total}}
|
||||
</span>
|
||||
<span ng-show="panel.legend.avg">
|
||||
Avg: {{series.avg}}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<script type="text/ng-template" id="colorPopup.html">
|
||||
<div class="histogram-legend-popover">
|
||||
<a class="close" ng-click="dismiss();" href="">×</a>
|
||||
|
||||
<div class="editor-row small" style="padding-bottom: 0;">
|
||||
<label>Axis:</label>
|
||||
<button ng-click="toggleYAxis(series)"
|
||||
class="btn btn-mini"
|
||||
ng-class="{'btn-success': series.yaxis === 1 }">
|
||||
Left
|
||||
</button>
|
||||
<button ng-click="toggleYAxis(series)"
|
||||
class="btn btn-mini"
|
||||
ng-class="{'btn-success': series.yaxis === 2 }">
|
||||
Right
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<i ng-repeat="color in colors"
|
||||
class="pointer"
|
||||
ng-class="{'icon-circle-blank': color === series.color,'icon-circle': color !== series.color}"
|
||||
ng-style="{color:color}"
|
||||
ng-click="changeSeriesColor(series, color);dismiss();">
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
168
src/app/panels/graph/legend.js
Normal file
168
src/app/panels/graph/legend.js
Normal file
@@ -0,0 +1,168 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'lodash',
|
||||
'kbn',
|
||||
'jquery',
|
||||
'jquery.flot',
|
||||
'jquery.flot.time',
|
||||
],
|
||||
function (angular, app, _, kbn, $) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.panels.graph');
|
||||
|
||||
module.directive('graphLegend', function(popoverSrv) {
|
||||
|
||||
return {
|
||||
link: function(scope, elem) {
|
||||
var $container = $('<section class="graph-legend"></section>');
|
||||
var firstRender = true;
|
||||
var panel = scope.panel;
|
||||
var data;
|
||||
var seriesList;
|
||||
var i;
|
||||
|
||||
scope.$on('render', function() {
|
||||
data = scope.seriesList;
|
||||
if (data) {
|
||||
render();
|
||||
}
|
||||
});
|
||||
|
||||
function getSeriesIndexForElement(el) {
|
||||
return el.parents('[data-series-index]').data('series-index');
|
||||
}
|
||||
|
||||
function openColorSelector(e) {
|
||||
var el = $(e.currentTarget);
|
||||
var index = getSeriesIndexForElement(el);
|
||||
var seriesInfo = seriesList[index];
|
||||
var popoverScope = scope.$new();
|
||||
popoverScope.series = seriesInfo;
|
||||
popoverSrv.show({
|
||||
element: $(':first-child', el),
|
||||
templateUrl: 'app/panels/graph/legend.popover.html',
|
||||
scope: popoverScope
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSeries(e) {
|
||||
var el = $(e.currentTarget);
|
||||
var index = getSeriesIndexForElement(el);
|
||||
var seriesInfo = seriesList[index];
|
||||
scope.toggleSeries(seriesInfo, e);
|
||||
}
|
||||
|
||||
function sortLegend(e) {
|
||||
var el = $(e.currentTarget);
|
||||
var stat = el.data('stat');
|
||||
|
||||
if (stat !== panel.legend.sort) { panel.legend.sortDesc = null; }
|
||||
|
||||
// if already sort ascending, disable sorting
|
||||
if (panel.legend.sortDesc === false) {
|
||||
panel.legend.sort = null;
|
||||
panel.legend.sortDesc = null;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
panel.legend.sortDesc = !panel.legend.sortDesc;
|
||||
panel.legend.sort = stat;
|
||||
render();
|
||||
}
|
||||
|
||||
function getTableHeaderHtml(statName) {
|
||||
if (!panel.legend[statName]) { return ""; }
|
||||
var html = '<th class="pointer" data-stat="' + statName + '">' + statName;
|
||||
|
||||
if (panel.legend.sort === statName) {
|
||||
var cssClass = panel.legend.sortDesc ? 'icon-caret-down' : 'icon-caret-up' ;
|
||||
html += ' <span class="' + cssClass + '"></span>';
|
||||
}
|
||||
|
||||
return html + '</th>';
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (firstRender) {
|
||||
elem.append($container);
|
||||
$container.on('click', '.graph-legend-icon', openColorSelector);
|
||||
$container.on('click', '.graph-legend-alias', toggleSeries);
|
||||
$container.on('click', 'th', sortLegend);
|
||||
firstRender = false;
|
||||
}
|
||||
|
||||
seriesList = data;
|
||||
|
||||
$container.empty();
|
||||
|
||||
$container.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
|
||||
|
||||
if (panel.legend.alignAsTable) {
|
||||
var header = '<tr>';
|
||||
header += '<th colspan="2" style="text-align:left"></th>';
|
||||
if (panel.legend.values) {
|
||||
header += getTableHeaderHtml('min');
|
||||
header += getTableHeaderHtml('max');
|
||||
header += getTableHeaderHtml('avg');
|
||||
header += getTableHeaderHtml('current');
|
||||
header += getTableHeaderHtml('total');
|
||||
}
|
||||
header += '</tr>';
|
||||
$container.append($(header));
|
||||
}
|
||||
|
||||
if (panel.legend.sort) {
|
||||
seriesList = _.sortBy(seriesList, function(series) {
|
||||
return series.stats[panel.legend.sort];
|
||||
});
|
||||
if (panel.legend.sortDesc) {
|
||||
seriesList = seriesList.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < seriesList.length; i++) {
|
||||
var series = seriesList[i];
|
||||
|
||||
// ignore empty series
|
||||
if (panel.legend.hideEmpty && series.allIsNull) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var html = '<div class="graph-legend-series';
|
||||
if (series.yaxis === 2) { html += ' pull-right'; }
|
||||
if (scope.hiddenSeries[series.alias]) { html += ' graph-legend-series-hidden'; }
|
||||
html += '" data-series-index="' + i + '">';
|
||||
html += '<div class="graph-legend-icon">';
|
||||
html += '<i class="icon-minus pointer" style="color:' + series.color + '"></i>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="graph-legend-alias">';
|
||||
html += '<a>' + series.label + '</a>';
|
||||
html += '</div>';
|
||||
|
||||
var avg = series.formatValue(series.stats.avg);
|
||||
var current = series.formatValue(series.stats.current);
|
||||
var min = series.formatValue(series.stats.min);
|
||||
var max = series.formatValue(series.stats.max);
|
||||
var total = series.formatValue(series.stats.total);
|
||||
|
||||
if (panel.legend.values) {
|
||||
if (panel.legend.min) { html += '<div class="graph-legend-value min">' + min + '</div>'; }
|
||||
if (panel.legend.max) { html += '<div class="graph-legend-value max">' + max + '</div>'; }
|
||||
if (panel.legend.avg) { html += '<div class="graph-legend-value avg">' + avg + '</div>'; }
|
||||
if (panel.legend.current) { html += '<div class="graph-legend-value current">' + current + '</div>'; }
|
||||
if (panel.legend.total) { html += '<div class="graph-legend-value total">' + total + '</div>'; }
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
$container.append($(html));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
26
src/app/panels/graph/legend.popover.html
Normal file
26
src/app/panels/graph/legend.popover.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<div class="graph-legend-popover">
|
||||
<a class="close" ng-click="dismiss();" href="">×</a>
|
||||
|
||||
<div class="editor-row small" style="padding-bottom: 0;">
|
||||
<label>Axis:</label>
|
||||
<button ng-click="toggleYAxis(series);dismiss();"
|
||||
class="btn btn-mini"
|
||||
ng-class="{'btn-success': series.yaxis === 1 }">
|
||||
Left
|
||||
</button>
|
||||
<button ng-click="toggleYAxis(series);dismiss();"
|
||||
class="btn btn-mini"
|
||||
ng-class="{'btn-success': series.yaxis === 2 }">
|
||||
Right
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<i ng-repeat="color in colors"
|
||||
class="pointer"
|
||||
ng-class="{'icon-circle-blank': color === series.color,'icon-circle': color !== series.color}"
|
||||
ng-style="{color:color}"
|
||||
ng-click="changeSeriesColor(series, color);dismiss();"> </i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,33 +1,42 @@
|
||||
<div ng-controller='graph'
|
||||
ng-init="init()"
|
||||
style="min-height:{{panel.height || row.height}}"
|
||||
ng-class="{'panel-fullscreen': fullscreen}">
|
||||
<div ng-controller='GraphCtrl'>
|
||||
|
||||
<div style="position: relative">
|
||||
<div class="graph-wrapper" ng-class="{'graph-legend-rightside': panel.legend.rightSide}">
|
||||
<div class="graph-canvas-wrapper">
|
||||
|
||||
<div ng-if="datapointsWarning" class="datapoints-warning">
|
||||
<span class="small" ng-show="!datapointsCount">No datapoints <tip>Can be caused by timezone mismatch between browser and graphite server</tip></span>
|
||||
<span class="small" ng-show="datapointsOutside">Datapoints outside time range <tip>Can be caused by timezone mismatch between browser and graphite server</tip></span>
|
||||
</div>
|
||||
<div ng-if="datapointsWarning" class="datapoints-warning">
|
||||
<span class="small" ng-show="!datapointsCount">
|
||||
No datapoints <tip>No datapoints returned from metric query</tip>
|
||||
</span>
|
||||
<span class="small" ng-show="datapointsOutside">Datapoints outside time range <tip>Can be caused by timezone mismatch between browser and graphite server</tip></span>
|
||||
</div>
|
||||
|
||||
<div grafana-graph class="pointer histogram-chart">
|
||||
</div>
|
||||
<div grafana-graph class="histogram-chart">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="panel.legend" class="grafana-legend-container">
|
||||
<div ng-include="'app/panels/graph/legend.html'"></div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<div class="graph-legend-wrapper" ng-if="panel.legend.show" graph-legend></div>
|
||||
</div>
|
||||
|
||||
<div class="panel-full-edit-tabs" ng-if="editMode">
|
||||
<div ng-model="editor.index" bs-tabs>
|
||||
<div ng-repeat="tab in editorTabs" data-title="{{tab}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="tab-content" ng-repeat="tab in panelMeta.fullEditorTabs" ng-show="editorTabs[editor.index] == tab.title">
|
||||
<div ng-include src="tab.src"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 30px" ng-if="editMode">
|
||||
<div class="dashboard-editor-header">
|
||||
<div class="dashboard-editor-title">
|
||||
<i class="icon icon-bar-chart"></i>
|
||||
Graph
|
||||
</div>
|
||||
|
||||
<div ng-model="editor.index" bs-tabs>
|
||||
<div ng-repeat="tab in panelMeta.editorTabs" data-title="{{tab.title}}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-editor-body">
|
||||
<div ng-repeat="tab in panelMeta.editorTabs" ng-if="editor.index === $index">
|
||||
<div ng-include src="tab.src"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,149 +1,78 @@
|
||||
/** @scratch /panels/5
|
||||
* include::panels/histogram.asciidoc[]
|
||||
*/
|
||||
|
||||
/** @scratch /panels/histogram/0
|
||||
* == Histogram
|
||||
* Status: *Stable*
|
||||
*
|
||||
* The histogram panel allow for the display of time charts. It includes several modes and tranformations
|
||||
* to display event counts, mean, min, max and total of numeric fields, and derivatives of counter
|
||||
* fields.
|
||||
*
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'lodash',
|
||||
'kbn',
|
||||
'moment',
|
||||
'./timeSeries',
|
||||
'components/timeSeries',
|
||||
'components/panelmeta',
|
||||
'services/panelSrv',
|
||||
'services/annotationsSrv',
|
||||
'services/datasourceSrv',
|
||||
'jquery.flot',
|
||||
'jquery.flot.events',
|
||||
'jquery.flot.selection',
|
||||
'jquery.flot.time',
|
||||
'jquery.flot.byte',
|
||||
'jquery.flot.stack',
|
||||
'jquery.flot.stackpercent'
|
||||
'./seriesOverridesCtrl',
|
||||
'./graph',
|
||||
'./legend',
|
||||
],
|
||||
function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
|
||||
function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.graph', []);
|
||||
app.useModule(module);
|
||||
var module = angular.module('grafana.panels.graph');
|
||||
|
||||
module.controller('graph', function($scope, $rootScope, datasourceSrv, $timeout, annotationsSrv) {
|
||||
module.controller('GraphCtrl', function($scope, $rootScope, panelSrv, annotationsSrv, timeSrv) {
|
||||
|
||||
$scope.panelMeta = {
|
||||
modals : [],
|
||||
editorTabs: [],
|
||||
fullEditorTabs : [
|
||||
{
|
||||
title: 'General',
|
||||
src:'app/partials/panelgeneral.html'
|
||||
},
|
||||
{
|
||||
title: 'Metrics',
|
||||
src:'app/partials/metrics.html'
|
||||
},
|
||||
{
|
||||
title:'Axes & Grid',
|
||||
src:'app/panels/graph/axisEditor.html'
|
||||
},
|
||||
{
|
||||
title:'Display Styles',
|
||||
src:'app/panels/graph/styleEditor.html'
|
||||
}
|
||||
],
|
||||
fullscreenEdit: true,
|
||||
fullscreenView: true,
|
||||
description : "Graphing"
|
||||
};
|
||||
$scope.panelMeta = new PanelMeta({
|
||||
description: 'Graph panel',
|
||||
fullscreen: true,
|
||||
metricsEditor: true
|
||||
});
|
||||
|
||||
$scope.panelMeta.addEditorTab('Axes & Grid', 'app/panels/graph/axisEditor.html');
|
||||
$scope.panelMeta.addEditorTab('Display Styles', 'app/panels/graph/styleEditor.html');
|
||||
|
||||
$scope.panelMeta.addExtendedMenuItem('Export CSV', '', 'exportCsv()');
|
||||
$scope.panelMeta.addExtendedMenuItem('Toggle legend', '', 'toggleLegend()');
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
|
||||
// datasource name, null = default datasource
|
||||
datasource: null,
|
||||
|
||||
/** @scratch /panels/histogram/3
|
||||
* renderer:: sets client side (flot) or native graphite png renderer (png)
|
||||
*/
|
||||
// sets client side (flot) or native graphite png renderer (png)
|
||||
renderer: 'flot',
|
||||
/** @scratch /panels/histogram/3
|
||||
* x-axis:: Show the x-axis
|
||||
*/
|
||||
// Show/hide the x-axis
|
||||
'x-axis' : true,
|
||||
/** @scratch /panels/histogram/3
|
||||
* y-axis:: Show the y-axis
|
||||
*/
|
||||
// Show/hide y-axis
|
||||
'y-axis' : true,
|
||||
/** @scratch /panels/histogram/3
|
||||
* scale:: Scale the y-axis by this factor
|
||||
*/
|
||||
scale : 1,
|
||||
/** @scratch /panels/histogram/3
|
||||
* y_formats :: 'none','bytes','bits','short', 's', 'ms'
|
||||
*/
|
||||
// y axis formats, [left axis,right axis]
|
||||
y_formats : ['short', 'short'],
|
||||
/** @scratch /panels/histogram/5
|
||||
* grid object:: Min and max y-axis values
|
||||
* grid.min::: Minimum y-axis value
|
||||
* grid.ma1::: Maximum y-axis value
|
||||
*/
|
||||
// grid options
|
||||
grid : {
|
||||
max: null,
|
||||
min: null,
|
||||
leftMax: null,
|
||||
rightMax: null,
|
||||
leftMin: null,
|
||||
rightMin: null,
|
||||
threshold1: null,
|
||||
threshold2: null,
|
||||
threshold1Color: 'rgba(216, 200, 27, 0.27)',
|
||||
threshold2Color: 'rgba(234, 112, 112, 0.22)'
|
||||
},
|
||||
|
||||
annotate : {
|
||||
enable : false,
|
||||
},
|
||||
|
||||
/** @scratch /panels/histogram/3
|
||||
* resolution:: If auto_int is true, shoot for this many bars.
|
||||
*/
|
||||
resolution : 100,
|
||||
|
||||
/** @scratch /panels/histogram/3
|
||||
* ==== Drawing options
|
||||
* lines:: Show line chart
|
||||
*/
|
||||
// show/hide lines
|
||||
lines : true,
|
||||
/** @scratch /panels/histogram/3
|
||||
* fill:: Area fill factor for line charts, 1-10
|
||||
*/
|
||||
// fill factor
|
||||
fill : 0,
|
||||
/** @scratch /panels/histogram/3
|
||||
* linewidth:: Weight of lines in pixels
|
||||
*/
|
||||
// line width in pixels
|
||||
linewidth : 1,
|
||||
/** @scratch /panels/histogram/3
|
||||
* points:: Show points on chart
|
||||
*/
|
||||
// show hide points
|
||||
points : false,
|
||||
/** @scratch /panels/histogram/3
|
||||
* pointradius:: Size of points in pixels
|
||||
*/
|
||||
// point radius in pixels
|
||||
pointradius : 5,
|
||||
/** @scratch /panels/histogram/3
|
||||
* bars:: Show bars on chart
|
||||
*/
|
||||
// show hide bars
|
||||
bars : false,
|
||||
/** @scratch /panels/histogram/3
|
||||
* stack:: Stack multiple series
|
||||
*/
|
||||
// enable/disable stacking
|
||||
stack : false,
|
||||
/** @scratch /panels/histogram/3
|
||||
* legend:: Display the legend
|
||||
*/
|
||||
// stack percentage mode
|
||||
percentage : false,
|
||||
// legend options
|
||||
legend: {
|
||||
show: true, // disable/enable legend
|
||||
values: false, // disable/enable legend values
|
||||
@@ -153,133 +82,73 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
total: false,
|
||||
avg: false
|
||||
},
|
||||
/** @scratch /panels/histogram/3
|
||||
* ==== Transformations
|
||||
/** @scratch /panels/histogram/3
|
||||
* percentage:: Show the y-axis as a percentage of the axis total. Only makes sense for multiple
|
||||
* queries
|
||||
*/
|
||||
percentage : false,
|
||||
/** @scratch /panels/histogram/3
|
||||
* zerofill:: Improves the accuracy of line charts at a small performance cost.
|
||||
*/
|
||||
zerofill : true,
|
||||
|
||||
// how null points should be handled
|
||||
nullPointMode : 'connected',
|
||||
|
||||
// staircase line mode
|
||||
steppedLine: false,
|
||||
|
||||
// tooltip options
|
||||
tooltip : {
|
||||
value_type: 'cumulative',
|
||||
query_as_alias: true
|
||||
shared: false,
|
||||
},
|
||||
|
||||
targets: [],
|
||||
|
||||
// metric queries
|
||||
targets: [{}],
|
||||
// series color overrides
|
||||
aliasColors: {},
|
||||
aliasYAxis: {},
|
||||
// other style overrides
|
||||
seriesOverrides: [],
|
||||
};
|
||||
|
||||
_.defaults($scope.panel,_d);
|
||||
_.defaults($scope.panel.tooltip, _d.tooltip);
|
||||
_.defaults($scope.panel.annotate, _d.annotate);
|
||||
_.defaults($scope.panel.grid, _d.grid);
|
||||
_.defaults($scope.panel.legend, _d.legend);
|
||||
|
||||
// backward compatible stuff
|
||||
if (_.isBoolean($scope.panel.legend)) {
|
||||
$scope.panel.legend = { show: $scope.panel.legend };
|
||||
_.defaults($scope.panel.legend, _d.legend);
|
||||
}
|
||||
|
||||
if ($scope.panel.y_format) {
|
||||
$scope.panel.y_formats[0] = $scope.panel.y_format;
|
||||
delete $scope.panel.y_format;
|
||||
}
|
||||
if ($scope.panel.y2_format) {
|
||||
$scope.panel.y_formats[1] = $scope.panel.y2_format;
|
||||
delete $scope.panel.y2_format;
|
||||
}
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.initBaseController(this, $scope);
|
||||
|
||||
$scope.fullscreen = false;
|
||||
$scope.editor = { index: 1 };
|
||||
$scope.editorTabs = _.pluck($scope.panelMeta.fullEditorTabs,'title');
|
||||
$scope.hiddenSeries = {};
|
||||
|
||||
$scope.datasources = datasourceSrv.listOptions();
|
||||
$scope.setDatasource($scope.panel.datasource);
|
||||
|
||||
if ($scope.panel.targets.length === 0) {
|
||||
$scope.panel.targets.push({});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setDatasource = function(datasource) {
|
||||
$scope.panel.datasource = datasource;
|
||||
$scope.datasource = datasourceSrv.get(datasource);
|
||||
|
||||
if (!$scope.datasource) {
|
||||
$scope.panel.error = "Cannot find datasource " + datasource;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.removeTarget = function (target) {
|
||||
$scope.panel.targets = _.without($scope.panel.targets, target);
|
||||
$scope.get_data();
|
||||
};
|
||||
$scope.hiddenSeries = {};
|
||||
$scope.seriesList = [];
|
||||
|
||||
$scope.updateTimeRange = function () {
|
||||
$scope.range = this.filter.timeRange();
|
||||
$scope.rangeUnparsed = this.filter.timeRange(false);
|
||||
$scope.resolution = Math.ceil($(window).width() * ($scope.panel.span / 12));
|
||||
$scope.interval = '10m';
|
||||
|
||||
if ($scope.range) {
|
||||
$scope.interval = kbn.secondsToHms(
|
||||
kbn.calculate_interval($scope.range.from, $scope.range.to, $scope.resolution, 0) / 1000
|
||||
);
|
||||
$scope.range = timeSrv.timeRange();
|
||||
$scope.rangeUnparsed = timeSrv.timeRange(false);
|
||||
if ($scope.panel.maxDataPoints) {
|
||||
$scope.resolution = $scope.panel.maxDataPoints;
|
||||
}
|
||||
else {
|
||||
$scope.resolution = Math.ceil($(window).width() * ($scope.panel.span / 12));
|
||||
}
|
||||
$scope.interval = kbn.calculateInterval($scope.range, $scope.resolution, $scope.panel.interval);
|
||||
};
|
||||
|
||||
$scope.get_data = function() {
|
||||
delete $scope.panel.error;
|
||||
|
||||
$scope.panelMeta.loading = true;
|
||||
|
||||
$scope.updateTimeRange();
|
||||
|
||||
var graphiteQuery = {
|
||||
var metricsQuery = {
|
||||
range: $scope.rangeUnparsed,
|
||||
interval: $scope.interval,
|
||||
targets: $scope.panel.targets,
|
||||
format: $scope.panel.renderer === 'png' ? 'png' : 'json',
|
||||
maxDataPoints: $scope.resolution,
|
||||
datasource: $scope.panel.datasource
|
||||
cacheTimeout: $scope.panel.cacheTimeout
|
||||
};
|
||||
|
||||
$scope.annotationsPromise = annotationsSrv.getAnnotations($scope.filter, $scope.rangeUnparsed);
|
||||
$scope.annotationsPromise = annotationsSrv.getAnnotations($scope.rangeUnparsed, $scope.dashboard);
|
||||
|
||||
return $scope.datasource.query($scope.filter, graphiteQuery)
|
||||
return $scope.datasource.query(metricsQuery)
|
||||
.then($scope.dataHandler)
|
||||
.then(null, function(err) {
|
||||
$scope.panelMeta.loading = false;
|
||||
$scope.panel.error = err.message || "Timeseries data request error";
|
||||
$scope.panelMeta.error = err.message || "Timeseries data request error";
|
||||
$scope.inspector.error = err;
|
||||
$scope.seriesList = [];
|
||||
$scope.render([]);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.dataHandler = function(results) {
|
||||
$scope.panelMeta.loading = false;
|
||||
$scope.legend = [];
|
||||
|
||||
// png renderer returns just a url
|
||||
if (_.isString(results)) {
|
||||
$scope.panelMeta.loading = false;
|
||||
$scope.render(results);
|
||||
return;
|
||||
}
|
||||
@@ -288,41 +157,34 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
$scope.datapointsCount = 0;
|
||||
$scope.datapointsOutside = false;
|
||||
|
||||
var data = _.map(results.data, $scope.seriesHandler);
|
||||
$scope.seriesList = _.map(results.data, $scope.seriesHandler);
|
||||
|
||||
$scope.datapointsWarning = $scope.datapointsCount || !$scope.datapointsOutside;
|
||||
$scope.datapointsWarning = $scope.datapointsCount === 0 || $scope.datapointsOutside;
|
||||
|
||||
$scope.annotationsPromise
|
||||
.then(function(annotations) {
|
||||
data.annotations = annotations;
|
||||
$scope.render(data);
|
||||
$scope.panelMeta.loading = false;
|
||||
$scope.seriesList.annotations = annotations;
|
||||
$scope.render($scope.seriesList);
|
||||
}, function() {
|
||||
$scope.render(data);
|
||||
$scope.panelMeta.loading = false;
|
||||
$scope.render($scope.seriesList);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.seriesHandler = function(seriesData, index) {
|
||||
var datapoints = seriesData.datapoints;
|
||||
var alias = seriesData.target;
|
||||
var color = $scope.panel.aliasColors[alias] || $scope.colors[index];
|
||||
var yaxis = $scope.panel.aliasYAxis[alias] || 1;
|
||||
var color = $scope.panel.aliasColors[alias] || $rootScope.colors[index];
|
||||
|
||||
var seriesInfo = {
|
||||
alias: alias,
|
||||
color: color,
|
||||
enable: true,
|
||||
yaxis: yaxis
|
||||
};
|
||||
|
||||
$scope.legend.push(seriesInfo);
|
||||
|
||||
var series = new timeSeries.ZeroFilled({
|
||||
var series = new TimeSeries({
|
||||
datapoints: datapoints,
|
||||
info: seriesInfo,
|
||||
alias: alias,
|
||||
color: color,
|
||||
});
|
||||
|
||||
if (datapoints && datapoints.length > 0) {
|
||||
var last = moment.utc(datapoints[datapoints.length - 1][1] * 1000);
|
||||
var last = moment.utc(datapoints[datapoints.length - 1][1]);
|
||||
var from = moment.utc($scope.range.from);
|
||||
if (last - from < -10000) {
|
||||
$scope.datapointsOutside = true;
|
||||
@@ -334,16 +196,8 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
return series;
|
||||
};
|
||||
|
||||
$scope.add_target = function() {
|
||||
$scope.panel.targets.push({target: ''});
|
||||
};
|
||||
|
||||
$scope.otherPanelInFullscreenMode = function() {
|
||||
return $rootScope.fullscreen && !$scope.fullscreen;
|
||||
};
|
||||
|
||||
$scope.render = function(data) {
|
||||
$scope.$emit('render', data);
|
||||
$scope.$broadcast('render', data);
|
||||
};
|
||||
|
||||
$scope.changeSeriesColor = function(series, color) {
|
||||
@@ -353,18 +207,18 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
};
|
||||
|
||||
$scope.toggleSeries = function(serie, event) {
|
||||
if ($scope.hiddenSeries[serie.alias]) {
|
||||
delete $scope.hiddenSeries[serie.alias];
|
||||
}
|
||||
else {
|
||||
$scope.hiddenSeries[serie.alias] = true;
|
||||
}
|
||||
|
||||
if (event.ctrlKey || event.metaKey || event.shiftKey) {
|
||||
if ($scope.hiddenSeries[serie.alias]) {
|
||||
delete $scope.hiddenSeries[serie.alias];
|
||||
}
|
||||
else {
|
||||
$scope.hiddenSeries[serie.alias] = true;
|
||||
}
|
||||
} else {
|
||||
$scope.toggleSeriesExclusiveMode(serie);
|
||||
}
|
||||
|
||||
$scope.$emit('toggleLegend', $scope.legend);
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.toggleSeriesExclusiveMode = function(serie) {
|
||||
@@ -375,7 +229,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
}
|
||||
|
||||
// check if every other series is hidden
|
||||
var alreadyExclusive = _.every($scope.legend, function(value) {
|
||||
var alreadyExclusive = _.every($scope.seriesList, function(value) {
|
||||
if (value.alias === serie.alias) {
|
||||
return true;
|
||||
}
|
||||
@@ -385,13 +239,13 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
|
||||
if (alreadyExclusive) {
|
||||
// remove all hidden series
|
||||
_.each($scope.legend, function(value) {
|
||||
_.each($scope.seriesList, function(value) {
|
||||
delete $scope.hiddenSeries[value.alias];
|
||||
});
|
||||
}
|
||||
else {
|
||||
// hide all but this serie
|
||||
_.each($scope.legend, function(value) {
|
||||
_.each($scope.seriesList, function(value) {
|
||||
if (value.alias === serie.alias) {
|
||||
return;
|
||||
}
|
||||
@@ -402,8 +256,12 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
};
|
||||
|
||||
$scope.toggleYAxis = function(info) {
|
||||
info.yaxis = info.yaxis === 2 ? 1 : 2;
|
||||
$scope.panel.aliasYAxis[info.alias] = info.yaxis;
|
||||
var override = _.findWhere($scope.panel.seriesOverrides, { alias: info.alias });
|
||||
if (!override) {
|
||||
override = { alias: info.alias };
|
||||
$scope.panel.seriesOverrides.push(override);
|
||||
}
|
||||
override.yaxis = info.yaxis === 2 ? 1 : 2;
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
@@ -412,6 +270,26 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.addSeriesOverride = function(override) {
|
||||
$scope.panel.seriesOverrides.push(override || {});
|
||||
};
|
||||
|
||||
$scope.removeSeriesOverride = function(override) {
|
||||
$scope.panel.seriesOverrides = _.without($scope.panel.seriesOverrides, override);
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
// Called from panel menu
|
||||
$scope.toggleLegend = function() {
|
||||
$scope.panel.legend.show = !$scope.panel.legend.show;
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.exportCsv = function() {
|
||||
kbn.exportSeriesListToCsv($scope.seriesList);
|
||||
};
|
||||
|
||||
panelSrv.init($scope);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
89
src/app/panels/graph/seriesOverridesCtrl.js
Normal file
89
src/app/panels/graph/seriesOverridesCtrl.js
Normal file
@@ -0,0 +1,89 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'lodash',
|
||||
], function(angular, app, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.panels.graph', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('SeriesOverridesCtrl', function($scope) {
|
||||
$scope.overrideMenu = [];
|
||||
$scope.currentOverrides = [];
|
||||
$scope.override = $scope.override || {};
|
||||
|
||||
$scope.addOverrideOption = function(name, propertyName, values) {
|
||||
var option = {};
|
||||
option.text = name;
|
||||
option.propertyName = propertyName;
|
||||
option.index = $scope.overrideMenu.length;
|
||||
option.values = values;
|
||||
|
||||
option.submenu = _.map(values, function(value, index) {
|
||||
return {
|
||||
text: String(value),
|
||||
click: 'menuItemSelected(' + option.index + ',' + index + ')'
|
||||
};
|
||||
});
|
||||
|
||||
$scope.overrideMenu.push(option);
|
||||
};
|
||||
|
||||
$scope.setOverride = function(optionIndex, valueIndex) {
|
||||
var option = $scope.overrideMenu[optionIndex];
|
||||
var value = option.values[valueIndex];
|
||||
$scope.override[option.propertyName] = value;
|
||||
|
||||
// automatically disable lines for this series and the fill bellow to series
|
||||
// can be removed by the user if they still want lines
|
||||
if (option.propertyName === 'fillBelowTo') {
|
||||
$scope.override['lines'] = false;
|
||||
$scope.addSeriesOverride({ alias: value, lines: false });
|
||||
}
|
||||
|
||||
$scope.updateCurrentOverrides();
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.removeOverride = function(option) {
|
||||
delete $scope.override[option.propertyName];
|
||||
$scope.updateCurrentOverrides();
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.getSeriesNames = function() {
|
||||
return _.map($scope.seriesList, function(series) {
|
||||
return series.alias;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updateCurrentOverrides = function() {
|
||||
$scope.currentOverrides = [];
|
||||
_.each($scope.overrideMenu, function(option) {
|
||||
var value = $scope.override[option.propertyName];
|
||||
if (_.isUndefined(value)) { return; }
|
||||
$scope.currentOverrides.push({
|
||||
name: option.text,
|
||||
propertyName: option.propertyName,
|
||||
value: String(value)
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addOverrideOption('Bars', 'bars', [true, false]);
|
||||
$scope.addOverrideOption('Lines', 'lines', [true, false]);
|
||||
$scope.addOverrideOption('Line fill', 'fill', [0,1,2,3,4,5,6,7,8,9,10]);
|
||||
$scope.addOverrideOption('Line width', 'linewidth', [0,1,2,3,4,5,6,7,8,9,10]);
|
||||
$scope.addOverrideOption('Fill below to', 'fillBelowTo', $scope.getSeriesNames());
|
||||
$scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]);
|
||||
$scope.addOverrideOption('Points', 'points', [true, false]);
|
||||
$scope.addOverrideOption('Points Radius', 'pointradius', [1,2,3,4,5]);
|
||||
$scope.addOverrideOption('Stack', 'stack', [true, false, 2, 3, 4, 5]);
|
||||
$scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]);
|
||||
$scope.addOverrideOption('Z-index', 'zindex', [-1,-2,-3,0,1,2,3]);
|
||||
$scope.updateCurrentOverrides();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,17 +1,9 @@
|
||||
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="section">
|
||||
<h5>Chart Options</h5>
|
||||
<div class="editor-option">
|
||||
<label class="small">Bars</label><input type="checkbox" ng-model="panel.bars" ng-checked="panel.bars" ng-change="render()">
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Lines</label><input type="checkbox" ng-model="panel.lines" ng-checked="panel.lines" ng-change="render()">
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Points</label><input type="checkbox" ng-model="panel.points" ng-checked="panel.points" ng-change="render()">
|
||||
</div>
|
||||
<editor-opt-bool text="Bars" model="panel.bars" change="render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Lines" model="panel.lines" change="render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Points" model="panel.points" change="render()"></editor-opt-bool>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
@@ -29,27 +21,19 @@
|
||||
<select class="input-mini" ng-model="panel.pointradius" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10]" ng-change="render()"></select>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Null point mode <tip>Define how null values should be drawn</tip></label>
|
||||
<label class="small">Null point mode<tip>Define how null values should be drawn</tip></label>
|
||||
<select class="input-medium" ng-model="panel.nullPointMode" ng-options="f for f in ['connected', 'null', 'null as zero']" ng-change="render()"></select>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Staircase line</label><input type="checkbox" ng-model="panel.steppedLine" ng-checked="panel.steppedLine" ng-change="render()">
|
||||
</div>
|
||||
|
||||
<editor-opt-bool text="Staircase line" model="panel.steppedLine" change="render()"></editor-opt-bool>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h5>Multiple Series</h5>
|
||||
<div class="editor-option">
|
||||
<label class="small">Stack</label><input type="checkbox" ng-model="panel.stack" ng-checked="panel.stack" ng-change="render()">
|
||||
</div>
|
||||
<div class="editor-option" ng-show="panel.stack">
|
||||
<label style="white-space:nowrap" class="small">Percent <tip>Stack as a percentage of total</tip></label>
|
||||
<input type="checkbox" ng-model="panel.percentage" ng-checked="panel.percentage" ng-change="render()">
|
||||
</div>
|
||||
<div class="editor-option" ng-show="panel.stack">
|
||||
<label class="small">Stacked Values <tip>How should the values in stacked charts to be calculated?</tip></label>
|
||||
<select class="input-small" ng-model="panel.tooltip.value_type" ng-options="f for f in ['cumulative','individual']" ng-change="render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<editor-opt-bool text="Stack" model="panel.stack" change="render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Percent" model="panel.percentage" change="render()" tip="Stack as a percentage of total"></editor-opt-bool>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="section">
|
||||
@@ -63,4 +47,57 @@
|
||||
<input type="radio" class="input-small" ng-model="panel.renderer" value="png" ng-change="get_data()" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h5>Tooltip</h5>
|
||||
<editor-opt-bool
|
||||
text="All series" model="panel.tooltip.shared" change="render()"
|
||||
tip="Show all series on same tooltip and a x croshair to help follow all series">
|
||||
</editor-opt-bool>
|
||||
<div class="editor-option" ng-show="panel.stack">
|
||||
<label class="small">Stacked Values <tip>How should the values in stacked charts to be calculated?</tip></label>
|
||||
<select class="input-small" ng-model="panel.tooltip.value_type" ng-options="f for f in ['cumulative','individual']" ng-change="render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="section">
|
||||
<h5>Series specific overrides <tip>Regex match example: /server[0-3]/i </tip></h5>
|
||||
<div>
|
||||
<div class="grafana-target" ng-repeat="override in panel.seriesOverrides" ng-controller="SeriesOverridesCtrl">
|
||||
<div class="grafana-target-inner">
|
||||
<ul class="grafana-segment-list">
|
||||
<li class="grafana-target-segment">
|
||||
<i class="icon-remove pointer" ng-click="removeSeriesOverride(override)"></i>
|
||||
</li>
|
||||
|
||||
<li class="grafana-target-segment">
|
||||
alias or regex
|
||||
</li>
|
||||
<li>
|
||||
<input type="text"
|
||||
ng-model="override.alias"
|
||||
bs-typeahead="getSeriesNames"
|
||||
ng-blur="render()"
|
||||
data-min-length=0 data-items=100
|
||||
class="input-medium grafana-target-segment-input" >
|
||||
</li>
|
||||
<li class="grafana-target-segment" ng-repeat="option in currentOverrides">
|
||||
<i class="pointer icon-remove" ng-click="removeOverride(option)"></i>
|
||||
{{option.name}}: {{option.value}}
|
||||
</li>
|
||||
|
||||
<li class="dropdown" dropdown-typeahead="overrideMenu" dropdown-typeahead-on-select="setOverride($optionIndex, $valueIndex)">
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" style="margin-top: 20px" ng-click="addSeriesOverride()">Add series override rule</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
define([
|
||||
'underscore',
|
||||
'kbn'
|
||||
],
|
||||
function (_, kbn) {
|
||||
'use strict';
|
||||
|
||||
var ts = {};
|
||||
|
||||
ts.ZeroFilled = function (opts) {
|
||||
this.datapoints = opts.datapoints;
|
||||
this.info = opts.info;
|
||||
this.label = opts.info.alias;
|
||||
};
|
||||
|
||||
ts.ZeroFilled.prototype.getFlotPairs = function (fillStyle, yFormats) {
|
||||
var result = [];
|
||||
|
||||
this.color = this.info.color;
|
||||
this.yaxis = this.info.yaxis;
|
||||
|
||||
this.info.total = 0;
|
||||
this.info.max = null;
|
||||
this.info.min = 212312321312;
|
||||
|
||||
_.each(this.datapoints, function(valueArray) {
|
||||
var currentTime = valueArray[1];
|
||||
var currentValue = valueArray[0];
|
||||
if (currentValue === null) {
|
||||
if (fillStyle === 'connected') {
|
||||
return;
|
||||
}
|
||||
if (fillStyle === 'null as zero') {
|
||||
currentValue = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (_.isNumber(currentValue)) {
|
||||
this.info.total += currentValue;
|
||||
}
|
||||
|
||||
if (currentValue > this.info.max) {
|
||||
this.info.max = currentValue;
|
||||
}
|
||||
|
||||
if (currentValue < this.info.min) {
|
||||
this.info.min = currentValue;
|
||||
}
|
||||
|
||||
result.push([currentTime * 1000, currentValue]);
|
||||
}, this);
|
||||
|
||||
if (result.length > 2) {
|
||||
this.info.timeStep = result[1][0] - result[0][0];
|
||||
}
|
||||
|
||||
if (result.length) {
|
||||
|
||||
this.info.avg = (this.info.total / result.length);
|
||||
this.info.current = result[result.length-1][1];
|
||||
|
||||
var formater = kbn.getFormatFunction(yFormats[this.yaxis - 1], 2);
|
||||
this.info.avg = this.info.avg != null ? formater(this.info.avg) : null;
|
||||
this.info.current = this.info.current != null ? formater(this.info.current) : null;
|
||||
this.info.min = this.info.min != null ? formater(this.info.min) : null;
|
||||
this.info.max = this.info.max != null ? formater(this.info.max) : null;
|
||||
this.info.total = this.info.total != null ? formater(this.info.total) : null;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return ts;
|
||||
});
|
||||
115
src/app/panels/singlestat/editor.html
Normal file
115
src/app/panels/singlestat/editor.html
Normal file
@@ -0,0 +1,115 @@
|
||||
<div class="editor-row">
|
||||
<div class="section">
|
||||
<h5>Big value</h5>
|
||||
<div class="editor-option">
|
||||
<label class="small">Prefix</label>
|
||||
<input type="text" class="input-small" ng-model="panel.prefix" ng-blur="render()"></input>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Value</label>
|
||||
<select class="input-small" ng-model="panel.valueName" ng-options="f for f in ['min','max','avg', 'current', 'total']" ng-change="render()"></select>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Postfix</label>
|
||||
<input type="text" class="input-small" ng-model="panel.postfix" ng-blur="render()" ng-trim="false"></input>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Null point mode<tip>Define how null values should handled, connected = ignored</tip></label>
|
||||
<select class="input-medium" ng-model="panel.nullPointMode" ng-options="f for f in ['connected', 'null', 'null as zero']" ng-change="get_data()"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h5>Big value font size</h5>
|
||||
<div class="editor-option">
|
||||
<label class="small">Prefix</label>
|
||||
<select class="input-mini" style="width: 75px;" ng-model="panel.prefixFontSize" ng-options="f for f in fontSizes" ng-change="render()"></select>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Value</label>
|
||||
<select class="input-mini" style="width: 75px;" ng-model="panel.valueFontSize" ng-options="f for f in fontSizes" ng-change="render()"></select>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Postfix</label>
|
||||
<select class="input-mini" style="width: 75px;" ng-model="panel.postfixFontSize" ng-options="f for f in fontSizes" ng-change="render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h5>Formats</h5>
|
||||
<div class="editor-option">
|
||||
<label class="small">Unit format</label>
|
||||
<select class="input-small" ng-model="panel.format" ng-options="f for f in ['none','short','bytes', 'bits', 'bps', 's', 'ms', 'µs', 'ns', 'percent']" ng-change="render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="section">
|
||||
<h5>Coloring</h5>
|
||||
<editor-opt-bool text="Background" model="panel.colorBackground" change="setColoring({background: true})"></editor-opt-bool>
|
||||
<editor-opt-bool text="Value" model="panel.colorValue" change="setColoring({value: true})"></editor-opt-bool>
|
||||
<div class="editor-option" ng-show="panel.colorBackground || panel.colorValue">
|
||||
<label class="small">Thresholds<tip>Comma seperated values</tip></label>
|
||||
<input type="text" class="input-large" ng-model="panel.thresholds" ng-blur="render()" placeholder="0,50,80"></input>
|
||||
</div>
|
||||
<div class="editor-option" ng-show="panel.colorBackground || panel.colorValue">
|
||||
<label class="small">Colors</label>
|
||||
<spectrum-picker ng-model="panel.colors[0]" ng-change="render()" ></spectrum-picker>
|
||||
<spectrum-picker ng-model="panel.colors[1]" ng-change="render()" ></spectrum-picker>
|
||||
<spectrum-picker ng-model="panel.colors[2]" ng-change="render()" ></spectrum-picker>
|
||||
<a class="pointer" ng-click="invertColorOrder()">invert order</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h5>Spark lines</h5>
|
||||
<editor-opt-bool text="Spark line" model="panel.sparkline.show" change="render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Background mode" model="panel.sparkline.full" change="render()"></editor-opt-bool>
|
||||
<div class="editor-option">
|
||||
<label class="small">Line color</label>
|
||||
<spectrum-picker ng-model="panel.sparkline.lineColor" ng-change="render()" ></spectrum-picker>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Fill color</label>
|
||||
<spectrum-picker ng-model="panel.sparkline.fillColor" ng-change="render()" ></spectrum-picker>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="section">
|
||||
<h5>Value to text mapping</h5>
|
||||
<div class="editor-option">
|
||||
<label class="small">Specify mappings</label>
|
||||
<div class="grafana-target">
|
||||
<div class="grafana-target-inner">
|
||||
<ul class="grafana-segment-list">
|
||||
<li class="grafana-target-segment" ng-repeat-start="map in panel.valueMaps">
|
||||
<i class="icon-remove pointer" ng-click="removeValueMap(map)"></i>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<input type="text" ng-model="map.value" placeholder="value" class="input-mini grafana-target-segment-input" ng-blur="render()">
|
||||
</li>
|
||||
<li class="grafana-target-segment">
|
||||
<i class="icon-arrow-right"></i>
|
||||
</li>
|
||||
<li ng-repeat-end>
|
||||
<input type="text" placeholder="text" ng-model="map.text" class="input-mini grafana-target-segment-input" ng-blur="render()">
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="pointer grafana-target-segment" ng-click="addValueMap();">
|
||||
<i class="icon-plus"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
26
src/app/panels/singlestat/module.html
Normal file
26
src/app/panels/singlestat/module.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<div ng-controller='SingleStatCtrl'>
|
||||
|
||||
<div class="singlestat-panel" singlestat-panel></div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div style="margin-top: 30px" ng-if="editMode">
|
||||
<div class="dashboard-editor-header">
|
||||
<div class="dashboard-editor-title">
|
||||
<i class="icon icon-dashboard"></i>
|
||||
Singlestat
|
||||
</div>
|
||||
|
||||
<div ng-model="editor.index" bs-tabs>
|
||||
<div ng-repeat="tab in panelMeta.editorTabs" data-title="{{tab.title}}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-editor-body">
|
||||
<div ng-repeat="tab in panelMeta.editorTabs" ng-if="editor.index === $index">
|
||||
<div ng-include src="tab.src"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
233
src/app/panels/singlestat/module.js
Normal file
233
src/app/panels/singlestat/module.js
Normal file
@@ -0,0 +1,233 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'lodash',
|
||||
'components/timeSeries',
|
||||
'kbn',
|
||||
'components/panelmeta',
|
||||
'services/panelSrv',
|
||||
'./singleStatPanel',
|
||||
],
|
||||
function (angular, app, _, TimeSeries, kbn, PanelMeta) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.panels.singlestat');
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('SingleStatCtrl', function($scope, panelSrv, timeSrv) {
|
||||
|
||||
$scope.panelMeta = new PanelMeta({
|
||||
description: 'Singlestat panel',
|
||||
fullscreen: true,
|
||||
metricsEditor: true
|
||||
});
|
||||
|
||||
$scope.fontSizes = ['20%', '30%','50%','70%','80%','100%', '110%', '120%', '150%', '170%', '200%'];
|
||||
|
||||
$scope.panelMeta.addEditorTab('Options', 'app/panels/singlestat/editor.html');
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
links: [],
|
||||
maxDataPoints: 100,
|
||||
interval: null,
|
||||
targets: [{}],
|
||||
cacheTimeout: null,
|
||||
format: 'none',
|
||||
prefix: '',
|
||||
postfix: '',
|
||||
nullText: null,
|
||||
valueMaps: [
|
||||
{ value: 'null', op: '=', text: 'N/A' }
|
||||
],
|
||||
nullPointMode: 'connected',
|
||||
valueName: 'avg',
|
||||
prefixFontSize: '50%',
|
||||
valueFontSize: '80%',
|
||||
postfixFontSize: '50%',
|
||||
thresholds: '',
|
||||
colorBackground: false,
|
||||
colorValue: false,
|
||||
colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
|
||||
sparkline: {
|
||||
show: false,
|
||||
full: false,
|
||||
lineColor: 'rgb(31, 120, 193)',
|
||||
fillColor: 'rgba(31, 118, 189, 0.18)',
|
||||
}
|
||||
};
|
||||
|
||||
_.defaults($scope.panel, _d);
|
||||
|
||||
$scope.init = function() {
|
||||
panelSrv.init($scope);
|
||||
$scope.$on('refresh', $scope.get_data);
|
||||
};
|
||||
|
||||
$scope.updateTimeRange = function () {
|
||||
$scope.range = timeSrv.timeRange();
|
||||
$scope.rangeUnparsed = timeSrv.timeRange(false);
|
||||
$scope.resolution = $scope.panel.maxDataPoints;
|
||||
$scope.interval = kbn.calculateInterval($scope.range, $scope.resolution, $scope.panel.interval);
|
||||
};
|
||||
|
||||
$scope.get_data = function() {
|
||||
$scope.updateTimeRange();
|
||||
|
||||
var metricsQuery = {
|
||||
range: $scope.rangeUnparsed,
|
||||
interval: $scope.interval,
|
||||
targets: $scope.panel.targets,
|
||||
maxDataPoints: $scope.resolution,
|
||||
cacheTimeout: $scope.panel.cacheTimeout
|
||||
};
|
||||
|
||||
return $scope.datasource.query(metricsQuery)
|
||||
.then($scope.dataHandler)
|
||||
.then(null, function(err) {
|
||||
console.log("err");
|
||||
$scope.panelMeta.loading = false;
|
||||
$scope.panelMeta.error = err.message || "Timeseries data request error";
|
||||
$scope.inspector.error = err;
|
||||
$scope.render();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.dataHandler = function(results) {
|
||||
$scope.panelMeta.loading = false;
|
||||
$scope.series = _.map(results.data, $scope.seriesHandler);
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.seriesHandler = function(seriesData) {
|
||||
var series = new TimeSeries({
|
||||
datapoints: seriesData.datapoints,
|
||||
alias: seriesData.target,
|
||||
});
|
||||
|
||||
series.flotpairs = series.getFlotPairs($scope.panel.nullPointMode);
|
||||
|
||||
return series;
|
||||
};
|
||||
|
||||
$scope.setColoring = function(options) {
|
||||
if (options.background) {
|
||||
$scope.panel.colorValue = false;
|
||||
$scope.panel.colors = ['rgba(71, 212, 59, 0.4)', 'rgba(245, 150, 40, 0.73)', 'rgba(225, 40, 40, 0.59)'];
|
||||
}
|
||||
else {
|
||||
$scope.panel.colorBackground = false;
|
||||
$scope.panel.colors = ['rgba(50, 172, 45, 0.97)', 'rgba(237, 129, 40, 0.89)', 'rgba(245, 54, 54, 0.9)'];
|
||||
}
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.invertColorOrder = function() {
|
||||
var tmp = $scope.panel.colors[0];
|
||||
$scope.panel.colors[0] = $scope.panel.colors[2];
|
||||
$scope.panel.colors[2] = tmp;
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.getDecimalsForValue = function(value) {
|
||||
|
||||
var delta = value / 2;
|
||||
var dec = -Math.floor(Math.log(delta) / Math.LN10);
|
||||
|
||||
var magn = Math.pow(10, -dec),
|
||||
norm = delta / magn, // norm is between 1.0 and 10.0
|
||||
size;
|
||||
|
||||
if (norm < 1.5) {
|
||||
size = 1;
|
||||
} else if (norm < 3) {
|
||||
size = 2;
|
||||
// special case for 2.5, requires an extra decimal
|
||||
if (norm > 2.25) {
|
||||
size = 2.5;
|
||||
++dec;
|
||||
}
|
||||
} else if (norm < 7.5) {
|
||||
size = 5;
|
||||
} else {
|
||||
size = 10;
|
||||
}
|
||||
|
||||
size *= magn;
|
||||
|
||||
// reduce starting decimals if not needed
|
||||
if (Math.floor(value) === value) { dec = 0; }
|
||||
|
||||
var result = {};
|
||||
result.decimals = Math.max(0, dec);
|
||||
result.scaledDecimals = result.decimals - Math.floor(Math.log(size) / Math.LN10) + 2;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
$scope.render = function() {
|
||||
var data = {};
|
||||
|
||||
if (!$scope.series || $scope.series.length === 0) {
|
||||
data.flotpairs = [];
|
||||
data.mainValue = Number.NaN;
|
||||
data.mainValueFormated = $scope.getFormatedValue(null);
|
||||
}
|
||||
else {
|
||||
var series = $scope.series[0];
|
||||
data.mainValue = series.stats[$scope.panel.valueName];
|
||||
data.mainValueFormated = $scope.getFormatedValue(data.mainValue);
|
||||
data.flotpairs = series.flotpairs;
|
||||
}
|
||||
|
||||
data.thresholds = $scope.panel.thresholds.split(',').map(function(strVale) {
|
||||
return Number(strVale.trim());
|
||||
});
|
||||
|
||||
data.colorMap = $scope.panel.colors;
|
||||
|
||||
$scope.data = data;
|
||||
$scope.$emit('render');
|
||||
};
|
||||
|
||||
$scope.getFormatedValue = function(mainValue) {
|
||||
|
||||
// first check value to text mappings
|
||||
for(var i = 0; i < $scope.panel.valueMaps.length; i++) {
|
||||
var map = $scope.panel.valueMaps[i];
|
||||
// special null case
|
||||
if (map.value === 'null') {
|
||||
if (mainValue === null || mainValue === void 0) {
|
||||
return map.text;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// value/number to text mapping
|
||||
var value = parseFloat(map.value);
|
||||
if (value === mainValue) {
|
||||
return map.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (mainValue === null || mainValue === void 0) {
|
||||
return "no value";
|
||||
}
|
||||
|
||||
var decimalInfo = $scope.getDecimalsForValue(mainValue);
|
||||
var formatFunc = kbn.valueFormats[$scope.panel.format];
|
||||
return formatFunc(mainValue, decimalInfo.decimals, decimalInfo.scaledDecimals);
|
||||
};
|
||||
|
||||
$scope.removeValueMap = function(map) {
|
||||
var index = _.indexOf($scope.panel.valueMaps, map);
|
||||
$scope.panel.valueMaps.splice(index, 1);
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.addValueMap = function() {
|
||||
$scope.panel.valueMaps.push({value: '', op: '=', text: '' });
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
});
|
||||
});
|
||||
211
src/app/panels/singlestat/singleStatPanel.js
Normal file
211
src/app/panels/singlestat/singleStatPanel.js
Normal file
@@ -0,0 +1,211 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'lodash',
|
||||
'jquery',
|
||||
'jquery.flot',
|
||||
],
|
||||
function (angular, app, _, $) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.panels.singlestat', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.directive('singlestatPanel', function($location, linkSrv, $timeout) {
|
||||
|
||||
return {
|
||||
link: function(scope, elem) {
|
||||
var data, panel;
|
||||
var $panelContainer = elem.parents('.panel-container');
|
||||
|
||||
scope.$on('render', function() {
|
||||
render();
|
||||
});
|
||||
|
||||
function setElementHeight() {
|
||||
try {
|
||||
var height = scope.height || panel.height || scope.row.height;
|
||||
if (_.isString(height)) {
|
||||
height = parseInt(height.replace('px', ''), 10);
|
||||
}
|
||||
|
||||
height -= 5; // padding
|
||||
height -= panel.title ? 24 : 9; // subtract panel title bar
|
||||
|
||||
elem.css('height', height + 'px');
|
||||
|
||||
return true;
|
||||
} catch(e) { // IE throws errors sometimes
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function applyColoringThresholds(value, valueString) {
|
||||
if (!panel.colorValue) {
|
||||
return valueString;
|
||||
}
|
||||
|
||||
var color = getColorForValue(value);
|
||||
if (color) {
|
||||
return '<span style="color:' + color + '">'+ valueString + '</span>';
|
||||
}
|
||||
|
||||
return valueString;
|
||||
}
|
||||
|
||||
function getColorForValue(value) {
|
||||
for (var i = data.thresholds.length - 1; i >= 0 ; i--) {
|
||||
if (value >= data.thresholds[i]) {
|
||||
return data.colorMap[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getSpan(className, fontSize, value) {
|
||||
return '<span class="' + className + '" style="font-size:' + fontSize + '">' +
|
||||
value + '</span>';
|
||||
}
|
||||
|
||||
function getBigValueHtml() {
|
||||
var body = '<div class="singlestat-panel-value-container">';
|
||||
|
||||
if (panel.prefix) { body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, scope.panel.prefix); }
|
||||
|
||||
var value = applyColoringThresholds(data.mainValue, data.mainValueFormated);
|
||||
body += getSpan('singlestat-panel-value', panel.valueFontSize, value);
|
||||
|
||||
if (panel.postfix) { body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, panel.postfix); }
|
||||
|
||||
body += '</div>';
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
function addSparkline() {
|
||||
var panel = scope.panel;
|
||||
var width = elem.width() + 20;
|
||||
var height = elem.height() || 100;
|
||||
|
||||
var plotCanvas = $('<div></div>');
|
||||
var plotCss = {};
|
||||
plotCss.position = 'absolute';
|
||||
|
||||
if (panel.sparkline.full) {
|
||||
plotCss.bottom = '5px';
|
||||
plotCss.left = '-5px';
|
||||
plotCss.width = (width - 10) + 'px';
|
||||
plotCss.height = (height - 45) + 'px';
|
||||
}
|
||||
else {
|
||||
plotCss.bottom = "0px";
|
||||
plotCss.left = "-5px";
|
||||
plotCss.width = (width - 10) + 'px';
|
||||
plotCss.height = Math.floor(height * 0.25) + "px";
|
||||
}
|
||||
|
||||
plotCanvas.css(plotCss);
|
||||
|
||||
var options = {
|
||||
legend: { show: false },
|
||||
series: {
|
||||
lines: {
|
||||
show: true,
|
||||
fill: 1,
|
||||
lineWidth: 1,
|
||||
fillColor: panel.sparkline.fillColor,
|
||||
},
|
||||
},
|
||||
yaxes: { show: false },
|
||||
xaxis: {
|
||||
show: false,
|
||||
mode: "time",
|
||||
min: scope.range.from.getTime(),
|
||||
max: scope.range.to.getTime(),
|
||||
},
|
||||
grid: { hoverable: false, show: false },
|
||||
};
|
||||
|
||||
elem.append(plotCanvas);
|
||||
|
||||
var plotSeries = {
|
||||
data: data.flotpairs,
|
||||
color: panel.sparkline.lineColor
|
||||
};
|
||||
|
||||
setTimeout(function() {
|
||||
$.plot(plotCanvas, [plotSeries], options);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!scope.data) { return; }
|
||||
|
||||
data = scope.data;
|
||||
panel = scope.panel;
|
||||
|
||||
setElementHeight();
|
||||
|
||||
var body = getBigValueHtml();
|
||||
|
||||
if (panel.colorBackground && !isNaN(data.mainValue)) {
|
||||
var color = getColorForValue(data.mainValue);
|
||||
if (color) {
|
||||
$panelContainer.css('background-color', color);
|
||||
if (scope.fullscreen) {
|
||||
elem.css('background-color', color);
|
||||
} else {
|
||||
elem.css('background-color', '');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$panelContainer.css('background-color', '');
|
||||
elem.css('background-color', '');
|
||||
}
|
||||
|
||||
elem.html(body);
|
||||
|
||||
if (panel.sparkline.show) {
|
||||
addSparkline();
|
||||
}
|
||||
|
||||
elem.toggleClass('pointer', panel.links.length > 0);
|
||||
}
|
||||
|
||||
// drilldown link tooltip
|
||||
var drilldownTooltip = $('<div id="tooltip" class="">gello</div>"');
|
||||
|
||||
elem.mouseleave(function() {
|
||||
if (panel.links.length === 0) { return;}
|
||||
drilldownTooltip.detach();
|
||||
});
|
||||
|
||||
elem.click(function() {
|
||||
if (panel.links.length === 0) { return; }
|
||||
|
||||
var linkInfo = linkSrv.getPanelLinkAnchorInfo(panel.links[0]);
|
||||
if (linkInfo.href[0] === '#') { linkInfo.href = linkInfo.href.substring(1); }
|
||||
|
||||
if (linkInfo.href.indexOf('http') === 0) {
|
||||
window.location.href = linkInfo.href;
|
||||
} else {
|
||||
$timeout(function() {
|
||||
$location.url(linkInfo.href);
|
||||
});
|
||||
}
|
||||
|
||||
drilldownTooltip.detach();
|
||||
});
|
||||
|
||||
elem.mousemove(function(e) {
|
||||
if (panel.links.length === 0) { return;}
|
||||
|
||||
drilldownTooltip.text('click to go to: ' + panel.links[0].title);
|
||||
|
||||
drilldownTooltip.place_tt(e.pageX+20, e.pageY-15);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
@@ -9,10 +9,9 @@
|
||||
</div>
|
||||
|
||||
<label class=small>Content
|
||||
<span ng-show="panel.mode == 'html'">(This area uses HTML sanitized via AngularJS's <a href='http://docs.angularjs.org/api/ngSanitize.$sanitize'>$sanitize</a> service)</span>
|
||||
<span ng-show="panel.mode == 'markdown'">(This area uses <a target="_blank" href="http://en.wikipedia.org/wiki/Markdown">Markdown</a>. HTML is not supported)</span>
|
||||
</label>
|
||||
|
||||
<textarea ng-model="panel.content" rows="6" style="width:95%" ng-change="render()" ng-model-onblur>
|
||||
<textarea ng-model="panel.content" rows="20" style="width:95%" ng-change="render()" ng-model-onblur>
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
<div ng-controller='text' ng-init="init()" style="min-height:{{panel.height || row.height}}" ng-dblclick="openEditor()">
|
||||
<!--<p ng-style="panel.style" ng-bind-html-unsafe="panel.content | striphtml | newlines"></p>-->
|
||||
<markdown ng-show="ready && panel.mode == 'markdown'">
|
||||
{{panel.content}}
|
||||
</markdown>
|
||||
<p ng-show="panel.mode == 'text'" ng-style='panel.style' ng-bind-html-unsafe="panel.content | striphtml | newlines">
|
||||
</p>
|
||||
<p ng-show="panel.mode == 'html'" ng-bind-html-unsafe="panel.content">
|
||||
<div ng-controller='text'>
|
||||
<p ng-bind-html="content" ng-show="content">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,101 +1,98 @@
|
||||
/** @scratch /panels/5
|
||||
* include::panels/text.asciidoc[]
|
||||
*/
|
||||
|
||||
/** @scratch /panels/text/0
|
||||
* == text
|
||||
* Status: *Stable*
|
||||
*
|
||||
* The text panel is used for displaying static text formated as markdown, sanitized html or as plain
|
||||
* text.
|
||||
*
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore',
|
||||
'require'
|
||||
'lodash',
|
||||
'require',
|
||||
'components/panelmeta',
|
||||
],
|
||||
function (angular, app, _, require) {
|
||||
function (angular, app, _, require, PanelMeta) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.text', []);
|
||||
var module = angular.module('grafana.panels.text', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('text', function($scope) {
|
||||
var converter;
|
||||
|
||||
$scope.panelMeta = {
|
||||
module.controller('text', function($scope, templateSrv, $sce, panelSrv) {
|
||||
|
||||
$scope.panelMeta = new PanelMeta({
|
||||
description : "A static text panel that can use plain text, markdown, or (sanitized) HTML"
|
||||
};
|
||||
});
|
||||
|
||||
$scope.panelMeta.addEditorTab('Edit text', 'app/panels/text/editor.html');
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
title : 'default title',
|
||||
mode : "markdown", // 'html', 'markdown', 'text'
|
||||
content : "",
|
||||
style: {},
|
||||
};
|
||||
|
||||
_.defaults($scope.panel,_d);
|
||||
_.defaults($scope.panel, _d);
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.initBaseController(this, $scope);
|
||||
|
||||
panelSrv.init($scope);
|
||||
$scope.ready = false;
|
||||
$scope.$on('refresh', $scope.render);
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.render = function() {
|
||||
$scope.$emit('render');
|
||||
};
|
||||
|
||||
$scope.openEditor = function() {
|
||||
//$scope.$emit('open-modal','paneleditor');
|
||||
console.log('scope id', $scope.$id);
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
module.directive('markdown', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function(scope, element) {
|
||||
scope.$on('render', function() {
|
||||
render_panel();
|
||||
});
|
||||
|
||||
function render_panel() {
|
||||
require(['./lib/showdown'], function (Showdown) {
|
||||
scope.ready = true;
|
||||
var converter = new Showdown.converter();
|
||||
var text = scope.panel.content.replace(/&/g, '&')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<');
|
||||
var htmlText = converter.makeHtml(text);
|
||||
element.html(htmlText);
|
||||
// For whatever reason, this fixes chrome. I don't like it, I think
|
||||
// it makes things slow?
|
||||
if(!scope.$$phase) {
|
||||
scope.$apply();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render_panel();
|
||||
if ($scope.panel.mode === 'markdown') {
|
||||
$scope.renderMarkdown($scope.panel.content);
|
||||
}
|
||||
else if ($scope.panel.mode === 'html') {
|
||||
$scope.updateContent($scope.panel.content);
|
||||
}
|
||||
else if ($scope.panel.mode === 'text') {
|
||||
$scope.renderText($scope.panel.content);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('newlines', function() {
|
||||
return function (input) {
|
||||
return input.replace(/\n/g, '<br/>');
|
||||
$scope.renderText = function(content) {
|
||||
content = content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<')
|
||||
.replace(/\n/g, '<br/>');
|
||||
|
||||
$scope.updateContent(content);
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('striphtml', function () {
|
||||
return function(text) {
|
||||
return text
|
||||
$scope.renderMarkdown = function(content) {
|
||||
var text = content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<');
|
||||
|
||||
if (converter) {
|
||||
$scope.updateContent(converter.makeHtml(text));
|
||||
}
|
||||
else {
|
||||
require(['./lib/showdown'], function (Showdown) {
|
||||
converter = new Showdown.converter();
|
||||
$scope.updateContent(converter.makeHtml(text));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.updateContent = function(html) {
|
||||
try {
|
||||
$scope.content = $sce.trustAsHtml(templateSrv.replace(html));
|
||||
} catch(e) {
|
||||
console.log('Text panel error: ', e);
|
||||
$scope.content = $sce.trustAsHtml(html);
|
||||
}
|
||||
|
||||
if(!$scope.$$phase) {
|
||||
$scope.$digest();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.openEditor = function() {
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user