Compare commits

..

137 Commits
v1.0 ... v1.3.0

Author SHA1 Message Date
Torkel Ödegaard
dceda6e27d Closes #86, dashboard tags and tag search feature is now done! turned out pretty good! 2014-02-13 07:48:47 +01:00
Torkel Ödegaard
ef264920db moved less file that caused travis build error 2014-02-12 20:47:47 +01:00
Torkel Ödegaard
e9b1c92911 more work on dashboard tags / search (#86) 2014-02-12 20:15:00 +01:00
Torkel Ödegaard
123f90d24d jshint fix 2014-02-12 15:54:37 +01:00
Torkel Ödegaard
ff01764f95 Fixes #91, custom datetime picker is one day off 2014-02-12 15:52:53 +01:00
Torkel Ödegaard
4132dd940a search fix (introduced yesterday) 2014-02-12 14:37:34 +01:00
Torkel Ödegaard
9fc6d3d6f0 Fixed tagsinput bug related to shallow clone of dashboard 2014-02-12 14:35:12 +01:00
Torkel Ödegaard
7d9141a055 Fixes #89, switching between two templated (filtered) dashboards caused error 2014-02-12 12:43:32 +01:00
Torkel Ödegaard
2214c0cdf8 More work on adding tags to dashboard, added bootstrap tagsinput 2014-02-12 12:29:46 +01:00
Torkel Ödegaard
11c5fdc328 Fixes #88, Closed row css bug fix 2014-02-12 12:16:13 +01:00
Torkel Odegaard
c81c4f184b #86 Added tags to dashboard general settings panel, search and search display needs more work (grouping, facets etc) 2014-02-11 22:10:16 +01:00
Torkel Odegaard
a35ed05bc3 Closes #54, template/filter can now use "All", when all is not a wildcard but the specific metric segments specified in the filter query. 2014-02-11 20:58:51 +01:00
Torkel Odegaard
e574314472 Closes #82, dashboard search/load now sorts in alfabetical order 2014-02-11 20:38:50 +01:00
Torkel Ödegaard
45ab6d7c5e Merge pull request #85 from ohTHATaaronbrown/master
Fixes #84: Added implementation of fund parameter ...
2014-02-11 17:16:26 +01:00
Torkel Ödegaard
48325b6452 Fixes #83, stack percent with no line fill (workaround fix for bug in flot.stackpercent) 2014-02-11 09:15:23 +01:00
Torkel Ödegaard
4a1c9ac390 added grafana version info to dashboard settings, and link to releases. 2014-02-11 09:03:21 +01:00
Aaron Brown
a868bab24e Fixes torkelo/grafana/issues#84: Added implementation of fund parameter for summarize graphite function 2014-02-10 17:35:34 -06:00
Torkel Ödegaard
1fada5dd0b build/grunt script fixes 2014-02-10 18:05:52 +01:00
Torkel Ödegaard
a55b9bb8e1 version bump 2014-02-10 17:54:31 +01:00
Torkel Ödegaard
6c0f3d701f Fixes #80, stacked percentage and min/max bug, Fixes #81 (grid y min/max not working) 2014-02-10 17:53:39 +01:00
Torkel Odegaard
e86f4f94ef removed annotation test 2014-02-09 22:00:07 +01:00
Torkel Odegaard
ca258deb7a Merge branch 'master' of github.com:torkelo/grafana 2014-02-09 21:57:37 +01:00
Torkel Odegaard
79ca016409 More work on annotations 2014-02-09 21:57:07 +01:00
Torkel Odegaard
869bebed6e more work on annotations 2014-02-09 19:31:06 +01:00
Torkel Odegaard
0e3c0fcc2e Started work on annotations submenu control 2014-02-09 15:43:43 +01:00
Torkel Ödegaard
bf9c0bb914 Merge pull request #76 from axe-felix/patch-1
added support for 'less is worse' thresholds
2014-02-09 13:41:12 +01:00
axe-felix
8669599089 added support for 'less is worse' thresholds
Sometimes you want to express 'less is worse' thresholds. That means setting the error threshold below the warning threshold. The problem was, that the error marking always went up to +Infinity. Concerns 16599a07a9.
2014-02-09 10:13:32 +01:00
Torkel Odegaard
775c0ff2f1 updated readme 2014-02-08 20:47:04 +01:00
Torkel Odegaard
98ab75029e added link to wiki article on scripted dashboards 2014-02-08 20:45:01 +01:00
Torkel Odegaard
682740a020 fix for requirejs optimizer and config.js fallback 2014-02-08 20:30:19 +01:00
Torkel Odegaard
278decfb87 fixed jshint error 2014-02-08 20:13:43 +01:00
Torkel Odegaard
b8a0ca082a updated readme and bumped version to v1.1.1 2014-02-08 20:10:59 +01:00
Torkel Odegaard
f03e4be683 Closes #72, added scripted dashboard example (feature inherited from kibana) 2014-02-08 20:10:59 +01:00
Torkel Ödegaard
ba6a6292f9 trying to get angular mocks / testing to work 2014-02-08 16:38:17 +01:00
Torkel Odegaard
16599a07a9 Closes #70, Fixes #50, Grid thresholds feature 2014-02-08 14:21:41 +01:00
Torkel Ödegaard
a9ac11216d Moved parser/target editor warning icon to the right, now the hide/show eye is still usefull even when only working with the text editor 2014-02-07 15:38:59 +01:00
Torkel Ödegaard
8e2008f821 Fixes #69, lexer correctly tokenizes number-number segments, numericLiterals are only considered literal token if it is folled by a punctuator. 2014-02-07 15:37:03 +01:00
Torkel Ödegaard
0a8b9bad4f Fixes #67, decimal inputs for function parameters (like scale) 2014-02-07 14:07:24 +01:00
Torkel Ödegaard
bd4b75f5d8 added single decimal to short y formater 2014-02-07 13:27:53 +01:00
Torkel Ödegaard
084e7e7d73 Fixes #68, bug when trying open dashboard while in edit/fullscreen mode 2014-02-07 13:27:17 +01:00
Torkel Ödegaard
76f4e3c5b4 Fixed issue where series color or axis change would note take immediate effect (needed a full refresh). 2014-02-07 13:10:35 +01:00
Torkel Ödegaard
adb97a0f3e Fixes #42 : Auto Y scale is working as it should (even when using line fill) 2014-02-07 09:28:54 +01:00
Torkel Ödegaard
f2f435b4cb Changed name off config.js to config.sample.js to make it easier to upgrade with having to worry about config being overwwritten, if there is no config.js file config.sample.js will be read instead (requirejs fallback). Closes #65 2014-02-06 17:10:25 +01:00
Torkel Ödegaard
0015f81645 Implementation of legend values (min, max, current, total, avg), Closes #60, Fixes #14 2014-02-06 13:29:55 +01:00
Torkel Ödegaard
ed76335c96 small change to time axis formats, more graphite like now 2014-02-06 10:39:24 +01:00
Torkel Ödegaard
e0a35a3958 work in progress for legend values 2014-02-06 10:19:26 +01:00
Torkel Ödegaard
d9ec9ed1ef removed metrics tab in dash editor modal (only created confusion, the feature is not ready) 2014-02-06 07:55:19 +01:00
Torkel Ödegaard
0032f934c9 Closes #62, there is now a New button in the open / search dashboard view. 2014-02-05 20:56:48 +01:00
Torkel Ödegaard
8816e18027 Merge pull request #63 from jippi/feature/unique-local-storage-name
Make sure kibana and grafana on same hostname do not interfere with each other
2014-02-05 19:16:02 +01:00
Christian Winther
e8be950752 Make sure kibana and grafana on same hostname do not interfere with each other
This happens if you have kibana on /kibana and grafana on /grafana, resulting in some bad redirects
2014-02-05 18:02:15 +00:00
Torkel Ödegaard
aea4b37760 added highestCurrent function (Fixes #57) 2014-02-05 16:10:09 +01:00
Torkel Ödegaard
1aff1a0000 mini improvement to xasis format (now shows just hours for larger time spans) 2014-02-05 15:39:50 +01:00
Torkel Ödegaard
df54c17dca small refactoring of check if other panel is in fullscreen edit/view mode 2014-02-05 15:26:34 +01:00
Torkel Ödegaard
73c5883f0d small fix for png width error when resize in edit mode / fullscreen mode 2014-02-05 15:13:24 +01:00
Torkel Ödegaard
e3561ce555 fixed jshint errors, dam fever is coming back, time to sleep... 2014-02-05 13:35:32 +01:00
Torkel Ödegaard
a5feb639d9 Merge pull request #58 from andreparodi/master
added nonNegativeDerivative function
2014-02-05 13:28:38 +01:00
Torkel Ödegaard
ac33286c8c added sortByName function (thx @jippi) 2014-02-05 13:23:14 +01:00
Torkel Ödegaard
19d5d32229 simplified a lot of the time series data manipulation prior to handing it to flot, kibana used a lot of code to sort and make sure all time points needed had values, this is not needed for graphite time series data. Fixes som strange issues with "null point mode" and inconsistent rendering between graphite and grafana flot, 2014-02-05 13:14:00 +01:00
Torkel Ödegaard
d90f2537a4 added unit (y axis format) mapping support for png renderer (#22) 2014-02-05 11:30:54 +01:00
Torkel Ödegaard
487aa1f2c3 Added hideAxes and hideYAxis mapping support for png renderer. #22 2014-02-05 11:21:37 +01:00
Torkel Ödegaard
be9ab15c4e added grid min max support for png renderer (url property mappings that is), #22 2014-02-05 11:14:13 +01:00
Torkel Ödegaard
7d4f0d24d5 restructured style settings between axes & grid, and Display Styles 2014-02-05 11:12:47 +01:00
Torkel Ödegaard
bafa334b94 moved timezone correction setting from graph style to dashboard 2014-02-05 09:42:49 +01:00
Torkel Ödegaard
80c5f99f3b Removed pointless setting "selectable" (interactive) 2014-02-05 08:56:10 +01:00
Torkel Ödegaard
212bf5df98 small fix to png rendering 2014-02-05 08:41:46 +01:00
Andre Parodi
32189afc67 added nonNegativeDerivative function 2014-02-02 23:16:53 +01:00
Torkel Ödegaard
0fa6b34f1a working on native png renderer support 2014-02-01 22:06:10 +01:00
Torkel Ödegaard
4d6bb8b6be progress on translating grafana/flot options to graphite png renderer options 2014-02-01 21:23:43 +01:00
Torkel Ödegaard
82da75bc0c began work on adding support for native graphite png renderer support (#22) 2014-02-01 21:23:22 +01:00
Torkel Ödegaard
beda378314 Fixes #45, zero values treated as null values 2014-02-01 14:09:04 +01:00
Torkel Ödegaard
6734864acd small change to readme.md 2014-02-01 13:16:39 +01:00
Torkel Ödegaard
9f548870d0 added build status to readme.md 2014-02-01 13:00:32 +01:00
Torkel Ödegaard
d2bae43d26 Adding travis-ci build 2014-02-01 11:26:22 +01:00
Torkel Ödegaard
e77e43faab Fixes #46, you can now edit a graph with no title 2014-02-01 11:02:24 +01:00
Torkel Ödegaard
224d2f92c3 Save now closes edit mode. (#52). 2014-02-01 10:51:35 +01:00
Torkel Ödegaard
250e354659 Dashboard search fix, only query against title field (Closes #51,Fixes #24) 2014-02-01 09:53:57 +01:00
John Dyer
2e59587c8e Clean up, and set default in config to false 2014-02-01 09:53:56 +01:00
John Dyer
6f4520254b Make search support elastic search clusters with _all disabled. Feature is disabled by default 2014-02-01 09:53:55 +01:00
Torkel Ödegaard
b939c02da0 updated property docs in config.js 2014-02-01 09:03:51 +01:00
Torkel Ödegaard
9e900d5885 fixed small variable name issue with pull request #55 (jshint warning) 2014-02-01 08:53:59 +01:00
Torkel Ödegaard
38d3450160 refactoring, moved graph directive to a seperate js file 2014-02-01 08:47:16 +01:00
Torkel Ödegaard
2c5bfec089 Merge pull request #55 from oroce/elasticsearch-basicauth
basic authentication added for elasticsearch
2014-01-31 23:45:48 -08:00
oroce
a56a2b0057 basic authentication added for elasticsearch 2014-01-31 21:23:32 +01:00
Torkel Ödegaard
89224401a9 Fixes #38, Lexer & parser now handles metrics segments that begin with numbers or only have numbers 2014-01-29 11:00:25 +01:00
Torkel Ödegaard
1c17c8661a Fixes #43, add grunt-cli as dev dependency 2014-01-29 09:17:17 +01:00
Torkel Ödegaard
4ed65891c2 Merge pull request #39 from jeviolle/master
fixed missing module for 'moment' and jslint missing semi colon
2014-01-28 03:37:35 -08:00
Rick Briganti
49e131ce21 updated how moment is included and removed moment as a dep since it is included 2014-01-27 17:30:28 -05:00
Rick Briganti
06f4b017d6 fixed missing module for 'moment' and jslint missing semi colon 2014-01-27 14:01:36 -05:00
Torkel Ödegaard
f52450aef4 Merge pull request #37 from tmeinlschmidt/master
fixes 'derivate' function name - correct name is 'derivative'
2014-01-26 23:33:43 -08:00
Tom Meinlschmidt
6476843e95 - fixes 'derivate' function name - corrent name is 'derivative' 2014-01-27 02:00:04 +01:00
Torkel Ödegaard
67d11dffb9 small readme fix 2014-01-25 17:25:21 +01:00
Torkel Ödegaard
58dbb01e76 Added new config setting timezoneOffset, usefull when your graphite server is on a different timezone thant your users browsers. This is not optimal (for example if your users have different timezones) can be improved by finding a way to query the timezone different between browser and graphite server. Or fix so that the &tz graphite parameter works for json, then all absolute filters can use UTC time. #31 2014-01-24 17:00:54 +01:00
Torkel Ödegaard
81e9a483bc Merge pull request #29 from rsommer/patch-1
Update graphiteSrv.js
2014-01-24 05:23:04 -08:00
Torkel Ödegaard
5688f792cb Merge pull request #30 from johanwiren/fix_graphite_import
Fixed graphite import to include all graphs in a dashboard.
2014-01-24 05:16:54 -08:00
Johan Wirén
3ffc04be4d Fixed graphite import to include all graphs in a dashboard. 2014-01-24 14:08:09 +01:00
Roland Sommer
55e586c2c6 Update graphiteSrv.js
If 'now' is not in 'data', the result is -1, so it should be '>= 0' ...
2014-01-24 13:32:16 +01:00
Torkel Ödegaard
76b535a2e4 Merge pull request #26 from johanwiren/add_title_on_import
Sets graph title when importing from graphite
2014-01-23 20:27:34 -08:00
Johan Wirén
6e27f97bc9 Sets graph title when importing from graphite 2014-01-24 00:07:43 +01:00
Torkel Ödegaard
d9ada8d94e Merge pull request #23 from longnguyen11288/master
Fixed typo in import
2014-01-23 10:39:24 -08:00
Long Nguyen
b3d67c3ed4 Fixed typo in import 2014-01-23 10:35:56 -08:00
Torkel Odegaard
459b0dc5bd Updated readme with link to getting started guide 2014-01-23 19:19:56 +01:00
Torkel Ödegaard
aefd95890e Merge pull request #21 from dcarley/probject_typo
Correct typo "probject".
2014-01-23 10:06:50 -08:00
Dan Carley
36d66494d3 Correct typo "probject". 2014-01-23 17:48:51 +00:00
Torkel Ödegaard
a8ba77f40c bumbed version 2014-01-23 13:57:52 +01:00
Torkel Ödegaard
0c0ca895a8 Closes #9 , new y format 'ms' formats millisecond values as "200 ms" , "3 s", "4 min" etc. 2014-01-23 13:31:02 +01:00
Torkel Ödegaard
3d35595397 Fixed fullscreen not increasing graph height as before 2014-01-23 13:22:35 +01:00
Torkel Ödegaard
533e176566 Fixed issue with date parsing, now saved absolute range should work again 2014-01-23 12:34:35 +01:00
Torkel Ödegaard
915f2a4914 updated readme.md 2014-01-23 11:58:59 +01:00
Torkel Odegaard
d21bd796f8 Added documentation in Readme.md for how to configure apache2 with Basic Auth and CORS 2014-01-22 22:25:15 +01:00
Torkel Odegaard
51186397d7 Fixes #16 (Basic Authentication), set graphiteUrl: http://username:password@domain.com and it should work, but requires special CORS settings in apache2 2014-01-22 22:19:58 +01:00
Torkel Ödegaard
e2dd201e4a Fixes #13 (partially) Use relative time ranges whenever possible when querying graphite 2014-01-22 13:05:07 +01:00
Torkel Ödegaard
f0c8b70f47 fixed config.js grafana_index default value was accidentally changed 2014-01-22 08:27:23 +01:00
Torkel Ödegaard
d259f14267 Initial work on #9 (millisecond y axis formater) 2014-01-21 22:06:54 +01:00
Torkel Ödegaard
3b6bf80ea4 removed more unused code 2014-01-21 21:45:25 +01:00
Torkel Ödegaard
0e4dc5a888 clean up unused code, left over from kibana 2014-01-21 21:11:09 +01:00
Torkel Ödegaard
fc713be9ce Fixes #12, operation without elastic search should now with without error 2014-01-21 21:05:54 +01:00
Torkel Ödegaard
a6df1a9320 Partial fix for #10 , the lexer & parser can now handle curly brace, but target editor needs to be updated to support selecting multiple segment nodes 2014-01-21 12:43:32 +01:00
Torkel Ödegaard
62edf0d266 updated readme and bumped version 2014-01-21 08:38:30 +01:00
Torkel Ödegaard
2ee2266865 Fixes #4, also improves responsive layout for mobiles 2014-01-20 22:40:06 +01:00
Torkel Ödegaard
0c1df4250e ported back some improvements from kibana 2014-01-20 22:28:35 +01:00
Torkel Ödegaard
61d6654547 improved drag and drop 2014-01-20 22:15:24 +01:00
Torkel Ödegaard
1170493ebe fixed loading spinner position and other minor changes 2014-01-20 19:40:39 +01:00
Torkel Ödegaard
1742c03b23 small refactoring 2014-01-20 18:59:31 +01:00
Torkel Ödegaard
00777ea14a added annotations to roadmap 2014-01-20 18:45:22 +01:00
Torkel Ödegaard
0524427a3b changes to flot.events 2014-01-20 18:42:37 +01:00
Torkel Ödegaard
8a469863d4 Fixes #1, duplicate panel with 3 colspan remaining on row 2014-01-20 18:35:24 +01:00
Torkel Ödegaard
73b96ef4ca updated default example dashboard with welcome text and an example graph using randomWalk function 2014-01-20 16:48:03 +01:00
Torkel Ödegaard
ad4c0ab5b6 added more function definitions, added small hack to make randomWalk and countLine that do not take any series work with the parser and target editor (the whole target editor and parsing & back to target rendering needs to be rewritten to handle functions that take multiple series) 2014-01-20 16:48:02 +01:00
Torkel Ödegaard
203b8df7f8 Merge pull request #7 from deniszh/master
Fixing download link in README
2014-01-20 02:57:28 -08:00
Denys Zhdanov
185d713a0e Fixing download link in README 2014-01-20 11:51:50 +01:00
Torkel Ödegaard
a089cd6e35 small fixes to readme.md 2014-01-20 11:00:07 +01:00
Torkel Ödegaard
8101f01de9 updated readme.md 2014-01-20 10:57:59 +01:00
Torkel Ödegaard
700b6d9450 spelling fix in readme.md 2014-01-20 09:47:48 +01:00
Torkel Ödegaard
b0d016f5e1 Merge branch 'master' of github.com:torkelo/grafana 2014-01-20 09:46:01 +01:00
Torkel Ödegaard
c3ceb90982 updated readme again 2014-01-19 17:39:15 +01:00
Torkel Ödegaard
0097e5fecd updated readme 2014-01-19 17:38:34 +01:00
Torkel Ödegaard
cd5b17b002 fix: segment dropdown is now scrollable 2014-01-15 17:38:46 +01:00
89 changed files with 8753 additions and 1581 deletions

2
.gitignore vendored
View File

@@ -2,5 +2,5 @@ node_modules
.aws-config.json
dist
web.config
config.dev.js
config.js
*.sublime-workspace

5
.travis.yml Normal file
View File

@@ -0,0 +1,5 @@
language: node_js
node_js:
- "0.10"
before_script:
- npm install -g grunt-cli

View File

@@ -1,6 +1,8 @@
# Grafana - Graphite Dashboard
[Grafana](http://grafana.org) [![Build Status](https://api.travis-ci.org/torkelo/grafana.png)](https://travis-ci.org/torkelo/grafana)
=================
A beautiful, easy to use and feature rich Graphite dashboard replacement and graph editor. Visit [grafana.org](http://grafana.org) for screenshots and an overview.
A beautifully, easy to use and feature rich Graphite dashboard replacement and graph editor. Visit [grafana.org](http://grafana.org) for screenshots and feature lists.
![](http://grafana.org/assets/img/edit_dashboards.png)
# Features
## Graphite Target Editor
@@ -11,7 +13,7 @@ A beautifully, easy to use and feature rich Graphite dashboard replacement and g
- Templating
- Integrated function documentation (TODO)
- Click & drag functions to rearrange order (TODO)
- Much more...
- Native Graphite PNG render support
## Graphing
- Fast rendering, even over large timespans.
@@ -21,7 +23,7 @@ A beautifully, easy to use and feature rich Graphite dashboard replacement and g
- Smart Y-axis formating
- Series toggles & color selector
- Axis labels
- Fullscreen views and more...
- Fullscreen views and more...
## Dashboards
- Create and edit dashboards
@@ -31,15 +33,25 @@ A beautifully, easy to use and feature rich Graphite dashboard replacement and g
- Import & export dashboard (json file)
- Import dashboard from Graphite
- Templating
- Much more...
- [Scripted dashboards](https://github.com/torkelo/grafana/wiki/Scripted-dashboards) (generate from js script and url parameters)
# 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.
# Installation
- Download and extract the [latest release](https://github.com/asimov-deploy/asimov-deploy-winagent/releases/latest).
- Edit config.js , the change graphiteUrl and elasticsearch to the correct urls. The urls entered here must be reachable by your browser.
- Point your browser to the installation.
- Download and extract the [latest release](https://github.com/torkelo/grafana/releases).
- Edit 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.
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
When you have Grafana up an running, read the [Getting started](https://github.com/torkelo/grafana/wiki/Getting-started) guide for
an introduction on how to use Grafana.
# 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:
@@ -47,10 +59,14 @@ If you haven't used an alternative dashboard for graphite before you need to ena
Header set Access-Control-Allow-Origin "*"
Header set Access-Control-Allow-Methods "GET, OPTIONS"
Header set Access-Control-Allow-Headers "origin, authorization, accept"
```
If your Graphite web is proteced by basic authentication, you have to enable the HTTP verb OPTIONS. This looks like the following for Apache:
```
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
<Location />
AuthName "graphs restricted"
AuthType Basic
@@ -65,18 +81,22 @@ If your Graphite web is proteced by basic authentication, you have to enable the
- Improve and refine the target parser and editing
- Improve graphite import feature
- Refine and simplify common tasks
- More panel types (not just graphs)
- More panel types (not just graphs)
- Use elasticsearch to search for metrics
- Improve template support
- Add support for other time series databases like InfluxDB
- Improve template support
- Annotate graph by querying ElasticSearch for events (or other event sources)
- Add support for other time series databases like InfluxDB
# Contribute
If you have any idea for 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!
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!
![](http://grafana.org/assets/img/edit_dashboards.png)
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
Grafana is distributed under Apache 2.0 License.
Grafana is distributed under Apache 2.0 License.

View File

@@ -4,7 +4,7 @@
"company": "Coding Instinct AB"
},
"name": "grafana",
"version": "0.1",
"version": "1.2.0",
"repository": {
"type": "git",
"url": "http://github.com/torkelo/grafana.git"
@@ -43,7 +43,15 @@
"karma": "~0.10.9",
"grunt-karma": "~0.6.2",
"karma-mocha": "~0.1.1",
"karma-expect": "~1.0.0"
"karma-expect": "~1.0.0",
"grunt-cli": "~0.1.13"
},
"engines": {
"node": "0.10.x",
"npm": "1.2.x"
},
"scripts": {
"test": "grunt"
},
"license": "Apache License"
}

View File

@@ -12,7 +12,7 @@ define([
'angular-strap',
'angular-dragdrop',
'extend-jquery',
'bindonce',
'bindonce'
],
function (angular, $, _, appLevelRequire) {
@@ -27,7 +27,7 @@ function (angular, $, _, appLevelRequire) {
register_fns = {};
// This stores the Kibana revision number, @REV@ is replaced by grunt.
app.constant('kbnVersion',"@REV@");
app.constant('grafanaVersion',"@grafanaVersion@");
// Use this for cache busting partials
app.constant('cacheBust',"cache-bust="+Date.now());

View File

@@ -4,94 +4,6 @@ function($, _, moment) {
var kbn = {};
kbn.get_object_fields = function(obj) {
var field_array = [];
obj = kbn.flatten_json(obj._source);
for (var field in obj) {
field_array.push(field);
}
return field_array.sort();
};
kbn.get_all_fields = function(data,flat) {
return _.uniq(_.without(_.reduce(data,function(memo,hit) {
return flat ? memo.concat(_.keys(kbn.flatten_json(hit._source))) : memo.concat(_.keys(hit._source));
},[]),'$$hashkey'));
};
kbn.has_field = function(obj,field) {
var obj_fields = kbn.get_object_fields(obj);
if (_.inArray(obj_fields,field) < 0) {
return false;
} else {
return true;
}
};
kbn.get_related_fields = function(docs,field) {
var field_array = [];
_.each(docs, function(doc) {
var keys = _.keys(doc);
if(_.contains(keys,field)) {
field_array = field_array.concat(keys);
}
});
var counts = _.countBy(_.without(field_array,field),function(field){return field;});
return _.map(counts, function(num, key){return {name:key,count:num};});
};
kbn.recurse_field_dots = function(object,field) {
var value = null;
var nested;
if (typeof object[field] !== 'undefined') {
value = object[field];
}
else if (nested = field.match(/(.*?)\.(.*)/)) {
if(typeof object[nested[1]] !== 'undefined') {
value = (typeof object[nested[1]][nested[2]] !== 'undefined') ?
object[nested[1]][nested[2]] : kbn.recurse_field_dots(
object[nested[1]],nested[2]);
}
}
return value;
};
kbn.top_field_values = function(docs,field,count,grouped) {
var all_values = _.pluck(docs,field),
groups = {},
counts,
hasArrays;
// manually grouping into pairs allows us to keep the original value,
_.each(all_values, function (value) {
var k;
if(_.isArray(value)) {
hasArrays = true;
}
if(_.isArray(value) && !grouped) {
k = value;
} else {
k = _.isUndefined(value) ? '' : [value.toString()];
}
_.each(k, function(key) {
if (_.has(groups, key)) {
groups[key][1] ++;
} else {
groups[key] = [(grouped ? value : key), 1];
}
});
});
counts = _.values(groups).sort(function(a, b) {
return a[1] - b[1];
}).reverse().slice(0,count);
return {
counts: counts,
hasArrays : hasArrays
};
};
/**
* Calculate a graph interval
*
@@ -593,5 +505,17 @@ function($, _, moment) {
return (size.toFixed(decimals) + ext);
};
kbn.msFormat = function(size) {
if (size < 1000) {
return size.toFixed(0) + " ms";
}
else if (size < 60000) {
return (size / 1000).toFixed(1) + " s";
}
else {
return (size / 60000).toFixed(1) + " min";
}
};
return kbn;
});

View File

@@ -5,7 +5,7 @@ require.config({
baseUrl: 'app',
// urlArgs: 'r=@REV@',
paths: {
config: '../config',
config: ['../config', '../config.sample'],
settings: 'components/settings',
kbn: 'components/kbn',
@@ -21,6 +21,8 @@ require.config({
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',
@@ -43,12 +45,22 @@ require.config({
modernizr: '../vendor/modernizr-2.6.1',
elasticjs: '../vendor/elasticjs/elastic-angular-client',
'bootstrap-tagsinput': '../vendor/tagsinput/bootstrap-tagsinput',
},
shim: {
underscore: {
exports: '_'
},
spectrum: {
deps: ['jquery']
},
crypto: {
exports: 'Crypto'
},
angular: {
deps: ['jquery','config'],
exports: 'angular'
@@ -94,6 +106,7 @@ require.config({
elasticjs: ['angular', '../vendor/elasticjs/elastic'],
'bootstrap-tagsinput': ['jquery'],
},
waitSeconds: 60,
});

View File

@@ -1,5 +1,8 @@
define(['underscore'],
function (_) {
define([
'underscore',
'crypto',
],
function (_, crypto) {
"use strict";
return function Settings (options) {
@@ -10,11 +13,13 @@ function (_) {
* @type {Object}
*/
var defaults = {
elasticsearch : "http://"+window.location.hostname+":9200",
graphiteUrl : "http://"+window.location.hostname+":8080",
panel_names : [],
default_route : '/dashboard/file/default.json',
grafana_index : 'grafana-dash'
elasticsearch : "http://"+window.location.hostname+":9200",
graphiteUrl : "http://"+window.location.hostname+":8080",
panel_names : [],
default_route : '/dashboard/file/default.json',
grafana_index : 'grafana-dash',
elasticsearch_all_disabled : false,
timezoneOffset : null,
};
// This initializes a new hash on purpose, to avoid adding parameters to
@@ -24,6 +29,19 @@ function (_) {
settings[key] = typeof options[key] !== 'undefined' ? options[key] : defaults[key];
});
var basicAuth = function(url) {
var passwordAt = url.indexOf('@');
if (passwordAt > 0) {
var userStart = url.indexOf('//') + 2;
var userAndPassword = url.substring(userStart, passwordAt);
var bytes = crypto.charenc.Binary.stringToBytes(userAndPassword);
var base64 = crypto.util.bytesToBase64(bytes);
return base64;
}
};
settings.graphiteBasicAuth = basicAuth(settings.graphiteUrl);
settings.elasticsearchBasicAuth = basicAuth(settings.elasticsearch);
return settings;
};
});

View File

@@ -2,6 +2,7 @@ define([
'./dash',
'./dashLoader',
'./row',
'./submenuCtrl',
'./pulldown',
'./search',
'./metricKeys',

View File

@@ -30,7 +30,7 @@ function (angular, $, config, _) {
var module = angular.module('kibana.controllers');
module.controller('DashCtrl', function(
$scope, $rootScope, $route, ejsResource, dashboard, alertSrv, panelMove, esVersion, keyboardManager) {
$scope, $rootScope, $route, ejsResource, dashboard, alertSrv, panelMove, keyboardManager, grafanaVersion) {
$scope.requiredElasticSearchVersion = ">=0.90.3";
@@ -38,6 +38,8 @@ function (angular, $, config, _) {
index: 0
};
$scope.grafanaVersion = grafanaVersion[0] === '@' ? 'version: master' : grafanaVersion;
// For moving stuff around the dashboard.
$scope.panelMoveDrop = panelMove.onDrop;
$scope.panelMoveStart = panelMove.onStart;
@@ -52,14 +54,13 @@ function (angular, $, config, _) {
$scope._ = _;
$scope.dashboard = dashboard;
$scope.dashAlerts = alertSrv;
$scope.esVersion = esVersion;
// Clear existing alerts
alertSrv.clearAll();
$scope.reset_row();
$scope.ejs = ejsResource(config.elasticsearch);
$scope.ejs = ejsResource(config.elasticsearch, config.elasticsearchBasicAuth);
$scope.bindKeyboardShortcuts();
};
@@ -73,6 +74,12 @@ function (angular, $, config, _) {
$rootScope.fullscreen = false;
});
$rootScope.$on('dashboard-saved', function() {
if ($rootScope.fullscreen) {
$rootScope.$emit('panel-fullscreen-exit');
}
});
keyboardManager.bind('ctrl+f', function(evt) {
$rootScope.$emit('open-search', evt);
}, { inputDisabled: true });

View File

@@ -65,17 +65,18 @@ function (angular, _, moment) {
type,
($scope.elasticsearch.title || dashboard.current.title),
($scope.loader.save_temp_ttl_enable ? ttl : false)
).then(
function(result) {
if(!_.isUndefined(result._id)) {
alertSrv.set('Dashboard Saved','This 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);
}
} else {
).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', 'This 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');
});
};
@@ -102,16 +103,6 @@ function (angular, _, moment) {
);
};
$scope.elasticsearch_dblist = function(query) {
dashboard.elasticsearch_list(query,$scope.loader.load_elasticsearch_size).then(
function(result) {
if(!_.isUndefined(result.hits)) {
$scope.hits = result.hits.total;
$scope.elasticsearch.dashboards = result.hits.hits;
}
});
};
$scope.save_gist = function() {
dashboard.save_gist($scope.gist.title).then(
function(link) {

View File

@@ -65,11 +65,13 @@ function (angular, app, _) {
_.each(state.graphs, function(graph) {
if (currentRow.panels.length === graphsPerRow) {
currentRow = angular.copy(rowTemplate);
newDashboard.rows.push(currentRow);
}
panel = {
type: 'graphite',
span: 12 / graphsPerRow,
title: graph[1].title,
targets: []
};
@@ -87,4 +89,4 @@ function (angular, app, _) {
});
});
});

View File

@@ -17,6 +17,8 @@ function (angular, _, config, gfunc, Parser) {
parseTarget();
};
// The way parsing and the target editor works needs
// to be rewritten to handle functions that take multiple series
function parseTarget() {
$scope.functions = [];
$scope.segments = [];
@@ -72,7 +74,13 @@ function (angular, _, config, gfunc, Parser) {
throw { message: 'invalid number of parameters to method ' + func.def.name };
}
func.params[index - 1] = astNode.value;
if (index === 0) {
func.params[index] = astNode.value;
}
else {
func.params[index - 1] = astNode.value;
}
break;
case 'metric':
@@ -141,16 +149,7 @@ function (angular, _, config, gfunc, Parser) {
}
function wrapFunction(target, func) {
var targetWrapped = func.def.name + '(' + target;
_.each(func.params, function(param) {
if (_.isString(param)) {
targetWrapped += ",'" + param + "'";
}
else {
targetWrapped += "," + param;
}
});
return targetWrapped + ')';
return func.render(target);
}
$scope.getAltSegments = function (index) {

View File

@@ -10,7 +10,13 @@ function (angular, _, config) {
module.controller('MetricKeysCtrl', function($scope, $http, $q) {
var elasticSearchUrlForMetricIndex = config.elasticsearch + '/' + config.grafana_metrics_index + '/';
var httpOptions = {};
if (config.elasticsearchBasicAuth) {
httpOptions.withCredentials = true;
httpOptions.headers = {
"Authorization": "Basic " + config.elasticsearchBasicAuth
};
}
$scope.init = function () {
$scope.metricPath = "prod.apps.api.boobarella.*";
$scope.metricCounter = 0;
@@ -77,7 +83,7 @@ function (angular, _, config) {
function deleteIndex()
{
var deferred = $q.defer();
$http.delete(elasticSearchUrlForMetricIndex)
$http.delete(elasticSearchUrlForMetricIndex, httpOptions)
.success(function() {
deferred.resolve('ok');
})
@@ -124,7 +130,7 @@ function (angular, _, config) {
}
}
}
});
}, httpOptions);
}
function receiveMetric(result) {

View File

@@ -12,10 +12,10 @@ function (angular, _, config, $) {
module.controller('SearchCtrl', function($scope, $rootScope, dashboard, $element, $location) {
$scope.init = function() {
$scope.elasticsearch = $scope.elasticsearch || {};
$scope.giveSearchFocus = 0;
$scope.selectedIndex = -1;
$scope.results = {dashboards: [], tags: [], metrics: []};
$scope.query = { query: 'title:' };
$rootScope.$on('open-search', $scope.openSearch);
};
@@ -30,7 +30,15 @@ function (angular, _, config, $) {
$scope.selectedIndex--;
}
if (evt.keyCode === 13) {
var selectedDash = $scope.search_results.dashboards[$scope.selectedIndex];
if ($scope.tagsOnly) {
var tag = $scope.results.tags[$scope.selectedIndex];
if (tag) {
$scope.filterByTag(tag.term);
}
return;
}
var selectedDash = $scope.results.dashboards[$scope.selectedIndex];
if (selectedDash) {
$location.path("/dashboard/elasticsearch/" + encodeURIComponent(selectedDash._id));
setTimeout(function(){
@@ -40,31 +48,69 @@ function (angular, _, config, $) {
}
};
$scope.elasticsearch_dashboards = function(queryStr) {
dashboard.elasticsearch_list(queryStr + '*', 50).then(function(results) {
if(_.isUndefined(results.hits)) {
$scope.search_results = { dashboards: [] };
return;
$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.search_results = { dashboards: results.hits.hits };
});
if (query[query.length - 1] !== '*') {
query += '*';
}
}
return request
.query($scope.ejs.QueryStringQuery(query))
.sort('_uid')
.facet($scope.ejs.TermsFacet("tags").field("tags").order('term').size(50))
.size(50).doSearch()
.then(function(results) {
if(_.isUndefined(results.hits)) {
$scope.results.dashboards = [];
$scope.results.tags = [];
return;
}
$scope.tagsOnly = tagsOnly;
$scope.results.dashboards = results.hits.hits;
$scope.results.tags = results.facets.tags.terms;
});
};
$scope.toggleImport = function ($event) {
$event.stopPropagation();
$scope.showImport = !$scope.showImport;
$scope.filterByTag = function(tag, evt) {
$scope.query.query = "tags:" + tag + " AND title:";
$scope.search();
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
if (evt) {
evt.stopPropagation();
evt.preventDefault();
}
};
$scope.elasticsearch_dblist = function(queryStr) {
$scope.showTags = function(evt) {
evt.stopPropagation();
$scope.tagsOnly = !$scope.tagsOnly;
$scope.query.query = $scope.tagsOnly ? "tags!:" : "";
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
$scope.selectedIndex = -1;
};
$scope.search = function() {
$scope.showImport = false;
$scope.selectedIndex = -1;
queryStr = queryStr.toLowerCase();
var queryStr = $scope.query.query.toLowerCase();
if (queryStr.indexOf('m:') !== 0) {
$scope.elasticsearch_dashboards(queryStr);
queryStr = queryStr.replace(' and ', ' AND ');
$scope.searchDasboards(queryStr);
return;
}
@@ -87,10 +133,10 @@ function (angular, _, config, $) {
results.then(function(results) {
if (results && results.hits && results.hits.hits.length > 0) {
$scope.search_results = { metrics: results.hits.hits };
$scope.results.metrics = { metrics: results.hits.hits };
}
else {
$scope.search_results = { metric: [] };
$scope.results.metrics = { metric: [] };
}
});
};
@@ -101,7 +147,8 @@ function (angular, _, config, $) {
}
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
$scope.elasticsearch_dblist("");
$scope.query.query = 'title:';
$scope.search();
};
$scope.addMetricToCurrentDashboard = function (metricId) {
@@ -120,10 +167,17 @@ function (angular, _, config, $) {
});
};
$scope.toggleImport = function ($event) {
$event.stopPropagation();
$scope.showImport = !$scope.showImport;
};
$scope.newDashboard = function() {
$location.url('/dashboard/file/empty.json');
};
});
module.directive('xngFocus', function() {
return function(scope, element, attrs) {
$(element).click(function(e) {

View File

@@ -0,0 +1,30 @@
define([
'angular',
'app',
'underscore'
],
function (angular, app, _) {
'use strict';
var module = angular.module('kibana.controllers');
module.controller('SubmenuCtrl', function($scope) {
var _d = {
collapse: false,
notice: false,
enable: true
};
_.defaults($scope.pulldown,_d);
$scope.init = function() {
$scope.panel = $scope.pulldown;
$scope.row = $scope.pulldown;
};
$scope.init();
}
);
});

View File

@@ -1,16 +1,154 @@
{
"title": "New Dashboard",
"title": "Welcome to Grafana!",
"services": {
"filter": {
"list": [],
"time": {
"from": "now-5m",
"from": "now-6h",
"to": "now"
}
}
},
"rows": [],
"rows": [
{
"title": "Welcome to Grafana",
"height": "150px",
"editable": true,
"collapse": false,
"collapsable": true,
"panels": [
{
"error": false,
"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.",
"style": {},
"title": "Welcome to Grafana"
}
],
"notice": false
},
{
"title": "test",
"height": "250px",
"editable": true,
"collapse": false,
"collapsable": true,
"panels": [
{
"span": 12,
"editable": true,
"type": "graphite",
"x-axis": true,
"y-axis": true,
"scale": 1,
"y_formats": ["short", "short"],
"grid": {
"max": null,
"min": 0
},
"resolution": 100,
"lines": true,
"fill": 1,
"linewidth": 2,
"points": false,
"pointradius": 5,
"bars": false,
"stack": true,
"spyable": true,
"options": false,
"legend": true,
"interactive": true,
"legend_counts": true,
"timezone": "browser",
"percentage": false,
"zerofill": true,
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {
"value_type": "cumulative",
"query_as_alias": true
},
"targets": [
{
"target": "randomWalk('random walk')"
},
{
"target": "randomWalk('random walk2')"
},
{
"target": "randomWalk('random walk3')"
}
],
"aliasColors": {},
"aliasYAxis": {},
"title": "Graphite test"
}
],
"notice": false
}
],
"editable": true,
"failover": false,
"panel_hints": true
"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": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
],
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"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
}

View File

@@ -0,0 +1,83 @@
{
"title": "New Dashboard",
"services": {
"filter": {
"list": [],
"time": {
"from": "now-6h",
"to": "now"
}
}
},
"rows": [
{
"title": "Row1",
"height": "250px",
"editable": true,
"collapse": false,
"collapsable": true,
"panels": [],
"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": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
],
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"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
}

View File

@@ -0,0 +1,74 @@
/* 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)
*
*/
'use strict';
// Setup some variables
var dashboard, _d_timespan;
// All url parameters are available via the ARGS object
var ARGS;
// Set a default timespan if one isn't specified
_d_timespan = '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 || _d_timespan),
to: "now"
}
};
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: 'graphite',
span: 12,
fill: 1,
linewidth: 2,
targets: [
{
'target': "randomWalk('" + seriesName + "')"
},
{
'target': "randomWalk('random walk2')"
}
],
}
]
});
}
// Now return the object and we're good!
return dashboard;

View File

@@ -32,4 +32,5 @@ function (angular, app, _) {
}
};
});
});

View File

@@ -8,6 +8,8 @@ define([
'./ngModelOnBlur',
'./tip',
'./confirmClick',
'./esVersion',
'./configModal'
'./configModal',
'./spectrumPicker',
'./grafanaGraph',
'./bootstrap-tagsinput'
], function () {});

View File

@@ -0,0 +1,85 @@
define([
'angular',
'jquery',
'bootstrap-tagsinput'
],
function (angular, $) {
'use strict';
angular
.module('kibana.directives')
.directive('bootstrapTagsinput', function() {
function getItemProperty(scope, property) {
if (!property) {
return undefined;
}
if (angular.isFunction(scope.$parent[property])) {
return scope.$parent[property];
}
return function(item) {
return item[property];
};
}
return {
restrict: 'EA',
scope: {
model: '=ngModel'
},
template: '<select multiple></select>',
replace: false,
link: function(scope, element, attrs) {
if (!angular.isArray(scope.model)) {
scope.model = [];
}
var select = $('select', element);
if (attrs.placeholder) {
select.attr('placeholder', attrs.placeholder);
}
select.tagsinput({
typeahead : {
source : angular.isFunction(scope.$parent[attrs.typeaheadSource]) ? scope.$parent[attrs.typeaheadSource] : null
},
itemValue: getItemProperty(scope, attrs.itemvalue),
itemText : getItemProperty(scope, attrs.itemtext),
tagClass : angular.isFunction(scope.$parent[attrs.tagclass]) ?
scope.$parent[attrs.tagclass] : function() { return attrs.tagclass; }
});
select.on('itemAdded', function(event) {
if (scope.model.indexOf(event.item) === -1) {
scope.model.push(event.item);
}
});
select.on('itemRemoved', function(event) {
var idx = scope.model.indexOf(event.item);
if (idx !== -1) {
scope.model.splice(idx, 1);
}
});
scope.$watch("model", function() {
if (!angular.isArray(scope.model)) {
scope.model = [];
}
select.tagsinput('removeAll');
for (var i = 0; i < scope.model.length; i++) {
select.tagsinput('add', scope.model[i]);
}
}, true);
}
};
});
});

View File

@@ -21,7 +21,6 @@ function (angular,_) {
return;
}
// Create a temp scope so we can discard changes to it if needed
var tmpScope = scope.$new();
tmpScope.panel = angular.copy(scope.panel);

View File

@@ -1,24 +0,0 @@
/*
Only show an element if it meets an Elasticsearch version requirement
*/
define([
'angular',
'app',
],
function (angular) {
'use strict';
angular
.module('kibana.directives')
.directive('esVersion', function(esVersion) {
return {
restrict: 'A',
link: function(scope, elem, attr) {
if(!esVersion.is(attr.esVersion)) {
elem.hide();
}
}
};
});
});

View File

@@ -0,0 +1,348 @@
define([
'angular',
'jquery',
'kbn',
'moment',
'underscore'
],
function (angular, $, kbn, moment, _) {
'use strict';
var module = angular.module('kibana.directives');
module.directive('grafanaGraph', function(filterSrv, $rootScope, dashboard) {
return {
restrict: 'A',
template: '<div> </div>',
link: function(scope, elem) {
var data, plot;
var hiddenData = {};
scope.$on('refresh',function() {
if (scope.otherPanelInFullscreenMode()) { return; }
scope.get_data();
});
scope.$on('toggleLegend', function(e, alias) {
if (hiddenData[alias]) {
data.push(hiddenData[alias]);
delete hiddenData[alias];
}
render_panel();
});
// Receive render events
scope.$on('render',function(event, d) {
data = d || data;
render_panel();
});
// Re-render if the window is resized
angular.element(window).bind('resize', function() {
render_panel();
});
function setElementHeight() {
try {
elem.css({ height: scope.height || scope.panel.height || scope.row.height });
return true;
} catch(e) { // IE throws errors sometimes
return false;
}
}
// Function for rendering panel
function render_panel() {
if (!data) { return; }
if (scope.otherPanelInFullscreenMode()) { return; }
if (!setElementHeight()) { return; }
if (_.isString(data)) {
render_panel_as_graphite_png(data);
return;
}
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);
}
});
// Set barwidth based on specified interval
var barwidth = kbn.interval_to_ms(scope.interval);
var stack = panel.stack ? true : null;
// Populate element
var options = {
legend: { show: false },
series: {
stackpercent: panel.stack ? panel.percentage : false,
stack: panel.percentage ? null : stack,
lines: {
show: panel.lines,
zero: false,
fill: panel.fill === 0 ? 0.001 : panel.fill/10,
lineWidth: panel.linewidth,
steps: panel.steppedLine
},
bars: {
show: panel.bars,
fill: 1,
barWidth: barwidth/1.5,
zero: false,
lineWidth: 0
},
points: {
show: panel.points,
fill: 1,
fillColor: false,
radius: panel.pointradius
},
shadowSize: 1
},
yaxes: [],
xaxis: {
timezone: dashboard.current.timezone,
show: panel['x-axis'],
mode: "time",
min: _.isUndefined(scope.range.from) ? null : scope.range.from.getTime(),
max: _.isUndefined(scope.range.to) ? null : scope.range.to.getTime(),
timeformat: time_format(scope.interval),
label: "Datetime",
ticks: elem.width()/100
},
grid: {
backgroundColor: null,
borderWidth: 0,
hoverable: true,
color: '#c8c8c8'
},
selection: {
mode: "x",
color: '#666'
}
};
if (panel.grid.threshold1) {
var limit1 = panel.grid.thresholdLine ? panel.grid.threshold1 : (panel.grid.threshold2 || null);
options.grid.markings = [];
options.grid.markings.push({
yaxis: { from: panel.grid.threshold1, to: limit1 },
color: panel.grid.threshold1Color
});
if (panel.grid.threshold2) {
var limit2;
if (panel.grid.thresholdLine) {
limit2 = panel.grid.threshold2;
} else {
limit2 = panel.grid.threshold1 > panel.grid.threshold2 ? -Infinity : +Infinity;
}
options.grid.markings.push({
yaxis: { from: panel.grid.threshold2, to: limit2 },
color: panel.grid.threshold2Color
});
}
}
addAnnotations(options);
for (var i = 0; i < data.length; i++) {
var _d = data[i].getFlotPairs(panel.nullPointMode);
data[i].data = _d;
}
configureAxisOptions(data, options);
plot = $.plot(elem, data, options);
addAxisLabels();
}
function render_panel_as_graphite_png(url) {
url += '&width=' + elem.width();
url += '&height=' + elem.css('height').replace('px', '');
url += '&bgcolor=1f1f1f'; // @grayDarker & @kibanaPanelBackground
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 ? '' : '&hideLegend=true';
url += scope.panel.grid.min ? '&yMin=' + scope.panel.grid.min : '';
url += scope.panel.grid.max ? '&yMax=' + scope.panel.grid.max : '';
url += scope.panel['x-axis'] ? '' : '&hideAxes=true';
url += scope.panel['y-axis'] ? '' : '&hideYAxis=true';
switch(scope.panel.y_formats[0]) {
case 'bytes':
url += '&yUnitSystem=binary';
break;
case 'short':
url += '&yUnitSystem=si';
break;
case 'none':
url += '&yUnitSystem=none';
break;
}
switch(scope.panel.nullPointMode) {
case 'connected':
url += '&lineMode=connected';
break;
case 'null':
break; // graphite default lineMode
case 'null as zero':
url += "&drawNullAsZero=true";
break;
}
url += scope.panel.steppedLine ? '&lineMode=staircase' : '';
elem.html('<img src="' + url + '"></img>');
}
function addAnnotations(options) {
if(!data.annotations || data.annotations.length === 0) {
return;
}
options.events = {
levels: 1,
data: data.annotations,
types: {
'annotation': {
level: 1,
icon: {
icon: "icon-tag icon-flip-vertical",
size: 20,
color: "#222",
outline: "#bbb"
}
}
}
};
}
function addAxisLabels() {
if (scope.panel.leftYAxisLabel) {
elem.css('margin-left', '10px');
var yaxisLabel = $("<div class='axisLabel yaxisLabel'></div>")
.text(scope.panel.leftYAxisLabel)
.appendTo(elem);
yaxisLabel.css("margin-top", yaxisLabel.width() / 2 - 20);
} else if (elem.css('margin-left')) {
elem.css('margin-left', '');
}
}
function configureAxisOptions(data, options) {
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,
};
options.yaxes.push(defaults);
if (_.findWhere(data, {yaxis: 2})) {
var secondY = _.clone(defaults);
secondY.position = 'right';
options.yaxes.push(secondY);
configureAxisMode(options.yaxes[1], scope.panel.y_formats[1]);
}
configureAxisMode(options.yaxes[0], scope.panel.y_formats[0]);
}
function configureAxisMode(axis, format) {
if (format === 'bytes') {
axis.mode = "byte";
}
if (format === 'short') {
axis.tickFormatter = function(val) {
return kbn.shortFormat(val, 1);
};
}
if (format === 'ms') {
axis.tickFormatter = kbn.msFormat;
}
}
function time_format(interval) {
var _int = kbn.interval_to_seconds(interval);
if(_int >= 2628000) {
return "%Y-%m";
}
if(_int >= 10000) {
return "%m/%d";
}
if(_int >= 3600) {
return "%m/%d %H:%M";
}
if(_int >= 700) {
return "%a %H:%M";
}
return "%H:%M";
}
var $tooltip = $('<div>');
elem.bind("plothover", function (event, pos, item) {
var group, value, timestamp;
if (item) {
if (item.series.info.alias || scope.panel.tooltip.query_as_alias) {
group = '<small style="font-size:0.9em;">' +
'<i class="icon-circle" style="color:'+item.series.color+';"></i>' + ' ' +
(item.series.info.alias || item.series.info.query)+
'</small><br>';
} else {
group = kbn.query_color_dot(item.series.color, 15) + ' ';
}
value = (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') ?
item.datapoint[1] - item.datapoint[2] :
item.datapoint[1];
if(item.series.info.y_format === 'bytes') {
value = kbn.byteFormat(value, 2);
}
if(item.series.info.y_format === 'short') {
value = kbn.shortFormat(value, 2);
}
if(item.series.info.y_format === 'ms') {
value = kbn.msFormat(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();
}
});
elem.bind("plotselected", function (event, ranges) {
filterSrv.setTime({
from : moment.utc(ranges.xaxis.from).toDate(),
to : moment.utc(ranges.xaxis.to).toDate(),
});
});
}
};
});
});

View File

@@ -1,45 +1,57 @@
define([
'angular'
'angular',
'jquery'
],
function (angular) {
function (angular, $) {
'use strict';
angular
.module('kibana.directives')
.directive('kibanaPanel', function($compile) {
var container = '<div class="panelCont"></div>';
var editorTemplate =
var container = '<div class="panel-container"></div>';
var content = '<div class="panel-content"></div>';
'<div class="row-fluid panel-extra"><div class="panel-extra-container">' +
var panelHeader =
'<div class="panel-header">'+
'<div class="row-fluid">' +
'<div class="span12 alert-error panel-error" ng-hide="!panel.error">' +
'<a class="close" ng-click="panel.error=false">&times;</a>' +
'<i class="icon-exclamation-sign"></i> <strong>Oops!</strong> {{panel.error}}' +
'</div>' +
'</div>\n' +
'<span class="row-button extra" ng-show="panelMeta.loading == true">' +
'<span>'+
'<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>' +
'<span ng-if="panelMeta.menuItems" class="dropdown" ng-show="panel.title">' +
'<span class="panel-text panel-title pointer" bs-dropdown="panelMeta.menuItems" tabindex="1" ' +
'data-drag=true data-jqyoui-options="{revert: \'invalid\',helper:\'clone\'}"'+
' jqyoui-draggable="'+
'{'+
'animate:false,'+
'mutate:false,'+
'index:{{$index}},'+
'onStart:\'panelMoveStart\','+
'onStop:\'panelMoveStop\''+
'}" ng-model="row.panels" ' +
'>' +
'{{panel.title}}' +
'</span>' +
'</span>'+
'<span ng-if="!panelMeta.menuItems" class="panel-text panel-title" ng-show="panel.title">' +
'{{panel.title}}' +
'</span>'+
'<span ng-if="panelMeta.menuItems" class="dropdown">' +
'<span class="panel-text panel-title pointer" bs-dropdown="panelMeta.menuItems" 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>'+
'<span ng-if="!panelMeta.menuItems" config-modal class="panel-text panel-title pointer" ng-show="panel.title">' +
'{{panel.title}}' +
'</span>'+
'</div>'+
'</div>\n'+
'</div>';
'</div></div>';
return {
restrict: 'E',
link: function($scope, elem, attr) {
@@ -47,6 +59,14 @@ function (angular) {
// 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);
@@ -77,7 +97,9 @@ function (angular) {
$controllers = $controllers.add($module.find('ngcontroller, [ng-controller], .ng-controller'));
if ($controllers.length) {
$controllers.first().prepend(editorTemplate);
$controllers.first().prepend(panelHeader);
$controllers.first().find('.panel-header').nextAll().wrapAll(content);
$scope.require([
'panels/'+nameAsPath+'/module'
], function() {

View File

@@ -0,0 +1,38 @@
define([
'angular',
'spectrum'
],
function (angular) {
'use strict';
angular
.module('kibana.directives')
.directive('spectrumPicker', function() {
return {
restrict: 'E',
require: 'ngModel',
scope: false,
replace: true,
template: "<span><input class='input-small' /></span>",
link: function(scope, element, attrs, ngModel) {
var input = element.find('input');
var options = angular.extend({
showAlpha: true,
showButtons: false,
color: ngModel.$viewValue,
change: function(color) {
scope.$apply(function() {
ngModel.$setViewValue(color.toRgbString());
});
}
}, scope.$eval(attrs.options));
ngModel.$render = function() {
input.spectrum('set', ngModel.$viewValue || '');
};
input.spectrum(options);
}
};
});
});

View File

@@ -0,0 +1,13 @@
<div ng-controller='AnnotationsCtrl' ng-init="init()">
<!-- <div class="submenu-toggle" ng-class="{'annotation-disabled': panel.hideAll }">
<a ng-click="hideAll();" class="small">Hide All</a>
<i class="icon-ok"></i>
</div>
-->
<div class="submenu-toggle" ng-repeat="annotation in annotationList" ng-class="{'annotation-disabled': !annotation.enabled }">
<a ng-click="hide(annotation)" class="small">{{annotation.name}}</a>
<i class="icon-ok"></i>
</div>
</div>

View File

@@ -0,0 +1,51 @@
/*
## 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, annotationsSrv, $rootScope) {
$scope.panelMeta = {
status : "Stable",
description : "Annotations"
};
// Set and populate defaults
var _d = {
};
_.defaults($scope.panel,_d);
$scope.init = function() {
$scope.annotationList = annotationsSrv.annotationList;
};
$scope.hideAll = function () {
$scope.panel.hideAll = !$scope.panel.hideAll;
_.each($scope.annotationList, function(annotation) {
annotation.enabled = !$scope.panel.hideAll;
});
};
$scope.hide = function (annotation) {
annotation.enabled = !annotation.enabled;
$scope.panel.hideAll = !annotation.enabled;
$rootScope.$broadcast('refresh');
};
});
});

View File

@@ -5,7 +5,6 @@
}
.filtering-container label {
float: left;
font-size: inherit;
}
.filtering-container input[type=checkbox] {
margin: 0;
@@ -13,11 +12,14 @@
.filter-panel-filter {
display:inline-block;
vertical-align: top;
padding: 3px 10px 0px 10px;
padding: 4px 10px 3px 10px;
border-right: 1px solid #202020;
}
.filter-panel-filter:first-child {
padding-left: 0;
}
.filter-panel-filter ul {
margin-bottom: 3px;
margin-bottom: 0px;
}
.filter-deselected {
@@ -25,12 +27,13 @@
}
.filter-action {
float:right;
padding-right: 2px;
margin-bottom: 0px !important;
margin-left: 3px;
margin-left: 0px;
margin-top: 4px;
}
.add-filter-action {
padding: 3px 10px 0px 10px;
padding: 3px 10px 0px 5px;
position: relative;
top: 4px;
}
@@ -78,7 +81,7 @@
<input type='text' ng-model="filter.query">
</li>
<li>
<label for="includeAll">Include all</label>:
<label for="includeAll">Include all:</label>
<input id="includeAll" type='checkbox' ng-model="filter.includeAll">
</li>
</ul>

View File

@@ -41,8 +41,19 @@ function (angular, app, _) {
filter.options = _.map(results, function(node) {
return { text: node.text, value: node.text };
});
if (filter.includeAll) {
filter.options.unshift({text: 'All', value: '*'});
if(endsWithWildcard(filter.query)) {
filter.options.unshift({text: 'All', value: '*'});
}
else {
var allExpr = '{';
_.each(filter.options, function(option) {
allExpr += option.text + ',';
});
allExpr = allExpr.substring(0, allExpr.length - 1) + '}';
filter.options.unshift({text: 'All', value: allExpr});
}
}
filterSrv.filterOptionSelected(filter, filter.options[0]);
@@ -66,5 +77,13 @@ function (angular, app, _) {
$rootScope.$broadcast('render');
};
function endsWithWildcard(query) {
if (query.length === 0) {
return false;
}
return query[query.length - 1] === '*';
}
});
});

View File

@@ -1,14 +1,105 @@
<div class="editor-row">
<div class="editor-row">
<div class="section">
<h5>Axes</h5>
<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-large" ng-model="panel.leftYAxisLabel">
<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">Right y-axis label</label>
<input ng-change="get_data()" ng-model-onblur placeholder="" type="text" class="input-large" ng-model="panel.rightYAxisLabel">
<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', 'ms']" 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', 'ms']" 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>
</div>
</div>
<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>
</div>
<div class="section">
<h5>Grid thresholds</h5>
<div class="editor-option">
<label class="small">Level1</label>
<input type="number" class="input-small" ng-model="panel.grid.threshold1" ng-change="render()" ng-model-onblur />
</div>
<div class="editor-option">
<label class="small">Color</label>
<spectrum-picker ng-model="panel.grid.threshold1Color" ng-change="render()" ></spectrum-picker>
</div>
<div class="editor-option">
<label class="small">Level2</label>
<input type="number" class="input-small" ng-model="panel.grid.threshold2" ng-change="render()" ng-model-onblur />
</div>
<div class="editor-option">
<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>
</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>
</div>
</div>

View File

@@ -9,9 +9,9 @@
<div class="grafana-target-inner-wrapper">
<div class="grafana-target-inner">
<ul class="grafana-target-controls">
<li ng-if="target.yaxis">
<a class="pointer" ng-click="setYAxis()">
y&sup2;
<li ng-show="parserError">
<a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
<i class="icon-warning-sign"></i>
</a>
</li>
<li>
@@ -42,24 +42,20 @@
</ul>
<ul class="grafana-target-controls-left">
<li ng-hide="parserError">
<li>
<a class="grafana-target-segment"
ng-click="target.hide = !target.hide; get_data();"
role="menuitem">
<i class="icon-eye-open"></i>
</a>
</li>
<li ng-show="parserError">
<a class="grafana-target-segment" bs-tooltip="parserError" style="color: rgb(229, 189, 28)" ng-click="hideit()" role="menuitem">
<i class="icon-warning-sign"></i>
</a>
</li>
</ul>
<input type="text"
class="grafana-target-text-input"
ng-model="target.target"
focus-me="showTextEditor"
spellcheck='false'
ng-model-onblur ng-change="targetTextChanged()"
ng-show="showTextEditor" />
@@ -72,7 +68,7 @@
focus-me="segment.focus"
ng-bind-html-unsafe="segment.html">
</a>
<ul class="dropdown-menu" role="menu">
<ul class="dropdown-menu scrollable" role="menu">
<li ng-repeat="altSegment in altSegments" role="menuitem">
<a href="javascript:void(0)" tabindex="1" ng-click="setSegment($index, $parent.$index)" ng-bind-html-unsafe="altSegment.html"></a>
</li>

View File

@@ -20,7 +20,7 @@
<div ng-switch-when="int">
<input
type="number"
placeholder="seconds"
step="any"
focus-me="true"
class="input-mini"
ng-change="functionParamsChanged(func)" ng-model-onblur

View File

@@ -1,15 +1,33 @@
<span ng-show="panel.legend"
<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}"
ng-click="toggleSeries(series)">
bs-popover="'colorPopup.html'"
>
</i>
<span class='small histogram-legend-item'>
<a bs-popover="'colorPopup.html'" data-unique="1" data-placement="{{series.yaxis === 2 ? 'bottomRight' : 'bottomLeft'}}">
<a ng-click="toggleSeries(series)" data-unique="1" data-placement="{{series.yaxis === 2 ? 'bottomRight' : 'bottomLeft'}}">
{{series.alias}}
</a>
<span ng-if="panel.legend.values">
<span ng-show="panel.legend.current">
&nbsp;&nbsp;Current: {{series.current}}&nbsp;
</span>
<span ng-show="panel.legend.min">
&nbsp;&nbsp;Min: {{series.min}}&nbsp;
</span>
<span ng-show="panel.legend.max">
&nbsp;&nbsp;Max: {{series.max}}&nbsp;
</span>
<span ng-show="panel.legend.total">
&nbsp;&nbsp;Total: {{series.total}}&nbsp;
</span>
<span ng-show="panel.legend.avg">
&nbsp;&nbsp;Avg: {{series.avg}}&nbsp;
</span>
</span>
</span>
</span>

View File

@@ -12,7 +12,7 @@
<center><img ng-show='panel.loading && _.isUndefined(data)' src="img/load_big.gif"></center>
<div histogram-chart class="pointer histogram-chart" params="{{panel}}">
<div grafana-graph class="pointer histogram-chart">
</div>
<div ng-if="panel.legend" class="grafana-legend-container">
@@ -24,9 +24,11 @@
<div ng-repeat="tab in editorTabs" data-title="{{tab}}">
</div>
</div>
<div class="tab-content" ng-show="editorTabs[editor.index] == 'General'">
<div ng-include src="'app/partials/panelgeneral.html'"></div>
</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>

View File

@@ -34,7 +34,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
var module = angular.module('kibana.panels.graphite', []);
app.useModule(module);
module.controller('graphite', function($scope, $rootScope, filterSrv, graphiteSrv, $timeout) {
module.controller('graphite', function($scope, $rootScope, filterSrv, graphiteSrv, $timeout, annotationsSrv) {
$scope.panelMeta = {
modals : [],
@@ -46,19 +46,19 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
src:'app/panels/graphite/editor.html'
},
{
title:'Axis labels',
title:'Axes & Grid',
src:'app/panels/graphite/axisEditor.html'
},
{
title:'Style',
title:'Display Styles',
src:'app/panels/graphite/styleEditor.html'
}
],
menuItems: [
{ text: 'View fullscreen', click: 'toggleFullscreen()' },
{ text: 'Edit', click: 'openConfigureModal()' },
{ text: 'Duplicate', click: 'duplicate()' },
{ text: 'Edit', click: 'openConfigureModal()' },
{ text: 'Fullscreen', click: 'toggleFullscreen()' },
{ text: 'Duplicate', click: 'duplicate()' },
{ text: 'Span', submenu: [
{ text: '1', click: 'updateColumnSpan(1)' },
{ text: '2', click: 'updateColumnSpan(2)' },
@@ -82,6 +82,10 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
// Set and populate defaults
var _d = {
/** @scratch /panels/histogram/3
* renderer:: sets client side (flot) or native graphite png renderer (png)
*/
renderer: 'flot',
/** @scratch /panels/histogram/3
* x-axis:: Show the x-axis
*/
@@ -95,53 +99,32 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
*/
scale : 1,
/** @scratch /panels/histogram/3
* y_format:: 'none','bytes','short '
* y_formats :: 'none','bytes','short', 'ms'
*/
y_format : 'none',
y2_format : 'none',
y_formats : ['short', 'short'],
/** @scratch /panels/histogram/5
* grid object:: Min and max y-axis values
* grid.min::: Minimum y-axis value
* grid.max::: Maximum y-axis value
* grid.ma1::: Maximum y-axis value
*/
grid : {
max: null,
min: 0
min: 0,
threshold1: null,
threshold2: null,
threshold1Color: 'rgba(216, 200, 27, 0.27)',
threshold2Color: 'rgba(234, 112, 112, 0.22)'
},
/** @scratch /panels/histogram/3
* ==== Annotations
* annotate object:: A query can be specified, the results of which will be displayed as markers on
* the chart. For example, for noting code deploys.
* annotate.enable::: Should annotations, aka markers, be shown?
* annotate.query::: Lucene query_string syntax query to use for markers.
* annotate.size::: Max number of markers to show
* annotate.field::: Field from documents to show
* annotate.sort::: Sort array in format [field,order], For example [`@timestamp',`desc']
*/
annotate : {
enable : false,
query : "*",
size : 20,
field : '_type',
sort : ['_score','desc']
},
/** @scratch /panels/histogram/3
* ==== Interval options
* auto_int:: Automatically scale intervals?
*/
auto_int : true,
/** @scratch /panels/histogram/3
* resolution:: If auto_int is true, shoot for this many bars.
*/
resolution : 100,
/** @scratch /panels/histogram/3
* interval:: If auto_int is set to false, use this as the interval.
*/
interval : '5m',
/** @scratch /panels/histogram/3
* interval:: Array of possible intervals in the *View* selector. Example [`auto',`1s',`5m',`3h']
*/
intervals : ['auto','1s','1m','5m','10m','30m','1h','3h','12h','1d','1w','1y'],
/** @scratch /panels/histogram/3
* ==== Drawing options
* lines:: Show line chart
@@ -171,35 +154,20 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
* stack:: Stack multiple series
*/
stack : true,
/** @scratch /panels/histogram/3
* spyable:: Show inspect icon
*/
spyable : true,
/** @scratch /panels/histogram/3
* zoomlinks:: Show `Zoom Out' link
*/
zoomlinks : false,
/** @scratch /panels/histogram/3
* options:: Show quick view options section
*/
options : false,
/** @scratch /panels/histogram/3
* legend:: Display the legond
*/
legend : true,
/** @scratch /panels/histogram/3
* interactive:: Enable click-and-drag to zoom functionality
*/
interactive : true,
/** @scratch /panels/histogram/3
* legend_counts:: Show counts in legend
*/
legend_counts : true,
legend: {
show: true, // disable/enable legend
values: false, // disable/enable legend values
min: false,
max: false,
current: false,
total: false,
avg: false
},
/** @scratch /panels/histogram/3
* ==== Transformations
* timezone:: Correct for browser timezone?. Valid values: browser, utc
*/
timezone : 'browser', // browser or utc
/** @scratch /panels/histogram/3
* percentage:: Show the y-axis as a percentage of the axis total. Only makes sense for multiple
* queries
@@ -211,6 +179,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
zerofill : true,
nullPointMode : 'connected',
steppedLine: false,
tooltip : {
@@ -225,13 +194,26 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
};
_.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.tooltip, _d.tooltip);
_.defaults($scope.panel.annotate, _d.annotate);
_.defaults($scope.panel.grid, _d.grid);
// 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() {
// Hide view options by default
$scope.fullscreen = false;
$scope.options = false;
@@ -243,20 +225,6 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
$scope.panel.tooltip.query_as_alias = true;
$scope.get_data();
};
$scope.set_interval = function(interval) {
if(interval !== 'auto') {
$scope.panel.auto_int = false;
$scope.panel.interval = interval;
} else {
$scope.panel.auto_int = true;
}
};
$scope.typeAheadSource = function () {
return ["test", "asd", "testing2"];
};
$scope.remove_panel_from_row = function(row, panel) {
@@ -273,24 +241,17 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
$scope.get_data();
};
$scope.interval_label = function(interval) {
return $scope.panel.auto_int && interval === $scope.panel.interval ? interval+" (auto)" : interval;
};
$scope.updateTimeRange = function () {
var range = filterSrv.timeRange();
var interval = filterSrv.timeRange();
$scope.range = filterSrv.timeRange();
$scope.rangeUnparsed = filterSrv.timeRange(false);
if ($scope.panel.auto_int) {
if (range) {
interval = kbn.secondsToHms(
kbn.calculate_interval(range.from, range.to, $scope.panel.resolution, 0) / 1000
);
}
$scope.interval = '10m';
if ($scope.range) {
$scope.interval = kbn.secondsToHms(
kbn.calculate_interval($scope.range.from, $scope.range.to, $scope.panel.resolution, 0) / 1000
);
}
$scope.interval = $scope.panel.interval = interval || '10m';
$scope.range = range;
};
$scope.colors = [
@@ -320,11 +281,14 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
$scope.updateTimeRange();
var graphiteQuery = {
range: $scope.range,
range: $scope.rangeUnparsed,
targets: $scope.panel.targets,
format: $scope.panel.renderer === 'png' ? 'png' : 'json',
maxDataPoints: $scope.panel.span * 50
};
$scope.annotationsPromise = annotationsSrv.getAnnotations($scope.rangeUnparsed);
return graphiteSrv.query(graphiteQuery)
.then($scope.receiveGraphiteData)
.then(null, function(err) {
@@ -334,9 +298,15 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
$scope.receiveGraphiteData = function(results) {
$scope.panelMeta.loading = false;
$scope.legend = [];
// png renderer returns just a url
if (_.isString(results)) {
$scope.render(results);
return;
}
results = results.data;
$scope.legend = [];
var data = [];
_.each(results, function(targetData) {
@@ -344,37 +314,30 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
var color = $scope.panel.aliasColors[alias] || $scope.colors[data.length];
var yaxis = $scope.panel.aliasYAxis[alias] || 1;
var tsOpts = {
interval: $scope.interval,
start_date: $scope.range && $scope.range.from,
end_date: $scope.range && $scope.range.to,
};
var time_series = new timeSeries.ZeroFilled(tsOpts);
_.each(targetData.datapoints, function(valueArray) {
if (valueArray[0]) {
time_series.addValue(valueArray[1] * 1000, valueArray[0]);
}
});
var seriesInfo = {
alias: alias,
color: color,
enable: true,
yaxis: yaxis,
y_format: $scope.panel.y_formats[yaxis - 1]
};
$scope.legend.push(seriesInfo);
data.push({
var series = new timeSeries.ZeroFilled({
datapoints: targetData.datapoints,
info: seriesInfo,
time_series: time_series,
});
$scope.legend.push(seriesInfo);
data.push(series);
});
$scope.render(data);
$scope.annotationsPromise
.then(function(annotations) {
data.annotations = annotations;
$scope.render(data);
}, function() {
$scope.render(data);
});
};
$scope.add_target = function() {
@@ -425,6 +388,10 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
$scope.enterFullscreenMode({edit: true});
};
$scope.otherPanelInFullscreenMode = function() {
return $rootScope.fullscreen && !$scope.fullscreen;
};
$scope.render = function(data) {
$scope.$emit('render', data);
};
@@ -438,7 +405,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
$scope.duplicate = function(addToRow) {
addToRow = addToRow || $scope.row;
var currentRowSpan = $scope.rowSpan(addToRow);
if (currentRowSpan <= 8) {
if (currentRowSpan <= 9) {
addToRow.panels.push(angular.copy($scope.panel));
}
else {
@@ -482,6 +449,11 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
$scope.render();
};
$scope.toggleGridMinMax = function(key) {
$scope.panel.grid[key] = _.toggle($scope.panel.grid[key], null, 0);
$scope.render();
};
$scope.updateColumnSpan = function(span) {
$scope.panel.span = span;
$timeout($scope.render);
@@ -489,268 +461,6 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
});
module.directive('histogramChart', function(filterSrv, $rootScope) {
return {
restrict: 'A',
template: '<div> </div>',
link: function(scope, elem) {
var data, plot;
var hiddenData = {};
scope.$on('refresh',function() {
if ($rootScope.fullscreen && !scope.fullscreen) {
return;
}
scope.get_data();
});
scope.$on('toggleLegend', function(e, alias) {
if (hiddenData[alias]) {
data.push(hiddenData[alias]);
delete hiddenData[alias];
}
render_panel();
});
// Receive render events
scope.$on('render',function(event, d) {
data = d || data;
render_panel();
});
// Re-render if the window is resized
angular.element(window).bind('resize', function() {
render_panel();
});
// Function for rendering panel
function render_panel() {
if (!data) {
return;
}
// IE doesn't work without this
elem.css({height:scope.height || scope.row.height});
_.each(data, function(series) {
series.label = series.info.alias;
series.color = series.info.color;
});
_.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);
}
});
// Set barwidth based on specified interval
var barwidth = kbn.interval_to_ms(scope.panel.interval);
var stack = scope.panel.stack ? true : null;
// Populate element
var options = {
legend: { show: false },
series: {
stackpercent: scope.panel.stack ? scope.panel.percentage : false,
stack: scope.panel.percentage ? null : stack,
lines: {
show: scope.panel.lines,
// Silly, but fixes bug in stacked percentages
fill: scope.panel.fill === 0 ? 0.001 : scope.panel.fill/10,
lineWidth: scope.panel.linewidth,
steps: scope.panel.steppedLine
},
bars: {
show: scope.panel.bars,
fill: 1,
barWidth: barwidth/1.5,
zero: false,
lineWidth: 0
},
points: {
show: scope.panel.points,
fill: 1,
fillColor: false,
radius: scope.panel.pointradius
},
shadowSize: 1
},
yaxes: [],
xaxis: {
timezone: scope.panel.timezone,
show: scope.panel['x-axis'],
mode: "time",
min: _.isUndefined(scope.range.from) ? null : scope.range.from.getTime(),
max: _.isUndefined(scope.range.to) ? null : scope.range.to.getTime(),
timeformat: time_format(scope.panel.interval),
label: "Datetime",
ticks: elem.width()/100
},
grid: {
backgroundColor: null,
borderWidth: 0,
hoverable: true,
color: '#c8c8c8'
}
};
if(scope.panel.annotate.enable) {
options.events = {
levels: 1,
data: scope.annotations,
types: {
'annotation': {
level: 1,
icon: {
icon: "icon-tag icon-flip-vertical",
size: 20,
color: "#222",
outline: "#bbb"
}
}
}
//xaxis: int // the x axis to attach events to
};
}
if(scope.panel.interactive) {
options.selection = { mode: "x", color: '#666' };
}
// when rendering stacked bars, we need to ensure each point that has data is zero-filled
// so that the stacking happens in the proper order
var required_times = [];
if (data.length > 1) {
required_times = Array.prototype.concat.apply([], _.map(data, function (query) {
return query.time_series.getOrderedTimes();
}));
required_times = _.uniq(required_times.sort(function (a, b) {
// decending numeric sort
return a-b;
}), true);
}
for (var i = 0; i < data.length; i++) {
var _d = data[i].time_series.getFlotPairs(required_times, scope.panel.nullPointMode);
data[i].yaxis = data[i].info.yaxis;
data[i].data = _d;
data[i].info.y_format = data[i].yaxis === 1 ? scope.panel.y_format : scope.panel.y2_format;
}
configureAxisOptions(data, options);
plot = $.plot(elem, data, options);
if (scope.panel.leftYAxisLabel) {
elem.css('margin-left', '10px');
var yaxisLabel = $("<div class='axisLabel yaxisLabel'></div>")
.text(scope.panel.leftYAxisLabel)
.appendTo(elem);
yaxisLabel.css("margin-top", yaxisLabel.width() / 2 - 20);
} else if (elem.css('margin-left')) {
elem.css('margin-left', '');
}
}
function configureAxisOptions(data, options)
{
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
};
options.yaxes.push(defaults);
if (_.findWhere(data, {yaxis: 2})) {
var secondY = _.clone(defaults);
secondY.position = 'right';
options.yaxes.push(secondY);
configureAxisMode(options.yaxes[1], scope.panel.y2_format);
}
configureAxisMode(options.yaxes[0], scope.panel.y_format);
}
function configureAxisMode(axis, format) {
if (format === 'bytes') {
axis.mode = "byte";
}
if (format === 'short') {
axis.tickFormatter = function(val) {
return kbn.shortFormat(val,0);
};
}
}
function time_format(interval) {
var _int = kbn.interval_to_seconds(interval);
if(_int >= 2628000) {
return "%Y-%m";
}
if(_int >= 10000) {
return "%Y-%m-%d";
}
if(_int >= 60) {
return "%H:%M<br>%m-%d";
}
return "%H:%M:%S";
}
var $tooltip = $('<div>');
elem.bind("plothover", function (event, pos, item) {
var group, value, timestamp;
if (item) {
if (item.series.info.alias || scope.panel.tooltip.query_as_alias) {
group = '<small style="font-size:0.9em;">' +
'<i class="icon-circle" style="color:'+item.series.color+';"></i>' + ' ' +
(item.series.info.alias || item.series.info.query)+
'</small><br>';
} else {
group = kbn.query_color_dot(item.series.color, 15) + ' ';
}
value = (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') ?
item.datapoint[1] - item.datapoint[2] :
item.datapoint[1];
if(item.series.info.y_format === 'bytes') {
value = kbn.byteFormat(value,2);
}
if(item.series.info.y_format === 'short') {
value = kbn.shortFormat(value,2);
}
timestamp = scope.panel.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();
}
});
elem.bind("plotselected", function (event, ranges) {
filterSrv.setTime({
from : moment.utc(ranges.xaxis.from).toDate(),
to : moment.utc(ranges.xaxis.to).toDate(),
});
});
}
};
});
});

View File

@@ -1,3 +1,5 @@
<div class="editor-row">
<div class="section">
<h5>Chart Options</h5>
@@ -10,31 +12,8 @@
<div class="editor-option">
<label class="small">Points</label><input type="checkbox" ng-model="panel.points" ng-checked="panel.points" ng-change="render()">
</div>
<div class="editor-option">
<label class="small">Selectable</label><input type="checkbox" ng-model="panel.interactive" ng-checked="panel.interactive">
</div>
</div>
<div class="section">
<h5>Axis</h5>
<div class="editor-option">
<label class="small">xAxis</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">yAxis</label><input type="checkbox" ng-model="panel['y-axis']" ng-checked="panel['y-axis']" ng-change="render()">
</div>
<div class="editor-option" ng-show="panel.points">
<label class="small">Point Radius</label>
<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">Left Y Format <tip>Y-axis formatting</tip></label>
<select class="input-small" ng-model="panel.y_format" ng-options="f for f in ['none','short','bytes']" 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.y2_format" ng-options="f for f in ['none','short','bytes']" ng-change="render()"></select>
</div>
</div>
<div class="section">
<h5>Line options</h5>
<div class="editor-option" ng-show="panel.lines">
@@ -45,12 +24,16 @@
<label class="small">Line Width</label>
<select class="input-mini" ng-model="panel.linewidth" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]" ng-change="render()"></select>
</div>
<div class="editor-option" ng-show="panel.points">
<label class="small">Point Radius</label>
<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>
<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">Stepped lines</label><input type="checkbox" ng-model="panel.steppedLine" ng-checked="panel.steppedLine" ng-change="render()">
<label class="small">Staircase line</label><input type="checkbox" ng-model="panel.steppedLine" ng-checked="panel.steppedLine" ng-change="render()">
</div>
</div>
<div class="section">
@@ -67,32 +50,17 @@
<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>Legend<h5>
<div class="editor-option">
<label class="small">Legend</label><input type="checkbox" ng-model="panel.legend" ng-checked="panel.legend">
</div>
<div ng-show="panel.legend" class="editor-option">
<label class="small">Query <tip>If no alias is set, show the query in the legend</tip></label><input type="checkbox" ng-model="panel.show_query" ng-checked="panel.show_query">
</div>
<div ng-show="panel.legend" class="editor-option">
<label class="small">Counts</label><input type="checkbox" ng-model="panel.legend_counts" ng-checked="panel.legend_counts">
</div>
</div>
<div class="section">
<h5>Grid<h5>
<h5>Rendering</h5>
<div class="editor-option">
<label class="small">Min / <a href='' ng-click="panel.grid.min = _.toggle(panel.grid.min,null,0)">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"/>
<label class="small">Flot <tip>client side</tip></label>
<input type="radio" class="input-small" ng-model="panel.renderer" value="flot" ng-change="get_data()" />
</div>
<div class="editor-option">
<label class="small">Max / <a ref='' ng-click="panel.grid.max = _.toggle(panel.grid.max,null,0)">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"/>
<label class="small">Graphite PNG <tip>server side</tip></label>
<input type="radio" class="input-small" ng-model="panel.renderer" value="png" ng-change="get_data()" />
</div>
</div>
</div>

View File

@@ -1,229 +1,62 @@
define([
'underscore',
'./interval'
'underscore'
],
function (_, Interval) {
function (_) {
'use strict';
var ts = {};
// map compatable parseInt
function base10Int(val) {
return parseInt(val, 10);
}
// trim the ms off of a time, but return it with empty ms.
function getDatesTime(date) {
return Math.floor(date.getTime() / 1000)*1000;
}
/**
* Certain graphs require 0 entries to be specified for them to render
* properly (like the line graph). So with this we will caluclate all of
* the expected time measurements, and fill the missing ones in with 0
* @param {object} opts An object specifying some/all of the options
*
* OPTIONS:
* @opt {string} interval The interval notion describing the expected spacing between
* each data point.
* @opt {date} start_date (optional) The start point for the time series, setting this and the
* end_date will ensure that the series streches to resemble the entire
* expected result
* @opt {date} end_date (optional) The end point for the time series, see start_date
*/
ts.ZeroFilled = function (opts) {
opts = _.defaults(opts, {
interval: '10m',
start_date: null,
end_date: null,
});
// the expected differenece between readings.
this.interval = new Interval(opts.interval);
// will keep all values here, keyed by their time
this._data = {};
this.start_time = opts.start_date && getDatesTime(opts.start_date);
this.end_time = opts.end_date && getDatesTime(opts.end_date);
this.opts = opts;
this.datapoints = opts.datapoints;
this.info = opts.info;
this.label = opts.info.alias;
};
/**
* Add a row
* @param {int} time The time for the value, in
* @param {any} value The value at this time
*/
ts.ZeroFilled.prototype.addValue = function (time, value) {
if (time instanceof Date) {
time = getDatesTime(time);
} else {
time = base10Int(time);
}
if (!isNaN(time)) {
this._data[time] = (_.isUndefined(value) ? 0 : value);
}
this._cached_times = null;
};
ts.ZeroFilled.prototype.getFlotPairs = function (fillStyle) {
var result = [];
/**
* Get an array of the times that have been explicitly set in the series
* @param {array} include (optional) list of timestamps to include in the response
* @return {array} An array of integer times.
*/
ts.ZeroFilled.prototype.getOrderedTimes = function (include) {
var times = _.map(_.keys(this._data), base10Int);
if (_.isArray(include)) {
times = times.concat(include);
}
return _.uniq(times.sort(function (a, b) {
// decending numeric sort
return a - b;
}), true);
};
this.color = this.info.color;
this.yaxis = this.info.yaxis;
/**
* return the rows in the format:
* [ [time, value], [time, value], ... ]
*
* Heavy lifting is done by _get(Min|Default|All)FlotPairs()
* @param {array} required_times An array of timestamps that must be in the resulting pairs
* @return {array}
*/
ts.ZeroFilled.prototype.getFlotPairs = function (required_times, fillStyle) {
var times = this.getOrderedTimes(required_times),
strategy,
pairs;
this.info.total = 0;
this.info.max = null;
this.info.min = 212312321312;
if(fillStyle === 'null as zero') {
strategy = this._getAllFlotPairs;
} else if(fillStyle === 'null') {
strategy = this._getNullFlotPairs;
} else if(fillStyle === 'connected') {
strategy = this._getNoZeroFlotPairs;
} else {
strategy = this._getMinFlotPairs;
}
pairs = _.reduce(
times, // what
strategy, // how
[], // where
this // context
);
// if the first or last pair is inside either the start or end time,
// add those times to the series with null values so the graph will stretch to contain them.
// Removing, flot 0.8.1's max/min params satisfy this
/*
if (this.start_time && (pairs.length === 0 || pairs[0][0] > this.start_time)) {
pairs.unshift([this.start_time, null]);
}
if (this.end_time && (pairs.length === 0 || pairs[pairs.length - 1][0] < this.end_time)) {
pairs.push([this.end_time, null]);
}
*/
return pairs;
};
/**
* ** called as a reduce stragegy in getFlotPairs() **
* Fill zero's on either side of the current time, unless there is already a measurement there or
* we are looking at an edge.
* @return {array} An array of points to plot with flot
*/
ts.ZeroFilled.prototype._getMinFlotPairs = function (result, time, i, times) {
var next, expected_next, prev, expected_prev;
// check for previous measurement
if (i > 0) {
prev = times[i - 1];
expected_prev = this.interval.before(time);
if (prev < expected_prev) {
result.push([expected_prev, 0]);
_.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;
}
}
}
// add the current time
result.push([ time, this._data[time] || 0]);
// check for next measurement
if (times.length > i) {
next = times[i + 1];
expected_next = this.interval.after(time);
if (next > expected_next) {
result.push([expected_next, 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) {
this.info.avg = (this.info.total / result.length).toFixed(2);
this.info.current = result[result.length-1][1];
}
return result;
};
/**
* ** called as a reduce stragegy in getFlotPairs() **
* Fill zero's to the right of each time, until the next measurement is reached or we are at the
* last measurement
* @return {array} An array of points to plot with flot
*/
ts.ZeroFilled.prototype._getAllFlotPairs = function (result, time, i, times) {
var next, expected_next;
result.push([ times[i], this._data[times[i]] || 0 ]);
next = times[i + 1];
expected_next = this.interval.after(time);
for(; times.length > i && next > expected_next; expected_next = this.interval.after(expected_next)) {
result.push([expected_next, 0]);
}
return result;
};
/**
* ** called as a reduce stragegy in getFlotPairs() **
* Same as min, but fills with nulls
* @return {array} An array of points to plot with flot
*/
ts.ZeroFilled.prototype._getNullFlotPairs = function (result, time, i, times) {
var next, expected_next, prev, expected_prev;
// check for previous measurement
if (i > 0) {
prev = times[i - 1];
expected_prev = this.interval.before(time);
if (prev < expected_prev) {
result.push([expected_prev, null]);
}
}
// add the current time
result.push([ time, this._data[time] || null]);
// check for next measurement
if (times.length > i) {
next = times[i + 1];
expected_next = this.interval.after(time);
if (next > expected_next) {
result.push([expected_next, null]);
}
}
return result;
};
/**
* ** called as a reduce stragegy in getFlotPairs() **
* Not fill zero's on either side of the current time, only the current time
* @return {array} An array of points to plot with flot
*/
ts.ZeroFilled.prototype._getNoZeroFlotPairs = function (result, time) {
// add the current time
if(this._data[time]){
result.push([ time, this._data[time]]);
}
return result;
};
return ts;
});

View File

@@ -77,8 +77,8 @@ function (angular, app, _, moment, kbn) {
$scope.temptime = cloneTime($scope.time);
// Date picker needs the date to be at the start of the day
$scope.temptime.from.date.setHours(0,0,0,0);
$scope.temptime.to.date.setHours(0,0,0,0);
$scope.temptime.from.date.setHours(1,0,0,0);
$scope.temptime.to.date.setHours(1,0,0,0);
$q.when(customTimeModal).then(function(modalEl) {
modalEl.modal('show');

View File

@@ -1,23 +1,24 @@
<!-- is there a better way to repeat without actually affecting the page? -->
<div class="filter-pulldown" ng-repeat="pulldown in dashboard.current.pulldowns" ng-controller="PulldownCtrl" ng-show="pulldown.enable">
<div class="top-row-close pointer pull-left" ng-click="toggle_pulldown(pulldown);dismiss();" bs-tooltip="'Toggle '+pulldown.type" data-placement="bottom">
<span class="small"><strong>{{pulldown.type}}</strong></span>
</div>
<div class="top-row-open" ng-hide="pulldown.collapse">
<kibana-simple-panel type="pulldown.type" ng-cloak></kibana-simple-panel>
<div class="submenu-controls">
<div class="submenu-panel" ng-controller="SubmenuCtrl" ng-repeat="pulldown in dashboard.current.pulldowns | filter:{ enable: true }">
<div class="submenu-panel-title">
<span class="small"><strong>{{pulldown.type}}:</strong></span>
</div>
<div class="submenu-panel-wrapper">
<kibana-simple-panel type="pulldown.type" ng-cloak></kibana-simple-panel>
</div>
</div>
<div class="clearfix"></div>
</div>
<div class="clearfix"></div>
<div class="container-fluid main" ng-class="{'grafana-dashboard-hide-controls': dashboard.current.hideControls}">
<div class="row-fluid">
<div class="row-fluid container" style="margin-top:10px; width:98%">
<div>
<div class="grafana-container container">
<!-- Rows -->
<div class="row-fluid kibana-row" ng-controller="RowCtrl" ng-repeat="(row_name, row) in dashboard.current.rows" ng-style="row_style(row)">
<div class="kibana-row" ng-controller="RowCtrl" ng-repeat="(row_name, row) in dashboard.current.rows" ng-style="row_style(row)">
<div class="row-control">
<div class="row-fluid grafana-row" style="padding:0px;margin:0px;position:relative;">
<div class="row-close span12" ng-show="row.collapse" data-placement="bottom" >
<div class="grafana-row" style="padding:0px;margin:0px;position:relative;">
<div class="row-close" ng-show="row.collapse" data-placement="bottom" >
<span class="row-button bgWarning" bs-modal="'app/partials/roweditor.html'" class="pointer">
<i bs-tooltip="'Configure row'" data-placement="right" ng-show="row.editable" class="icon-cog pointer"></i>
</span>
@@ -43,33 +44,26 @@
</div>
</div>
<div class="row-fluid" style="padding-top:0px" ng-if="!row.collapse">
<div style="padding-top:0px" ng-if="!row.collapse">
<!-- Panels -->
<div ng-repeat="(name, panel) in row.panels|filter:isPanel" ng-hide="panel.span == 0 || panel.hide" class="span{{panel.span}} panel nospace" style="min-height:{{row.height}}; position:relative" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:$index,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver(true)',onOut:'panelMoveOut'}">
<!-- Error Panel -->
<div class="row-fluid">
<div class="span12 alert-error panel-error" ng-hide="!panel.error">
<a class="close" ng-click="panel.error=false">&times;</a>
<i class="icon-exclamation-sign"></i> <strong>Oops!</strong> {{panel.error}}
</div>
</div>
<div ng-repeat="(name, panel) in row.panels|filter:isPanel" ng-hide="panel.hide" class="panel nospace" ng-style="{'width':!panel.span?'100%':(panel.span/1.2)*10+'%'}" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:$index,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver(true)',onOut:'panelMoveOut'}" ng-class="{'dragInProgress':dashboard.panelDragging}">
<!-- Content Panel -->
<div class="row-fluid" style="position:relative" ng-class="{'dragInProgress':dashboard.panelDragging}" >
<div style="position:relative">
<kibana-panel type="panel.type" ng-cloak></kibana-panel>
</div>
</div>
<div ng-hide="(12-rowSpan(row)) < 1 || !dashboard.current.panel_hints" class="panel span{{(12-rowSpan(row))}}" ng-class="{'dragInProgress':dashboard.panelDragging}" ng-style="{height:row.height}" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:row.panels.length,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver',onOut:'panelMoveOut'}">
<span bs-modal="'app/partials/roweditor.html'" ng-show="row.editable && !dashboard.panelDragging">
<i ng-hide="rowSpan(row) == 0" class="pointer icon-plus-sign" ng-click="editor.index = 2" bs-tooltip="'Add a panel to this row'" data-placement="right"></i>
<span ng-click="editor.index = 2" style="margin-top: 8px; margin-left: 3px" ng-show="rowSpan(row) == 0" class="btn btn-mini">Add panel to empty row</btn>
</span>
<div ng-show="rowSpan(row) < 10 && dashboard.panelDragging" class="panel" style="margin:5px;width:30%;background:rgba(100,100,100,0.50)" ng-class="{'dragInProgress':dashboard.panelDragging}" ng-style="{height:row.height}" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:row.panels.length,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver',onOut:'panelMoveOut'}">
</div>
<span bs-modal="'app/partials/roweditor.html'" ng-show="!dashboard.panelDragging && !dashboard.current.hideControls">
<i ng-hide="rowSpan(row) >= 10" class="pointer icon-plus-sign" ng-click="editor.index = 2" bs-tooltip="'Add a panel to this row'" data-placement="right"></i>
</span>
<div class="clearfix"></div>
</div>
</div>
</div>
</div>

View File

@@ -18,7 +18,8 @@
<label class="small">Style</label><select class="input-small" ng-model="dashboard.current.style" ng-options="f for f in ['dark','light']"></select>
</div>
<div class="editor-option">
<label class="small"> Editable </label><input type="checkbox" ng-model="dashboard.current.editable" ng-checked="dashboard.current.editable" />
<label class="small">Time correction</label>
<select ng-model="dashboard.current.timezone" class='input-small' ng-options="f for f in ['browser','utc']"></select>
</div>
<div class="editor-option">
<label class="small"> Hints <tip>Show 'Add panel' hints in empty spaces</tip></label><input type="checkbox" ng-model="dashboard.current.panel_hints" ng-checked="dashboard.current.panel_hints" />
@@ -29,6 +30,16 @@
</div>
</div>
</div>
<div class="editor-row">
<div class="section">
<div class="editor-option">
<label class="small">Tags</label>
<bootstrap-tagsinput ng-model="dashboard.current.tags" tagclass="label label-tag" placeholder="add tags">
</bootstrap-tagsinput>
<tip>Press enter to a add tag</tip>
</div>
</div>
</div>
</div>
<div ng-if="editor.index == 1">
@@ -132,7 +143,7 @@
<ng-include src="'app/partials/import.html'"></ng-include>
</div>
<div ng-repeat="pulldown in dashboard.current.nav" ng-controller="PulldownCtrl" ng-show="editor.index == 5+$index">
<div ng-repeat="pulldown in dashboard.current.nav" ng-controller="SubmenuCtrl" ng-show="editor.index == 5+$index">
<ng-include ng-show="pulldown.enable" src="edit_path(pulldown.type)"></ng-include>
<button ng-hide="pulldown.enable" class="btn" ng-click="pulldown.enable = true">Enable the {{pulldown.type}}</button>
</div>
@@ -140,6 +151,13 @@
</div>
<div class="modal-footer">
<div class="pull-left" style="padding-top: 15px;" ng-if="editor.index == 0">
<span class="editor-option small">
Grafana {{grafanaVersion}}
</span>
(<a class="small" href="https://github.com/torkelo/grafana/releases" target="_blank">check for updates</a>)
</div>
<button ng-click="add_row(dashboard.current,row); reset_row();" class="btn btn-success" ng-show="editor.index == 1">Create Row</button>
<button type="button" class="btn btn-danger" ng-click="editor.index=0;dismiss();reset_panel();dashboard.refresh()">Close</button>
</div>

View File

@@ -1,5 +1,5 @@
<div ng-controller="GraphiteImportCtrl" ng-init="init()">
<h5>Import dashboards from graphite webb</h5>
<h5>Import dashboards from graphite web</h5>
<div class="editor-row">
<div class="section">

View File

@@ -1,7 +1,10 @@
<div ng-controller="MetricKeysCtrl" ng-init="init()">
<h5>Load metrics keys into elastic search</h5>
<div class="row-fluid">
<p>
Work in progress...
</p>
<!-- <div class="row-fluid">
<div class="span12">
<label class="small">Load metrics recursive starting from this metric path</label>
<input type="text" class="input-xlarge" ng-model="metricPath"> </input>
@@ -29,5 +32,5 @@
<div class="span12" style="padding-top: 10px;">
Metrics indexed: {{metricCounter}}
</div>
</div>
</div> -->
</div>

View File

@@ -6,17 +6,17 @@
</div>
</div>
<div class="row-fluid" ng-if="editor.index == 0">
<div class="span4">
<div class="editor-row" ng-if="editor.index == 0">
<div class="editor-option">
<label class="small">Title</label><input type="text" class="input-medium" ng-model='row.title'></input>
</div>
<div class="span2">
<div class="editor-option">
<label class="small">Height</label><input type="text" class="input-mini" ng-model='row.height'></input>
</div>
<div class="span1">
<div class="editor-option">
<label class="small"> Editable </label><input type="checkbox" ng-model="row.editable" ng-checked="row.editable" />
</div>
<div class="span1">
<div class="editor-option">
<label class="small"> Collapsable </label><input type="checkbox" ng-model="row.collapsable" ng-checked="row.collapsable" />
</div>
</div>

View File

@@ -19,15 +19,45 @@
<ul class="dropdown-menu" id="grafana-search">
<li ng-if="!showImport">
<div class="grafana-search-panel">
<input type="text"
placeholder="search dashboards, metrics, or graphs"
xng-focus="giveSearchFocus"
ng-keydown="keyDown($event)"
ng-model="elasticsearch.query"
ng-change="elasticsearch_dblist(elasticsearch.query)" />
<h6 ng-hide="search_results.dashboards.length || search_results.metrics.length">No dashboards or metrics matching your query found</h6>
<table class="table table-condensed table-striped">
<tr bindonce ng-repeat="row in search_results.metrics"
<div class="search-field-wrapper">
<button class="btn btn-success pull-right" ng-click="toggleImport($event)">
<i class="icon-download-alt"></i>
Import
</button>
<button class="btn btn-success pull-right" ng-click="newDashboard()">
<i class="icon-th-large"></i>
New
</button>
<span class="position: relative;">
<input type="text"
placeholder="search dashboards, metrics, or graphs"
xng-focus="giveSearchFocus"
ng-keydown="keyDown($event)"
ng-model="query.query" spellcheck='false'
ng-change="search()" />
<a class="search-tagview-switch" href="javascript:void(0);"
ng-class="{'active': tagsOnly}"
ng-click="showTags($event)">Tags</a>
</span>
</div>
<h6 ng-hide="results.dashboards.length || results.metrics.length">No dashboards or metrics matching your query found</h6>
<table class="table table-condensed table-striped" ng-if="tagsOnly">
<tr ng-repeat="tag in results.tags" ng-class="{'selected-tag': $index === selectedIndex }">
<td>
<a ng-click="filterByTag(tag.term, $event)" class="label label-tag">
{{tag.term}} &nbsp;({{tag.count}})
</a>
</td>
<td style="width:100%;padding-left: 10px;font-weight: bold;">
</td>
</tr>
</table>
<table class="table table-condensed table-striped" ng-if="!tagsOnly">
<tr bindonce ng-repeat="row in results.metrics"
class="grafana-search-metric-result"
ng-class="{'selected': $index === selectedIndex }">
<td><span class="label label-info">metric</span></td>
@@ -42,19 +72,23 @@
</tr>
<tr bindonce
ng-repeat="row in search_results.dashboards"
ng-repeat="row in results.dashboards"
ng-class="{'selected': $index === selectedIndex }">
<td><a confirm-click="elasticsearch_delete(row._id)" confirmation="Are you sure you want to delete the {{row._id}} dashboard"><i class="icon-remove"></i></a></td>
<td style="width:100%"><a href="#/dashboard/elasticsearch/{{row._id}}" bo-text="row._id"></a></td>
<td style="width:100%">
<a href="#/dashboard/elasticsearch/{{row._id}}" bo-text="row._id"></a>
</td>
<td style="white-space: nowrap; text-align: right;">
<a ng-click="filterByTag(tag, $event)" ng-repeat="tag in row._source.tags" style="margin-right: 5px;" class="label label-tag">
{{tag}}
</a>
</td>
<td><a><i class="icon-share" ng-click="share = dashboard.share_link(row._id,'elasticsearch',row._id)" bs-modal="'app/partials/dashLoaderShare.html'"></i></a></td>
</tr>
</table>
</div>
</li>
<li class="pull-right" style="margin: 5px;">
<a ng-click="toggleImport($event)">Import</a>
</li>
<!-- ng-show="dashboard.current.loader.load_gist || dashboard.current.loader.load_local" -->
<li ng-if="showImport" style="margin: 20px;">
<div class="editor-row">

View File

@@ -5,7 +5,7 @@ define([
'./timer',
'./panelMove',
'./graphite/graphiteSrv',
'./esVersion',
'./keyboardManager',
'./annotationsSrv',
],
function () {});

View File

@@ -0,0 +1,77 @@
define([
'angular',
'underscore',
'moment'
], function (angular, _, moment) {
'use strict';
var module = angular.module('kibana.services');
module.service('annotationsSrv', function(dashboard, graphiteSrv, $q, alertSrv) {
this.init = function() {
this.annotationList = [
/* {
type: 'graphite-target',
enabled: false,
target: 'metrics_data.mysite.dolph.counters.payment.cart_klarna_payment_completed.count',
name: 'deploys',
},
{
type: 'graphite-target',
enabled: true,
target: 'metrics_data.mysite.dolph.counters.payment.cart_paypal_payment_completed.count',
name: 'restarts',
}*/
];
};
this.getAnnotations = function(rangeUnparsed) {
var graphiteAnnotations = _.where(this.annotationList, { type: 'graphite-target', enabled: true });
var graphiteTargets = _.map(graphiteAnnotations, function(annotation) {
return { target: annotation.target };
});
if (graphiteTargets.length === 0) {
return $q.when(null);
}
var graphiteQuery = {
range: rangeUnparsed,
targets: graphiteTargets,
format: 'json',
maxDataPoints: 100
};
return graphiteSrv.query(graphiteQuery)
.then(function(results) {
return _.reduce(results.data, function(list, target) {
_.each(target.datapoints, function (values) {
if (values[0] === null) {
return;
}
list.push({
min: values[1] * 1000,
max: values[1] * 1000,
eventType: "annotation",
title: null,
description: "<small><i class='icon-tag icon-flip-vertical'></i>test</small><br>"+
moment(values[1] * 1000).format('YYYY-MM-DD HH:mm:ss'),
score: 1
});
});
return list;
}, []);
})
.then(null, function() {
alertSrv.set('Annotations','Could not fetch annotations','error');
});
};
// Now init
this.init();
});
});

View File

@@ -21,12 +21,14 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
var _dash = {
title: "",
tags: [],
style: "dark",
timezone: 'browser',
editable: true,
failover: false,
panel_hints: true,
rows: [],
pulldowns: [ { type: 'filtering' } ],
pulldowns: [ { type: 'filtering' }, /*{ type: 'annotations' }*/ ],
nav: [ { type: 'timepicker' } ],
services: {},
loader: {
@@ -47,7 +49,7 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
};
// An elasticJS client to use
var ejs = ejsResource(config.elasticsearch);
var ejs = ejsResource(config.elasticsearch, config.elasticsearchBasicAuth);
var gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/;
// Store a reference to this
@@ -101,8 +103,8 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
$location.path(config.default_route);
alertSrv.set('Saving to browser storage has been replaced',' with saving to Elasticsearch.'+
' Click <a href="#/dashboard/local/deprecated">here</a> to load your old dashboard anyway.');
} else if(!(_.isUndefined(window.localStorage.kibanaDashboardDefault))) {
$location.path(window.localStorage.kibanaDashboardDefault);
} else if(!(_.isUndefined(window.localStorage.grafanaDashboardDefault))) {
$location.path(window.localStorage.grafanaDashboardDefault);
} else {
$location.path(config.default_route);
}
@@ -118,18 +120,26 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
};
var dash_defaults = function(dashboard) {
_.defaults(dashboard,_dash);
_.defaults(dashboard, _dash);
_.defaults(dashboard.loader,_dash.loader);
var filtering = _.findWhere(dashboard.pulldowns, {type: 'filtering'});
if (!filtering) {
dashboard.pulldowns.push({
type: 'filtering',
enable: false,
collapse: true
enable: false
});
}
/*var annotations = _.findWhere(dashboard.pulldowns, {type: 'annotations'});
if (!annotations) {
dashboard.pulldowns.push({
type: 'annotations',
enable: false
});
}*/
return dashboard;
};
@@ -137,11 +147,14 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
// Cancel all timers
timer.cancel_all();
// reset fullscreen flag
$rootScope.fullscreen = false;
// Make sure the dashboard being loaded has everything required
dashboard = dash_defaults(dashboard);
// Set the current dashboard
self.current = _.clone(dashboard);
self.current = angular.copy(dashboard);
// Delay this until we're sure that querySrv and filterSrv are ready
$timeout(function() {
@@ -196,7 +209,7 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
if(!_.isUndefined(window.localStorage['dashboard'])) {
delete window.localStorage['dashboard'];
}
window.localStorage.kibanaDashboardDefault = route;
window.localStorage.grafanaDashboardDefault = route;
return true;
} else {
return false;
@@ -210,7 +223,7 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
delete window.localStorage['dashboard'];
}
delete window.localStorage.kibanaDashboardDefault;
delete window.localStorage.grafanaDashboardDefault;
return true;
} else {
return false;
@@ -286,13 +299,21 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
};
this.elasticsearch_load = function(type,id) {
return $http({
var options = {
url: config.elasticsearch + "/" + config.grafana_index + "/"+type+"/"+id+'?' + new Date().getTime(),
method: "GET",
transformResponse: function(response) {
return renderTemplate(angular.fromJson(response)._source.dashboard, $routeParams);
}
}).error(function(data, status) {
};
if (config.elasticsearchBasicAuth) {
options.withCredentials = true;
options.headers = {
"Authorization": "Basic " + config.elasticsearchBasicAuth
};
}
return $http(options)
.error(function(data, status) {
if(status === 0) {
alertSrv.set('Error',"Could not contact Elasticsearch at "+config.elasticsearch+
". Please ensure that Elasticsearch is reachable from your system." ,'error');
@@ -344,6 +365,7 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
user: 'guest',
group: 'guest',
title: save.title,
tags: save.tags,
dashboard: angular.toJson(save)
});
@@ -377,22 +399,6 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
);
};
this.elasticsearch_list = function(query,count) {
var request = ejs.Request().indices(config.grafana_index).types('dashboard');
return request.query(
ejs.QueryStringQuery(query || '*')
).size(count).doSearch(
// Success
function(result) {
return result;
},
// Failure
function() {
return false;
}
);
};
this.save_gist = function(title,dashboard) {
var save = _.clone(dashboard || self.current);
save.title = title || self.current.title;

View File

@@ -1,150 +0,0 @@
define([
'angular',
'underscore',
'config'
],
function (angular, _, config) {
'use strict';
var module = angular.module('kibana.services');
module.service('esVersion', function($http, alertSrv) {
this.versions = [];
// save a reference to this
var self = this;
this.init = function() {
getVersions();
};
var getVersions = function() {
var nodeInfo = $http({
url: config.elasticsearch + '/_nodes',
method: "GET"
}).error(function(data, status) {
if(status === 0) {
alertSrv.set('Error',"Could not contact Elasticsearch at "+config.elasticsearch+
". Please ensure that Elasticsearch is reachable from your system." ,'error');
} else {
alertSrv.set('Error',"Could not reach "+config.elasticsearch+"/_nodes. If you"+
" are using a proxy, ensure it is configured correctly",'error');
}
});
return nodeInfo.then(function(p) {
_.each(p.data.nodes, function(v) {
self.versions.push(v.version.split('-')[0]);
});
self.versions = sortVersions(_.uniq(self.versions));
});
};
// Get the max version in this cluster
this.max = function() {
return _.last(self.versions);
};
// Return the lowest version in the cluster
this.min = function() {
return _.first(self.versions);
};
// Sort versions from lowest to highest
var sortVersions = function(versions) {
var _versions = _.clone(versions),
_r = [];
while(_r.length < versions.length) {
var _h = "0";
/*jshint -W083 */
_.each(_versions,function(v){
if(self.compare(_h,v)) {
_h = v;
}
});
_versions = _.without(_versions,_h);
_r.push(_h);
}
return _r.reverse();
};
/*
Takes a version string with one of the following optional comparison prefixes: >,>=,<.<=
and evaluates if the cluster meets the requirement. If the prefix is omitted exact match
is assumed
*/
this.is = function(equation) {
var _v = equation,
_cf;
if(_v.charAt(0) === '>') {
_cf = _v.charAt(1) === '=' ? self.gte(_v.slice(2)) : self.gt(_v.slice(1));
} else if (_v.charAt(0) === '<') {
_cf = _v.charAt(1) === '=' ? self.lte(_v.slice(2)) : self.lt(_v.slice(1));
} else {
_cf = self.eq(_v);
}
return _cf;
};
// check if lowest version in cluster = `version`
this.eq = function(version) {
return version === self.min() ? true : false;
};
// version > lowest version in cluster?
this.gt = function(version) {
return version === self.min() ? false : self.gte(version);
};
// version < highest version in cluster?
this.lt = function(version) {
return version === self.max() ? false : self.lte(version);
};
// Check if the lowest version in the cluster is >= to `version`
this.gte = function(version) {
return self.compare(version,self.min());
};
// Check if the highest version in the cluster is <= to `version`
this.lte = function(version) {
return self.compare(self.max(),version);
};
// Determine if a specific version is greater than or equal to another
this.compare = function (required,installed) {
var a = installed.split('.');
var b = required.split('.');
var i;
for (i = 0; i < a.length; ++i) {
a[i] = Number(a[i]);
}
for (i = 0; i < b.length; ++i) {
b[i] = Number(b[i]);
}
if (a.length === 2) {
a[2] = 0;
}
if (a[0] > b[0]){return true;}
if (a[0] < b[0]){return false;}
if (a[1] > b[1]){return true;}
if (a[1] < b[1]){return false;}
if (a[2] > b[2]){return true;}
if (a[2] < b[2]){return false;}
return true;
};
this.init();
});
});

View File

@@ -26,6 +26,7 @@ define([
self.list = dashboard.current.services.filter.list;
self.time = dashboard.current.services.filter.time;
self.filterTemplateData = undefined;
self.templateSettings = {
interpolate : /\[\[([\s\S]+?)\]\]/g,

View File

@@ -31,13 +31,6 @@ function (_) {
defaultParams: [1],
});
addFuncDef({
name: "alias",
category: categories.Special,
params: [ { name: "alias", type: 'string' } ],
defaultParams: ['alias']
});
addFuncDef({
name: "holtWintersForecast",
category: categories.Calculate,
@@ -69,6 +62,20 @@ function (_) {
category: categories.Combine,
});
addFuncDef({
name: "alias",
category: categories.Special,
params: [ { name: "alias", type: 'string' } ],
defaultParams: ['alias']
});
addFuncDef({
name: "aliasSub",
category: categories.Special,
params: [ { name: "search", type: 'string' }, { name: "replace", type: 'string' } ],
defaultParams: ['', '']
});
addFuncDef({
name: "groupByNode",
category: categories.Special,
@@ -94,6 +101,40 @@ function (_) {
defaultParams: [3]
});
addFuncDef({
name: 'sortByName',
category: categories.Special
});
addFuncDef({
name: 'aliasByMetric',
category: categories.Special,
});
addFuncDef({
name: 'randomWalk',
category: categories.Special,
params: [ { name: "name", type: "string", } ],
defaultParams: ['randomWalk']
});
addFuncDef({
name: 'countSeries',
category: categories.Special
});
addFuncDef({
name: 'constantLine',
category: categories.Special,
params: [ { name: "value", type: "int", } ],
defaultParams: [10]
});
addFuncDef({
name: 'cactiStyle',
category: categories.Special,
});
addFuncDef({
name: 'scale',
category: categories.Transform,
@@ -101,16 +142,30 @@ function (_) {
defaultParams: [1]
});
addFuncDef({
name: 'offset',
category: categories.Transform,
params: [ { name: "amount", type: "int", } ],
defaultParams: [10]
});
addFuncDef({
name: 'integral',
category: categories.Transform,
});
addFuncDef({
name: 'derivate',
name: 'derivative',
category: categories.Transform,
});
addFuncDef({
name: 'nonNegativeDerivative',
category: categories.Transform,
params: [ { name: "max value or 0", type: "int", } ],
defaultParams: [0]
});
addFuncDef({
name: 'timeShift',
category: categories.Transform,
@@ -121,8 +176,34 @@ function (_) {
addFuncDef({
name: 'summarize',
category: categories.Transform,
params: [ { name: "interval", type: "string" }],
defaultParams: ['1h']
params: [ { name: "interval", type: "string" }, { name: "func", type: "select", options: ['sum', 'avg', 'min', 'max', 'last'] }],
defaultParams: ['1h', 'sum']
});
addFuncDef({
name: 'absolute',
category: categories.Transform,
});
addFuncDef({
name: 'averageAbove',
category: categories.Filter,
params: [ { name: "n", type: "int", } ],
defaultParams: [25]
});
addFuncDef({
name: 'averageBelow',
category: categories.Filter,
params: [ { name: "n", type: "int", } ],
defaultParams: [25]
});
addFuncDef({
name: 'highestCurrent',
category: categories.Filter,
params: [ { name: "count", type: "int" } ],
defaultParams: [5]
});
_.each(categories, function(funcList, catName) {
@@ -135,6 +216,19 @@ function (_) {
this.updateText();
}
FuncInstance.prototype.render = function (metricExp) {
var str = this.def.name + '(';
var parameters = _.map(this.params, function(value) {
return _.isString(value) ? "'" + value + "'" : value;
});
if (metricExp !== undefined) {
parameters.unshift(metricExp);
}
return str + parameters.join(',') + ')';
};
FuncInstance.prototype.updateText = function () {
if (this.params.length === 0) {
this.text = this.def.name + '()';
@@ -166,4 +260,4 @@ function (_) {
}
};
});
});

View File

@@ -2,31 +2,41 @@ define([
'angular',
'underscore',
'jquery',
'config'
'config',
'kbn',
'moment'
],
function (angular, _, $, config) {
function (angular, _, $, config, kbn, moment) {
'use strict';
var module = angular.module('kibana.services');
module.service('graphiteSrv', function($http, $q, filterSrv) {
var graphiteRenderUrl = config.graphiteUrl + "/render";
this.query = function(options) {
try {
var graphOptions = {
from: $.plot.formatDate(options.range.from, '%H%:%M_%Y%m%d'),
until: $.plot.formatDate(options.range.to, '%H%:%M_%Y%m%d'),
from: this.translateTime(options.range.from),
until: this.translateTime(options.range.to),
targets: options.targets,
format: options.format,
maxDataPoints: options.maxDataPoints
};
var params = buildGraphitePostParams(graphOptions);
var params = buildGraphiteParams(graphOptions);
return $http({
if (options.format === 'png') {
return $q.when(graphiteRenderUrl + '?' + params.join('&'));
}
return doGraphiteRequest({
method: 'POST',
url: config.graphiteUrl + '/render/',
url: graphiteRenderUrl,
data: params.join('&'),
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
});
}
catch(err) {
@@ -34,6 +44,30 @@ function (angular, _, $, config) {
}
};
this.translateTime = function(date) {
if (_.isString(date)) {
if (date === 'now') {
return 'now';
}
else if (date.indexOf('now') >= 0) {
date = date.substring(3);
date = date.replace('m', 'min');
date = date.replace('M', 'mon');
return date;
}
date = kbn.parseDate(date);
}
date = moment.utc(date).local();
if (config.timezoneOffset) {
date = date.zone(config.timezoneOffset);
}
return date.format('HH:mm_YYYYMMDD');
};
this.match = function(targets, graphiteTargetStr) {
var found = targets[0];
@@ -60,7 +94,7 @@ function (angular, _, $, config) {
}
var url = config.graphiteUrl + '/metrics/find/?query=' + interpolated;
return $http.get(url)
return doGraphiteRequest({method: 'GET', url: url})
.then(function(results) {
return _.map(results.data, function(metric) {
return {
@@ -73,7 +107,7 @@ function (angular, _, $, config) {
this.listDashboards = function(query) {
var url = config.graphiteUrl + '/dashboard/find/';
return $http.get(url, {params: {query: query || ''}})
return doGraphiteRequest({ method: 'GET', url: url, params: {query: query || ''} })
.then(function(results) {
return results.data.dashboards;
});
@@ -81,14 +115,26 @@ function (angular, _, $, config) {
this.loadDashboard = function(dashName) {
var url = config.graphiteUrl + '/dashboard/load/' + encodeURIComponent(dashName);
return $http.get(url);
return doGraphiteRequest({method: 'GET', url: url});
};
function buildGraphitePostParams(options) {
function doGraphiteRequest(options) {
if (config.graphiteBasicAuth) {
options.withCredentials = true;
options.headers = options.headers || {};
options.headers.Authorization = 'Basic ' + config.graphiteBasicAuth;
}
return $http(options);
}
function buildGraphiteParams(options) {
var clean_options = [];
var graphite_options = ['target', 'targets', 'from', 'until', 'rawData', 'format', 'maxDataPoints'];
options['format'] = 'json';
if (options.format !== 'png') {
options['format'] = 'json';
}
$.each(options, function (key, value) {
if ($.inArray(key, graphite_options) === -1) {
@@ -112,4 +158,4 @@ function (angular, _, $, config) {
});
});
});

View File

@@ -116,6 +116,7 @@ define([
for (var i = 0; i < 128; i++) {
identifierStartTable[i] =
i >= 48 && i <= 57 || // 0-9
i === 36 || // $
i >= 65 && i <= 90 || // A-Z
i === 95 || // _
@@ -183,10 +184,10 @@ define([
}
match =
this.scanIdentifier() ||
this.scanTemplateSequence() ||
this.scanPunctuator() ||
this.scanNumericLiteral();
this.scanNumericLiteral() ||
this.scanIdentifier() ||
this.scanTemplateSequence();
if (match) {
this.skip(match.value.length);
@@ -401,14 +402,20 @@ define([
(ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z");
}
// Numbers must start either with a decimal digit or a point.
// handle negative num literals
if (char === '-') {
value += char;
index += 1;
char = this.peek(index);
}
// Numbers must start either with a decimal digit or a point.
if (char !== "." && !isDecimalDigit(char)) {
return null;
}
if (char !== ".") {
value = this.peek(index);
value += this.peek(index);
index += 1;
char = this.peek(index);
@@ -555,7 +562,7 @@ define([
if (index < length) {
char = this.peek(index);
if (isIdentifierStart(char)) {
if (!this.isPunctuator(char)) {
return null;
}
}
@@ -569,9 +576,7 @@ define([
};
},
scanPunctuator: function () {
var ch1 = this.peek();
isPunctuator: function (ch1) {
switch (ch1) {
case ".":
case "(":
@@ -579,6 +584,16 @@ define([
case ",":
case "{":
case "}":
return true;
}
return false;
},
scanPunctuator: function () {
var ch1 = this.peek();
if (this.isPunctuator(ch1)) {
return {
type: ch1,
value: ch1,

View File

@@ -29,12 +29,47 @@ define([
}
},
metricSegment: function() {
if (this.match('identifier')) {
this.index++;
curlyBraceSegment: function() {
if (this.match('identifier', '{') || this.match('{')) {
var curlySegment = "";
while(!this.match('') && !this.match('}')) {
curlySegment += this.consumeToken().value;
}
if (!this.match('}')) {
this.errorMark("Expected closing '}'");
}
curlySegment += this.consumeToken().value;
// if curly segment is directly followed by identifier
// include it in the segment
if (this.match('identifier')) {
curlySegment += this.consumeToken().value;
}
return {
type: 'segment',
value: this.tokens[this.index-1].value
value: curlySegment
};
}
else {
return null;
}
},
metricSegment: function() {
var curly = this.curlyBraceSegment();
if (curly) {
return curly;
}
if (this.match('identifier') || this.match('number')) {
return {
type: 'segment',
value: this.consumeToken().value
};
}
@@ -42,7 +77,7 @@ define([
this.errorMark('Expected metric identifier');
}
this.index++;
this.consumeToken();
if (!this.match('identifier')) {
this.errorMark('Expected identifier after templateStart');
@@ -50,21 +85,19 @@ define([
var node = {
type: 'template',
value: this.tokens[this.index].value
value: this.consumeToken().value
};
this.index++;
if (!this.match('templateEnd')) {
this.errorMark('Expected templateEnd');
}
this.index++;
this.consumeToken();
return node;
},
metricExpression: function() {
if (!this.match('templateStart') && !this.match('identifier')) {
if (!this.match('templateStart') && !this.match('identifier') && !this.match('number')) {
return null;
}
@@ -76,7 +109,7 @@ define([
node.segments.push(this.metricSegment());
while(this.match('.')) {
this.index++;
this.consumeToken();
var segment = this.metricSegment();
if (!segment) {
@@ -96,10 +129,11 @@ define([
var node = {
type: 'function',
name: this.tokens[this.index].value,
name: this.consumeToken().value,
};
this.index += 2;
// consume left paranthesis
this.consumeToken();
node.params = this.functionParameters();
@@ -107,7 +141,7 @@ define([
this.errorMark('Expected closing paranthesis');
}
this.index++;
this.consumeToken();
return node;
},
@@ -119,15 +153,15 @@ define([
var param =
this.functionCall() ||
this.metricExpression() ||
this.numericLiteral() ||
this.metricExpression() ||
this.stringLiteral();
if (!this.match(',')) {
return [param];
}
this.index++;
this.consumeToken();
return [param].concat(this.functionParameters());
},
@@ -136,11 +170,9 @@ define([
return null;
}
this.index++;
return {
type: 'number',
value: parseInt(this.tokens[this.index-1].value, 10)
value: parseFloat(this.consumeToken().value)
};
},
@@ -149,13 +181,11 @@ define([
return null;
}
var token = this.tokens[this.index];
var token = this.consumeToken();
if (token.isUnclosed) {
throw { message: 'Unclosed string parameter', pos: token.pos };
}
this.index++;
return {
type: 'string',
value: token.value
@@ -171,6 +201,12 @@ define([
};
},
// returns token value and incre
consumeToken: function() {
this.index++;
return this.tokens[this.index-1];
},
matchToken: function(type, index) {
var token = this.tokens[this.index + index];
return (token === undefined && type === '') ||

View File

@@ -1,25 +0,0 @@
/** @scratch /configuration/config.js/1
* == Configuration
* config.js is where you will find the core Grafana configuration. This file contains parameter that
* must be set before kibana is run for the first time.
*/
define(['settings'],
function (Settings) {
"use strict";
return new Settings({
elasticsearch: "http://"+window.location.hostname+":9200",
graphiteUrl: "http://"+window.location.hostname+":8080",
default_route: '/dashboard/file/default.json',
grafana_index: "grafana-dash",
panel_names: [
'text',
'graphite'
]
});
});

42
src/config.sample.js Normal file
View File

@@ -0,0 +1,42 @@
/** @scratch /configuration/config.js/1
* == Configuration
* config.js is where you will find the core Grafana configuration. This file contains parameter that
* must be set before kibana is run for the first time.
*/
define(['settings'],
function (Settings) {
"use strict";
return new Settings({
/**
* elasticsearch url:
* For Basic authentication use: http://username:password@domain.com:9200
*/
elasticsearch: "http://"+window.location.hostname+":9200",
/**
* graphite-web url:
* For Basic authentication use: http://username:password@domain.com
* Basic authentication requires special HTTP headers to be configured
* in nginx or apache for cross origin domain sharing to work (CORS).
* Check install documentation on github
*/
graphiteUrl: "http://"+window.location.hostname+":8080",
default_route: '/dashboard/file/default.json',
/**
* If your graphite server has another timezone than you & users browsers specify the offset here
* Example: "-0500" (for UTC - 5 hours)
*/
timezoneOffset: null,
grafana_index: "grafana-dash",
panel_names: [
'text',
'graphite'
]
});
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

52
src/css/less/bootstrap-tagsinput.less vendored Normal file
View File

@@ -0,0 +1,52 @@
.bootstrap-tagsinput {
display: inline-block;
padding: 4px 6px;
margin-bottom: 10px;
color: #555;
vertical-align: middle;
border-radius: 4px;
max-width: 100%;
line-height: 22px;
background-color: @inputBackground;
border: 1px solid @inputBorder;
.box-shadow(inset 0 1px 1px rgba(0,0,0,.075));
.transition(~"border linear .2s, box-shadow linear .2s");
input {
border: none;
box-shadow: none;
outline: none;
background-color: transparent;
padding: 0;
padding-left: 5px;
margin: 0;
width: auto !important;
max-width: inherit;
&:focus {
border: none;
box-shadow: none;
}
}
.tag {
margin-right: 2px;
color: white;
[data-role="remove"] {
margin-left:8px;
cursor:pointer;
&:after{
content: "x";
padding:0px 2px;
}
&:hover {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
&:active {
box-shadow: inset 0 3px 5px rgba(0,0,0,0.125);
}
}
}
}
}

View File

@@ -21,3 +21,16 @@
// 768px-979px
@fluidGridColumnWidth768: percentage(@gridColumnWidth768/@gridRowWidth768);
@fluidGridGutterWidth768: percentage(@gridGutterWidth768/@gridRowWidth768);
// Media queries
// ---------------------
@media (max-width: 767px) {
div.panel {
width: 100% !important;
padding: 0px !important;
}
body {
padding: 0;
}
}

View File

@@ -1,3 +1,5 @@
@import "submenu.less";
@import "bootstrap-tagsinput.less";
.navbar-static-top {
border-bottom: 1px solid black;
@@ -10,12 +12,23 @@
}
}
// Search
.grafana-search-panel {
padding: 6px 10px;
input {
width: 100%;
.box-sizing(border-box);
padding: 15px;
.search-field-wrapper {
input {
width: 100%;
}
button {
margin: 0 2px 0 0;
}
> span {
display: block;
overflow: hidden;
padding-right: 25px;
}
}
.selected td, tr.selected:nth-child(odd)>td {
@@ -26,19 +39,20 @@
color: white;
}
}
.selected-tag .label-tag {
background-color: @blue;
}
}
.filter-pulldown {
background: #292929;
}
.top-row-close {
border-right: 1px solid #202020;
}
.top-row-open {
float: left;
padding: 0px;
background: none;
.search-tagview-switch {
position: absolute;
top: 15px;
right: 180px;
color: darken(@linkColor, 30%);
&.active {
color: @linkColor;
}
}
.row-button {
@@ -345,4 +359,79 @@ input[type=text].func-param {
.editor-row {
padding: 5px;
}
}
}
.scrollable {
max-height: 300px;
overflow: auto;
}
//
// Srollbars
//
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar:hover {
height: 8px;
}
::-webkit-scrollbar-button:start:decrement,
::-webkit-scrollbar-button:end:increment { display: none; }
::-webkit-scrollbar-button:horizontal:decrement { display: none; }
::-webkit-scrollbar-button:horizontal:increment { display: none; }
::-webkit-scrollbar-button:vertical:decrement { display: none; }
::-webkit-scrollbar-button:vertical:increment { display: none; }
::-webkit-scrollbar-button:horizontal:decrement:active { background-image: none; }
::-webkit-scrollbar-button:horizontal:increment:active { background-image: none; }
::-webkit-scrollbar-button:vertical:decrement:active { background-image: none; }
::-webkit-scrollbar-button:vertical:increment:active {background-image: none; }
::-webkit-scrollbar-track-piece { background-color: grayDark; }
::-webkit-scrollbar-thumb:vertical {
height: 50px;
background: -webkit-gradient(linear, left top, right top, color-stop(0%, #3a3a3a), color-stop(100%, #222222));
border: 1px solid #0d0d0d;
border-top: 1px solid #666666;
border-left: 1px solid #666666;
}
::-webkit-scrollbar-thumb:horizontal {
width: 50px;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #3a3a3a), color-stop(100%, #222222));
border: 1px solid #1f1f1f;
border-top: 1px solid #666666;
border-left: 1px solid #666666;
}
// SPECTRUM CSS overrides
.sp-replacer {
background: inherit;
border: none;
color: inherit;
}
.sp-replacer:hover, .sp-replacer.sp-active {
border-color: inherit;
color: inherit;
}
.sp-container {
border-radius: 0;
background-color: @heroUnitBackground;
border: none;
padding: 0;
}
.sp-palette-container, .sp-picker-container {
border: none;
}

View File

@@ -1,3 +1,12 @@
// Media queries
// ---------------------
@media (max-width: 767px) {
div.panel {
width: 100% !important;
padding: 0px !important;
}
}
// Containers
// ---------------------
.container-fluid {
@@ -5,6 +14,11 @@
padding-right: 0px;
}
.container.grafana-container {
padding: 5px 10px;
width: 100%;
box-sizing: border-box;
}
// Backgrounds
// ---------------------
@@ -51,14 +65,57 @@ code, pre {
background-color: @grayLighter;
}
.panelCont {
padding: 0px 10px 10px 10px;
background: @kibanaPanelBackground;
margin: 0px;
//border: 1px solid rgba(100, 100, 100, 0.25);
//outline: 1px solid darken(@bodyBackground, 10%);
.panel {
display: inline-table;
vertical-align: top;
}
.panel-container {
padding: 0px 0px 0px 0px;
background: @kibanaPanelBackground;
margin: 5px;
}
.panel-content {
padding: 0px 10px 10px 10px;
}
.panel-title {
border: 0px;
font-weight: bold;
}
.panel-loading {
position:absolute;
top: 0px;
right: 4px;
z-index: 800;
}
.panel div.panel-extra div.panel-extra-container {
margin-right: -10px;
margin-top: 3px;
text-align: center;
ul {
text-align: left;
}
}
.panel div.panel-extra {
font-size: 0.9em;
margin-bottom: 0px;
}
.panel div.panel-extra .extra {
float:right !important;
}
.panel-error {
color: @white;
padding: 3px 10px 0px 10px;
}
div.editor-row {
vertical-align: top;
}
@@ -138,7 +195,7 @@ form input.ng-invalid {
}
.kibana-row {
margin-bottom: 10px;
margin-bottom: 5px;
}
.row-tab {
@@ -173,21 +230,8 @@ form input.ng-invalid {
background: @kibanaPanelBackground;
}
.top-row-open {
background: @navbarBackground;
padding: 5px 25px 5px 25px;
}
.top-row-close {
padding: 5px 10px;
text-transform: uppercase;
margin: 0px;
text-align: left;
min-height: 16px !important;
line-height: 16px;
}
.row-open {
margin-top: 5px;
left:-34px;
position: absolute;
z-index: 100;
@@ -217,48 +261,13 @@ form input.ng-invalid {
border-top: 0px;
}
.panel-loading {
position:absolute;
top: 0px;
left: 0px;
z-index: 800;
}
.ui-draggable-dragging {
display: block;
visibility: visible;
opacity: 1;
z-index: 9999;
}
.panel-title {
border: 0px;
//text-transform: uppercase;
font-weight: bold;
}
.panel div.panel-extra div.panel-extra-container {
margin-right: -10px;
margin-top: 3px;
text-align: center;
ul {
text-align: left;
}
}
.panel div.panel-extra {
font-size: 0.9em;
margin-bottom: 10px;
}
.panel div.panel-extra .extra {
float:right !important;
//border-bottom: 1px solid lighten(@bodyBackground, 5%);
}
.dragInProgress {
background-color: darken(@bodyBackground,1%);
border: 1px solid @tableBorder;
.dragInProgress .panel-container {
border: 3px solid rgba(100,100,100,0.50);
}
.link {
@@ -352,10 +361,6 @@ div.flot-text {
color: @white;
}
.panel-error {
color: @white;
padding: 3px 10px 0px 10px;
}
.alert-warning {
background-color: @warningBackground;
@@ -545,4 +550,17 @@ div.flot-text {
border-top-color: @popoverArrowColor;
}
}
}
// Labels & Badges
.label-tag {
background-color: @purple;
color: @linkColor;
}
.label-tag:hover {
background-color: darken(@purple, 10%);
color: lighten(@linkColor, 5%);
}

41
src/css/less/submenu.less Normal file
View File

@@ -0,0 +1,41 @@
.submenu-controls {
background: #292929;
font-size: inherit;
label {
margin: 0;
padding-right: 4px;
display: inline;
}
input[type=checkbox] {
margin: 0;
}
}
.submenu-panel {
padding: 0 10px 0 17px;
border-right: 1px solid #202020;
float: left;
}
.submenu-panel-title {
float: left;
text-transform: uppercase;
padding: 4px 10px 3px 0;
}
.submenu-panel-wrapper {
float: left;
}
.submenu-toggle {
padding: 4px 0 3px 8px;
float: left;
}
.submenu-toggle:first-child {
padding-left: 0;
}
.annotation-disabled, .annotation-disabled a {
color: darken(@textColor, 25%);
}

View File

@@ -159,7 +159,7 @@
// Input placeholder text color
// -------------------------
@placeholderText: @grayLight;
@placeholderText: darken(@textColor, 25%);
// Hr border color

519
src/css/spectrum.css Normal file
View File

@@ -0,0 +1,519 @@
/***
Spectrum Colorpicker v1.3.0
https://github.com/bgrins/spectrum
Author: Brian Grinstead
License: MIT
***/
.sp-container {
position:absolute;
top:0;
left:0;
display:inline-block;
*display: inline;
*zoom: 1;
/* https://github.com/bgrins/spectrum/issues/40 */
z-index: 9999994;
overflow: hidden;
}
.sp-container.sp-flat {
position: relative;
}
/* Fix for * { box-sizing: border-box; } */
.sp-container,
.sp-container * {
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
/* http://ansciath.tumblr.com/post/7347495869/css-aspect-ratio */
.sp-top {
position:relative;
width: 100%;
display:inline-block;
}
.sp-top-inner {
position:absolute;
top:0;
left:0;
bottom:0;
right:0;
}
.sp-color {
position: absolute;
top:0;
left:0;
bottom:0;
right:20%;
}
.sp-hue {
position: absolute;
top:0;
right:0;
bottom:0;
left:84%;
height: 100%;
}
.sp-clear-enabled .sp-hue {
top:33px;
height: 77.5%;
}
.sp-fill {
padding-top: 80%;
}
.sp-sat, .sp-val {
position: absolute;
top:0;
left:0;
right:0;
bottom:0;
}
.sp-alpha-enabled .sp-top {
margin-bottom: 18px;
}
.sp-alpha-enabled .sp-alpha {
display: block;
}
.sp-alpha-handle {
position:absolute;
top:-4px;
bottom: -4px;
width: 6px;
left: 50%;
cursor: pointer;
border: 1px solid black;
background: white;
opacity: .8;
}
.sp-alpha {
display: none;
position: absolute;
bottom: -14px;
right: 0;
left: 0;
height: 8px;
}
.sp-alpha-inner {
border: solid 1px #333;
}
.sp-clear {
display: none;
}
.sp-clear.sp-clear-display {
background-position: center;
}
.sp-clear-enabled .sp-clear {
display: block;
position:absolute;
top:0px;
right:0;
bottom:0;
left:84%;
height: 28px;
}
/* Don't allow text selection */
.sp-container, .sp-replacer, .sp-preview, .sp-dragger, .sp-slider, .sp-alpha, .sp-clear, .sp-alpha-handle, .sp-container.sp-dragging .sp-input, .sp-container button {
-webkit-user-select:none;
-moz-user-select: -moz-none;
-o-user-select:none;
user-select: none;
}
.sp-container.sp-input-disabled .sp-input-container {
display: none;
}
.sp-container.sp-buttons-disabled .sp-button-container {
display: none;
}
.sp-palette-only .sp-picker-container {
display: none;
}
.sp-palette-disabled .sp-palette-container {
display: none;
}
.sp-initial-disabled .sp-initial {
display: none;
}
/* Gradients for hue, saturation and value instead of images. Not pretty... but it works */
.sp-sat {
background-image: -webkit-gradient(linear, 0 0, 100% 0, from(#FFF), to(rgba(204, 154, 129, 0)));
background-image: -webkit-linear-gradient(left, #FFF, rgba(204, 154, 129, 0));
background-image: -moz-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
background-image: -o-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
background-image: -ms-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
background-image: linear-gradient(to right, #fff, rgba(204, 154, 129, 0));
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#FFFFFFFF, endColorstr=#00CC9A81)";
filter : progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr='#FFFFFFFF', endColorstr='#00CC9A81');
}
.sp-val {
background-image: -webkit-gradient(linear, 0 100%, 0 0, from(#000000), to(rgba(204, 154, 129, 0)));
background-image: -webkit-linear-gradient(bottom, #000000, rgba(204, 154, 129, 0));
background-image: -moz-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
background-image: -o-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
background-image: -ms-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
background-image: linear-gradient(to top, #000, rgba(204, 154, 129, 0));
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#00CC9A81, endColorstr=#FF000000)";
filter : progid:DXImageTransform.Microsoft.gradient(startColorstr='#00CC9A81', endColorstr='#FF000000');
}
.sp-hue {
background: -moz-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
background: -ms-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
background: -o-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
background: -webkit-gradient(linear, left top, left bottom, from(#ff0000), color-stop(0.17, #ffff00), color-stop(0.33, #00ff00), color-stop(0.5, #00ffff), color-stop(0.67, #0000ff), color-stop(0.83, #ff00ff), to(#ff0000));
background: -webkit-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
}
/* IE filters do not support multiple color stops.
Generate 6 divs, line them up, and do two color gradients for each.
Yes, really.
*/
.sp-1 {
height:17%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0000', endColorstr='#ffff00');
}
.sp-2 {
height:16%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffff00', endColorstr='#00ff00');
}
.sp-3 {
height:17%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ff00', endColorstr='#00ffff');
}
.sp-4 {
height:17%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffff', endColorstr='#0000ff');
}
.sp-5 {
height:16%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0000ff', endColorstr='#ff00ff');
}
.sp-6 {
height:17%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff00ff', endColorstr='#ff0000');
}
.sp-hidden {
display: none !important;
}
/* Clearfix hack */
.sp-cf:before, .sp-cf:after { content: ""; display: table; }
.sp-cf:after { clear: both; }
.sp-cf { *zoom: 1; }
/* Mobile devices, make hue slider bigger so it is easier to slide */
@media (max-device-width: 480px) {
.sp-color { right: 40%; }
.sp-hue { left: 63%; }
.sp-fill { padding-top: 60%; }
}
.sp-dragger {
border-radius: 5px;
height: 5px;
width: 5px;
border: 1px solid #fff;
background: #000;
cursor: pointer;
position:absolute;
top:0;
left: 0;
}
.sp-slider {
position: absolute;
top:0;
cursor:pointer;
height: 3px;
left: -1px;
right: -1px;
border: 1px solid #000;
background: white;
opacity: .8;
}
/*
Theme authors:
Here are the basic themeable display options (colors, fonts, global widths).
See http://bgrins.github.io/spectrum/themes/ for instructions.
*/
.sp-container {
border-radius: 0;
background-color: #ECECEC;
border: solid 1px #f0c49B;
padding: 0;
}
.sp-container, .sp-container button, .sp-container input, .sp-color, .sp-hue, .sp-clear
{
font: normal 12px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
}
.sp-top
{
margin-bottom: 3px;
}
.sp-color, .sp-hue, .sp-clear
{
border: solid 1px #666;
}
/* Input */
.sp-input-container {
float:right;
width: 100px;
margin-bottom: 4px;
}
.sp-initial-disabled .sp-input-container {
width: 100%;
}
.sp-input {
font-size: 12px !important;
border: 1px inset;
padding: 4px 5px;
margin: 0;
width: 100%;
background:transparent;
border-radius: 3px;
color: #222;
}
.sp-input:focus {
border: 1px solid orange;
}
.sp-input.sp-validation-error
{
border: 1px solid red;
background: #fdd;
}
.sp-picker-container , .sp-palette-container
{
float:left;
position: relative;
padding: 10px;
padding-bottom: 300px;
margin-bottom: -290px;
}
.sp-picker-container
{
width: 172px;
border-left: solid 1px #fff;
}
/* Palettes */
.sp-palette-container
{
border-right: solid 1px #ccc;
}
.sp-palette .sp-thumb-el {
display: block;
position:relative;
float:left;
width: 24px;
height: 15px;
margin: 3px;
cursor: pointer;
border:solid 2px transparent;
}
.sp-palette .sp-thumb-el:hover, .sp-palette .sp-thumb-el.sp-thumb-active {
border-color: orange;
}
.sp-thumb-el
{
position:relative;
}
/* Initial */
.sp-initial
{
float: left;
border: solid 1px #333;
}
.sp-initial span {
width: 30px;
height: 25px;
border:none;
display:block;
float:left;
margin:0;
}
.sp-initial .sp-clear-display {
background-position: center;
}
/* Buttons */
.sp-button-container {
float: right;
}
/* Replacer (the little preview div that shows up instead of the <input>) */
.sp-replacer {
margin:0;
overflow:hidden;
cursor:pointer;
padding: 4px;
display:inline-block;
*zoom: 1;
*display: inline;
border: solid 1px #91765d;
background: #eee;
color: #333;
vertical-align: middle;
}
.sp-replacer:hover, .sp-replacer.sp-active {
border-color: #F0C49B;
color: #111;
}
.sp-replacer.sp-disabled {
cursor:default;
border-color: silver;
color: silver;
}
.sp-dd {
padding: 2px 0;
height: 16px;
line-height: 16px;
float:left;
font-size:10px;
}
.sp-preview
{
position:relative;
width:25px;
height: 20px;
border: solid 1px #222;
margin-right: 5px;
float:left;
z-index: 0;
}
.sp-palette
{
*width: 220px;
max-width: 220px;
}
.sp-palette .sp-thumb-el
{
width:16px;
height: 16px;
margin:2px 1px;
border: solid 1px #d0d0d0;
}
.sp-container
{
padding-bottom:0;
}
/* Buttons: http://hellohappy.org/css3-buttons/ */
.sp-container button {
background-color: #eeeeee;
background-image: -webkit-linear-gradient(top, #eeeeee, #cccccc);
background-image: -moz-linear-gradient(top, #eeeeee, #cccccc);
background-image: -ms-linear-gradient(top, #eeeeee, #cccccc);
background-image: -o-linear-gradient(top, #eeeeee, #cccccc);
background-image: linear-gradient(to bottom, #eeeeee, #cccccc);
border: 1px solid #ccc;
border-bottom: 1px solid #bbb;
border-radius: 3px;
color: #333;
font-size: 14px;
line-height: 1;
padding: 5px 4px;
text-align: center;
text-shadow: 0 1px 0 #eee;
vertical-align: middle;
}
.sp-container button:hover {
background-color: #dddddd;
background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb);
background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb);
background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb);
background-image: -o-linear-gradient(top, #dddddd, #bbbbbb);
background-image: linear-gradient(to bottom, #dddddd, #bbbbbb);
border: 1px solid #bbb;
border-bottom: 1px solid #999;
cursor: pointer;
text-shadow: 0 1px 0 #ddd;
}
.sp-container button:active {
border: 1px solid #aaa;
border-bottom: 1px solid #888;
-webkit-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
-moz-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
-ms-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
-o-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
}
.sp-cancel
{
font-size: 11px;
color: #d93f3f !important;
margin:0;
padding:2px;
margin-right: 5px;
vertical-align: middle;
text-decoration:none;
}
.sp-cancel:hover
{
color: #d93f3f !important;
text-decoration: underline;
}
.sp-palette span:hover, .sp-palette span.sp-thumb-active
{
border-color: #000;
}
.sp-preview, .sp-alpha, .sp-thumb-el
{
position:relative;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
}
.sp-preview-inner, .sp-alpha-inner, .sp-thumb-inner
{
display:block;
position:absolute;
top:0;left:0;bottom:0;right:0;
}
.sp-palette .sp-thumb-inner
{
background-position: 50% 50%;
background-repeat: no-repeat;
}
.sp-palette .sp-thumb-light.sp-thumb-active .sp-thumb-inner
{
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIVJREFUeNpiYBhsgJFMffxAXABlN5JruT4Q3wfi/0DsT64h8UD8HmpIPCWG/KemIfOJCUB+Aoacx6EGBZyHBqI+WsDCwuQ9mhxeg2A210Ntfo8klk9sOMijaURm7yc1UP2RNCMbKE9ODK1HM6iegYLkfx8pligC9lCD7KmRof0ZhjQACDAAceovrtpVBRkAAAAASUVORK5CYII=);
}
.sp-palette .sp-thumb-dark.sp-thumb-active .sp-thumb-inner
{
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAMdJREFUOE+tkgsNwzAMRMugEAahEAahEAZhEAqlEAZhEAohEAYh81X2dIm8fKpEspLGvudPOsUYpxE2BIJCroJmEW9qJ+MKaBFhEMNabSy9oIcIPwrB+afvAUFoK4H0tMaQ3XtlrggDhOVVMuT4E5MMG0FBbCEYzjYT7OxLEvIHQLY2zWwQ3D+9luyOQTfKDiFD3iUIfPk8VqrKjgAiSfGFPecrg6HN6m/iBcwiDAo7WiBeawa+Kwh7tZoSCGLMqwlSAzVDhoK+6vH4G0P5wdkAAAAASUVORK5CYII=);
}
.sp-clear-display {
background-repeat:no-repeat;
background-position: center;
background-image: url(data:image/gif;base64,R0lGODlhFAAUAPcAAAAAAJmZmZ2dnZ6enqKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq/Hx8fLy8vT09PX19ff39/j4+Pn5+fr6+vv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAAUABQAAAihAP9FoPCvoMGDBy08+EdhQAIJCCMybCDAAYUEARBAlFiQQoMABQhKUJBxY0SPICEYHBnggEmDKAuoPMjS5cGYMxHW3IiT478JJA8M/CjTZ0GgLRekNGpwAsYABHIypcAgQMsITDtWJYBR6NSqMico9cqR6tKfY7GeBCuVwlipDNmefAtTrkSzB1RaIAoXodsABiZAEFB06gIBWC1mLVgBa0AAOw==);
}

View File

@@ -9,6 +9,7 @@
<title>Grafana</title>
<link rel="stylesheet" href="css/bootstrap.dark.min.css" title="Light">
<link rel="stylesheet" href="css/timepicker.css">
<link rel="stylesheet" href="css/spectrum.css">
<link rel="stylesheet" href="css/animate.min.css">
<link rel="stylesheet" href="css/normalize.min.css">
<!-- load the root require context -->

View File

@@ -0,0 +1,26 @@
define([
'angular',
'angularMocks',
'panels/graphite/module'
], function(angular) {
/* describe('controller', function() {
var scope, metricCtrl;
beforeEach(function() {
angular.mock.inject(function($rootScope, $controller) {
scope = $rootScope.$new();
metricCtrl = $controller('kibana.panels.graphite.graphite', {
$scope: scope
});
});
});
it('should work', function() {
metricCtrl.toggleYAxis({alias:'myAlias'});
scope.panel.aliasYAxis['myAlias'].should.be(2);
});
});*/
});

View File

@@ -1,8 +1,8 @@
define([
'app/services/graphite/gfunc'
'services/graphite/gfunc'
], function(gfunc) {
describe('when creating func instance from func namae', function() {
describe('when creating func instance from func names', function() {
it('should return func instance', function() {
var func = gfunc.createFuncInstance('sumSeries');
@@ -34,11 +34,33 @@ define([
});
describe('when rendering func instance', function() {
it('should handle single metric param', function() {
var func = gfunc.createFuncInstance('sumSeries');
expect(func.render('hello.metric')).to.equal("sumSeries(hello.metric)");
});
it('should handle metric param and int param and string param', function() {
var func = gfunc.createFuncInstance('groupByNode');
func.params[0] = 5;
func.params[1] = 'avg';
expect(func.render('hello.metric')).to.equal("groupByNode(hello.metric,5,'avg')");
});
it('should handle function with no metric param', function() {
var func = gfunc.createFuncInstance('randomWalk');
func.params[0] = 'test';
expect(func.render(undefined)).to.equal("randomWalk('test')");
});
});
describe('when requesting function categories', function() {
it('should return function categories', function() {
var catIndex = gfunc.getCategories();
expect(catIndex.Special.length).to.equal(3);
expect(catIndex.Special.length).to.be.greaterThan(8);
});
});

View File

@@ -1,5 +1,5 @@
define([
'app/services/graphite/lexer'
'services/graphite/lexer'
], function(Lexer) {
describe('when lexing graphite expression', function() {
@@ -21,21 +21,50 @@ define([
expect(tokens[4].value).to.be('se1-server-*');
});
it('should tokenize functions and args', function() {
var lexer = new Lexer("sum(metric.test, 12, 'test')");
it('should tokenize metric expression with dash2', function() {
var lexer = new Lexer('net.192-168-1-1.192-168-1-9.ping_value.*');
var tokens = lexer.tokenize();
expect(tokens[0].value).to.be('sum');
expect(tokens[0].type).to.be('identifier');
expect(tokens[1].value).to.be('(');
expect(tokens[1].type).to.be('(');
expect(tokens[5].type).to.be(',');
expect(tokens[5].value).to.be(',');
expect(tokens[0].value).to.be('net');
expect(tokens[2].value).to.be('192-168-1-1');
});
it('simple function2', function() {
var lexer = new Lexer('offset(test.metric, -100)');
var tokens = lexer.tokenize();
expect(tokens[2].type).to.be('identifier');
expect(tokens[4].type).to.be('identifier');
expect(tokens[6].type).to.be('number');
});
it('should tokenize metric expression with curly braces', function() {
var lexer = new Lexer('metric.se1-{first, second}.count');
var tokens = lexer.tokenize();
expect(tokens.length).to.be(10);
expect(tokens[3].type).to.be('{');
expect(tokens[4].value).to.be('first');
expect(tokens[5].value).to.be(',');
expect(tokens[6].value).to.be('second');
});
it('should tokenize metric expression with number segments', function() {
var lexer = new Lexer("metric.10.12_10.test");
var tokens = lexer.tokenize();
expect(tokens[0].type).to.be('identifier');
expect(tokens[2].type).to.be('identifier');
expect(tokens[2].value).to.be('10');
expect(tokens[4].value).to.be('12_10');
expect(tokens[4].type).to.be('identifier');
});
it('should tokenize func call with numbered metric and number arg', function() {
var lexer = new Lexer("scale(metric.10, 15)");
var tokens = lexer.tokenize();
expect(tokens[0].type).to.be('identifier');
expect(tokens[2].type).to.be('identifier');
expect(tokens[2].value).to.be('metric');
expect(tokens[4].value).to.be('10');
expect(tokens[4].type).to.be('number');
expect(tokens[6].type).to.be('number');
expect(tokens[6].value).to.be('12');
expect(tokens[8].type).to.be('string');
expect(tokens[8].value).to.be('test');
expect(tokens[tokens.length - 1].value).to.be(')');
});
it('should tokenize metric with template parameter', function() {

View File

@@ -1,5 +1,5 @@
define([
'app/services/graphite/parser'
'services/graphite/parser'
], function(Parser) {
describe('when parsing', function() {
@@ -13,6 +13,35 @@ define([
expect(rootNode.segments[0].value).to.be('metric');
});
it('simple metric expression with numbers in segments', function() {
var parser = new Parser('metric.10.15_20.5');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('metric');
expect(rootNode.segments.length).to.be(4);
expect(rootNode.segments[1].value).to.be('10');
expect(rootNode.segments[2].value).to.be('15_20');
expect(rootNode.segments[3].value).to.be('5');
});
it('simple metric expression with curly braces', function() {
var parser = new Parser('metric.se1-{count, max}');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('metric');
expect(rootNode.segments.length).to.be(2);
expect(rootNode.segments[1].value).to.be('se1-{count,max}');
});
it('simple metric expression with curly braces at start of segment and with post chars', function() {
var parser = new Parser('metric.{count, max}-something.count');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('metric');
expect(rootNode.segments.length).to.be(3);
expect(rootNode.segments[1].value).to.be('{count,max}-something');
});
it('simple function', function() {
var parser = new Parser('sum(test)');
var rootNode = parser.getAst();
@@ -20,6 +49,22 @@ define([
expect(rootNode.params.length).to.be(1);
});
it('simple function2', function() {
var parser = new Parser('offset(test.metric, -100)');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('function');
expect(rootNode.params[0].type).to.be('metric');
expect(rootNode.params[1].type).to.be('number');
});
it('simple function with string arg', function() {
var parser = new Parser("randomWalk('test')");
var rootNode = parser.getAst();
expect(rootNode.type).to.be('function');
expect(rootNode.params.length).to.be(1);
expect(rootNode.params[0].type).to.be('string');
});
it('function with multiple args', function() {
var parser = new Parser("sum(test, 1, 'test')");
var rootNode = parser.getAst();
@@ -88,6 +133,13 @@ define([
expect(rootNode.pos).to.be(11);
});
it('handle issue #69', function() {
var parser = new Parser('cactiStyle(offset(scale(net.192-168-1-1.192-168-1-9.ping_value.*,0.001),-100))');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('function');
});
});
});

View File

@@ -1,22 +1,111 @@
require.config({
baseUrl: 'base',
baseUrl: 'base/app',
paths: {
underscore: 'app/components/underscore.extended',
'underscore-src': 'vendor/underscore',
specs: '../test/specs',
config: '../config.sample',
kbn: 'components/kbn',
settings: 'components/settings',
crypto: '../vendor/crypto.min',
underscore: 'components/underscore.extended',
'underscore-src': '../vendor/underscore',
moment: '../vendor/moment',
chromath: '../vendor/chromath',
angular: '../vendor/angular/angular',
angularMocks: '../vendor/angular/angular-mocks',
'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',
jquery: '../vendor/jquery/jquery-1.8.0',
bootstrap: '../vendor/bootstrap/bootstrap',
bindonce: '../vendor/angular/bindonce',
'jquery-ui': '../vendor/jquery/jquery-ui-1.10.3',
'extend-jquery': 'components/extend-jquery',
'jquery.flot': '../vendor/jquery/jquery.flot',
'jquery.flot.pie': '../vendor/jquery/jquery.flot.pie',
'jquery.flot.events': '../vendor/jquery/jquery.flot.events',
'jquery.flot.selection': '../vendor/jquery/jquery.flot.selection',
'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',
modernizr: '../vendor/modernizr-2.6.1',
elasticjs: '../vendor/elasticjs/elastic-angular-client',
},
shim: {
underscore: {
exports: '_'
},
bootstrap: {
deps: ['jquery']
},
modernizr: {
exports: 'Modernizr'
},
angular: {
deps: ['jquery', 'config'],
exports: 'angular'
},
angularMocks: {
deps: ['angular'],
},
crypto: {
exports: 'Crypto'
},
'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'],
'angular-mocks': ['angular'],
'angular-resource': ['angular'],
'angular-route': ['angular'],
'angular-touch': ['angular'],
'bindonce': ['angular'],
'angular-strap': ['angular', 'bootstrap','timepicker', 'datepicker'],
timepicker: ['jquery', 'bootstrap'],
datepicker: ['jquery', 'bootstrap'],
elasticjs: ['angular', '../vendor/elasticjs/elastic'],
}
});
require([
'test/specs/lexer-specs',
'test/specs/parser-specs',
'test/specs/gfunc-specs',
'specs/lexer-specs',
'specs/parser-specs',
'specs/gfunc-specs',
'specs/ctrl-specs',
], function () {
window.__karma__.start();
});

View File

@@ -1,7 +1,36 @@
/*
This has been modified from the default angular-draganddrop to provide the original
model and some other stuff as the 'data' arguement to callEventCallback
*/
/**
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/**
* Implementing Drag and Drop functionality in AngularJS is easier than ever.
* Demo: http://codef0rmer.github.com/angular-dragdrop/
*
* @version 1.0.4
*
* (c) 2013 Amit Gharat a.k.a codef0rmer <amit.2006.it@gmail.com> - amitgharat.wordpress.com
*/
/**
* This has been modified from the default angular-draganddrop to provide the original
* model and some other stuff as the 'data' arguement to callEventCallback
*/
(function (window, angular, undefined) {
'use strict';

1886
src/vendor/angular/angular-mocks.js vendored Normal file

File diff suppressed because it is too large Load Diff

10
src/vendor/crypto.min.js vendored Normal file
View File

@@ -0,0 +1,10 @@
/*
* Crypto-JS v2.5.3
* http://code.google.com/p/crypto-js/
* (c) 2009-2012 by Jeff Mott. All rights reserved.
* http://code.google.com/p/crypto-js/wiki/License
*/
(typeof Crypto=="undefined"||!Crypto.util)&&function(){var e=window.Crypto={},g=e.util={rotl:function(a,b){return a<<b|a>>>32-b},rotr:function(a,b){return a<<32-b|a>>>b},endian:function(a){if(a.constructor==Number)return g.rotl(a,8)&16711935|g.rotl(a,24)&4278255360;for(var b=0;b<a.length;b++)a[b]=g.endian(a[b]);return a},randomBytes:function(a){for(var b=[];a>0;a--)b.push(Math.floor(Math.random()*256));return b},bytesToWords:function(a){for(var b=[],c=0,d=0;c<a.length;c++,d+=8)b[d>>>5]|=(a[c]&255)<<
24-d%32;return b},wordsToBytes:function(a){for(var b=[],c=0;c<a.length*32;c+=8)b.push(a[c>>>5]>>>24-c%32&255);return b},bytesToHex:function(a){for(var b=[],c=0;c<a.length;c++)b.push((a[c]>>>4).toString(16)),b.push((a[c]&15).toString(16));return b.join("")},hexToBytes:function(a){for(var b=[],c=0;c<a.length;c+=2)b.push(parseInt(a.substr(c,2),16));return b},bytesToBase64:function(a){if(typeof btoa=="function")return btoa(f.bytesToString(a));for(var b=[],c=0;c<a.length;c+=3)for(var d=a[c]<<16|a[c+1]<<
8|a[c+2],e=0;e<4;e++)c*8+e*6<=a.length*8?b.push("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(d>>>6*(3-e)&63)):b.push("=");return b.join("")},base64ToBytes:function(a){if(typeof atob=="function")return f.stringToBytes(atob(a));for(var a=a.replace(/[^A-Z0-9+\/]/ig,""),b=[],c=0,d=0;c<a.length;d=++c%4)d!=0&&b.push(("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".indexOf(a.charAt(c-1))&Math.pow(2,-2*d+8)-1)<<d*2|"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".indexOf(a.charAt(c))>>>
6-d*2);return b}},e=e.charenc={};e.UTF8={stringToBytes:function(a){return f.stringToBytes(unescape(encodeURIComponent(a)))},bytesToString:function(a){return decodeURIComponent(escape(f.bytesToString(a)))}};var f=e.Binary={stringToBytes:function(a){for(var b=[],c=0;c<a.length;c++)b.push(a.charCodeAt(c)&255);return b},bytesToString:function(a){for(var b=[],c=0;c<a.length;c++)b.push(String.fromCharCode(a[c]));return b.join("")}}}();

View File

@@ -13,7 +13,7 @@ be injected into your angular controllers.
angular.module('elasticjs.service', [])
.factory('ejsResource', ['$http', function ($http) {
return function (config) {
return function (config, basicAuth) {
var
@@ -43,6 +43,12 @@ angular.module('elasticjs.service', [])
config.server = '';
}
// set authentication header
if (basicAuth || config.basicAuth) {
config.headers = angular.extend( config.headers||{}, {
"Authorization": "Basic " + (basicAuth||config.basicAuth)
});
}
/* implement the elastic.js client interface for angular */
ejs.client = {
server: function (s) {

View File

@@ -349,34 +349,7 @@
function(){
var pos = $(this).offset();
/*// check if the mouse is not already over the event
if ($(this).data("bouncing") == false || $(this).data("bouncing") == undefined) {
// check the div is not already bouncing
if ($(this).position().top == $(this).data("top")) {
$(this).effect("bounce", {
times: 3
}, 300);
}
$(this).data("bouncing", true);
_showTooltip(pos.left + $(this).width() / 2, pos.top, $(this).data("event"));
}*/
_showTooltip(pos.left + $(this).width() / 2, pos.top, $(this).data("event"));
if (event.min != event.max) {
plot.setSelection({
xaxis: {
from: event.min,
to: event.max
},
yaxis: {
from: yaxis.min,
to: yaxis.max
}
});
}
},
// mouseleave
function(){

View File

@@ -0,0 +1,225 @@
/* Flot plugin for computing bottoms for filled line and bar charts.
Copyright (c) 2007-2013 IOLA and Ole Laursen.
Licensed under the MIT license.
The case: you've got two series that you want to fill the area between. In Flot
terms, you need to use one as the fill bottom of the other. You can specify the
bottom of each data point as the third coordinate manually, or you can use this
plugin to compute it for you.
In order to name the other series, you need to give it an id, like this:
var dataset = [
{ data: [ ... ], id: "foo" } , // use default bottom
{ data: [ ... ], fillBetween: "foo" }, // use first dataset as bottom
];
$.plot($("#placeholder"), dataset, { lines: { show: true, fill: true }});
As a convenience, if the id given is a number that doesn't appear as an id in
the series, it is interpreted as the index in the array instead (so fillBetween:
0 can also mean the first series).
Internally, the plugin modifies the datapoints in each series. For line series,
extra data points might be inserted through interpolation. Note that at points
where the bottom line is not defined (due to a null point or start/end of line),
the current line will show a gap too. The algorithm comes from the
jquery.flot.stack.js plugin, possibly some code could be shared.
*/
(function ( $ ) {
var options = {
series: {
fillBetween: null // or number
}
};
function init( plot ) {
function findBottomSeries( s, allseries ) {
var i;
for ( i = 0; i < allseries.length; ++i ) {
if ( allseries[ i ].id === s.fillBetween ) {
return allseries[ i ];
}
}
if ( typeof s.fillBetween === "number" ) {
if ( s.fillBetween < 0 || s.fillBetween >= allseries.length ) {
return null;
}
return allseries[ s.fillBetween ];
}
return null;
}
function computeFillBottoms( plot, s, datapoints ) {
if ( s.fillBetween == null ) {
return;
}
var other = findBottomSeries( s, plot.getData() );
if ( !other ) {
return;
}
var ps = datapoints.pointsize,
points = datapoints.points,
otherps = other.datapoints.pointsize,
otherpoints = other.datapoints.points,
newpoints = [],
px, py, intery, qx, qy, bottom,
withlines = s.lines.show,
withbottom = ps > 2 && datapoints.format[2].y,
withsteps = withlines && s.lines.steps,
fromgap = true,
i = 0,
j = 0,
l, m;
while ( true ) {
if ( i >= points.length ) {
break;
}
l = newpoints.length;
if ( points[ i ] == null ) {
// copy gaps
for ( m = 0; m < ps; ++m ) {
newpoints.push( points[ i + m ] );
}
i += ps;
} else if ( j >= otherpoints.length ) {
// for lines, we can't use the rest of the points
if ( !withlines ) {
for ( m = 0; m < ps; ++m ) {
newpoints.push( points[ i + m ] );
}
}
i += ps;
} else if ( otherpoints[ j ] == null ) {
// oops, got a gap
for ( m = 0; m < ps; ++m ) {
newpoints.push( null );
}
fromgap = true;
j += otherps;
} else {
// cases where we actually got two points
px = points[ i ];
py = points[ i + 1 ];
qx = otherpoints[ j ];
qy = otherpoints[ j + 1 ];
bottom = 0;
if ( px === qx ) {
for ( m = 0; m < ps; ++m ) {
newpoints.push( points[ i + m ] );
}
//newpoints[ l + 1 ] += qy;
bottom = qy;
i += ps;
j += otherps;
} else if ( px > qx ) {
// we got past point below, might need to
// insert interpolated extra point
if ( withlines && i > 0 && points[ i - ps ] != null ) {
intery = py + ( points[ i - ps + 1 ] - py ) * ( qx - px ) / ( points[ i - ps ] - px );
newpoints.push( qx );
newpoints.push( intery );
for ( m = 2; m < ps; ++m ) {
newpoints.push( points[ i + m ] );
}
bottom = qy;
}
j += otherps;
} else { // px < qx
// if we come from a gap, we just skip this point
if ( fromgap && withlines ) {
i += ps;
continue;
}
for ( m = 0; m < ps; ++m ) {
newpoints.push( points[ i + m ] );
}
// we might be able to interpolate a point below,
// this can give us a better y
if ( withlines && j > 0 && otherpoints[ j - otherps ] != null ) {
bottom = qy + ( otherpoints[ j - otherps + 1 ] - qy ) * ( px - qx ) / ( otherpoints[ j - otherps ] - qx );
}
//newpoints[l + 1] += bottom;
i += ps;
}
fromgap = false;
if ( l !== newpoints.length && withbottom ) {
newpoints[ l + 2 ] = bottom;
}
}
// maintain the line steps invariant
if ( withsteps && l !== newpoints.length && l > 0 &&
newpoints[ l ] !== null &&
newpoints[ l ] !== newpoints[ l - ps ] &&
newpoints[ l + 1 ] !== newpoints[ l - ps + 1 ] ) {
for (m = 0; m < ps; ++m) {
newpoints[ l + ps + m ] = newpoints[ l + m ];
}
newpoints[ l + 1 ] = newpoints[ l - ps + 1 ];
}
}
datapoints.points = newpoints;
}
plot.hooks.processDatapoints.push( computeFillBottoms );
}
$.plot.plugins.push({
init: init,
options: options,
name: "fillbetween",
version: "1.0"
});
})(jQuery);

74
src/vendor/license.json vendored Normal file
View File

@@ -0,0 +1,74 @@
{
"angular": {
"version":"1.1.5",
"license":"MIT"
},
"angular-dragdrop": {
"version":"1.0.4",
"license":"MIT"
},
"angular-strap": {
"version":"0.7.5",
"license":"MIT"
},
"bindonce": {
"version":"0.2.1",
"license":"MIT"
},
"datepicker": {
"version":"12/3/2013",
"license":"Apache 2.0"
},
"timepicker": {
"version":"0.2.6",
"license":"MIT"
},
"bootstrap": {
"version":"2.3.2",
"license":"Apache 2.0"
},
"elasticjs": {
"version":"1.1.1",
"license":"MIT"
},
"jquery": {
"version":"1.8.0",
"license":"MIT"
},
"jquery-ui": {
"version":"1.10.3",
"license":"MIT"
},
"flot": {
"version":"0.8.1",
"license":"MIT"
},
"require": {
"version":"2.1.8",
"license":"MIT"
},
"chromath": {
"version":"0.0.5",
"license":"MIT"
},
"filesaver": {
"version":"2013-01-23",
"license":"MIT"
},
"modernizr": {
"version":"2.6.1",
"license":"MIT"
},
"moment": {
"version":"2.1.0",
"license":"MIT"
},
"timezone": {
"version":"2010",
"license":"Apache 2"
},
"underscore": {
"version":"1.5.1",
"license":"MIT"
}
}

1194
src/vendor/moment.js vendored

File diff suppressed because it is too large Load Diff

2032
src/vendor/spectrum.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,503 @@
(function ($) {
"use strict";
var defaultOptions = {
tagClass: function(item) {
return 'label label-info';
},
itemValue: function(item) {
return item ? item.toString() : item;
},
itemText: function(item) {
return this.itemValue(item);
},
freeInput: true,
maxTags: undefined,
confirmKeys: [13],
onTagExists: function(item, $tag) {
$tag.hide().fadeIn();
}
};
/**
* Constructor function
*/
function TagsInput(element, options) {
this.itemsArray = [];
this.$element = $(element);
this.$element.hide();
this.isSelect = (element.tagName === 'SELECT');
this.multiple = (this.isSelect && element.hasAttribute('multiple'));
this.objectItems = options && options.itemValue;
this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : '';
this.inputSize = Math.max(1, this.placeholderText.length);
this.$container = $('<div class="bootstrap-tagsinput"></div>');
this.$input = $('<input size="' + this.inputSize + '" type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container);
this.$element.after(this.$container);
this.build(options);
}
TagsInput.prototype = {
constructor: TagsInput,
/**
* Adds the given item as a new tag. Pass true to dontPushVal to prevent
* updating the elements val()
*/
add: function(item, dontPushVal) {
var self = this;
if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags)
return;
// Ignore falsey values, except false
if (item !== false && !item)
return;
// Throw an error when trying to add an object while the itemValue option was not set
if (typeof item === "object" && !self.objectItems)
throw("Can't add objects when itemValue option is not set");
// Ignore strings only containg whitespace
if (item.toString().match(/^\s*$/))
return;
// If SELECT but not multiple, remove current tag
if (self.isSelect && !self.multiple && self.itemsArray.length > 0)
self.remove(self.itemsArray[0]);
if (typeof item === "string" && this.$element[0].tagName === 'INPUT') {
var items = item.split(',');
if (items.length > 1) {
for (var i = 0; i < items.length; i++) {
this.add(items[i], true);
}
if (!dontPushVal)
self.pushVal();
return;
}
}
var itemValue = self.options.itemValue(item),
itemText = self.options.itemText(item),
tagClass = self.options.tagClass(item);
// Ignore items allready added
var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0];
if (existing) {
// Invoke onTagExists
if (self.options.onTagExists) {
var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; });
self.options.onTagExists(item, $existingTag);
}
return;
}
// register item in internal array and map
self.itemsArray.push(item);
// add a tag element
var $tag = $('<span class="tag ' + htmlEncode(tagClass) + '">' + htmlEncode(itemText) + '<span data-role="remove"></span></span>');
$tag.data('item', item);
self.findInputWrapper().before($tag);
$tag.after(' ');
// add <option /> if item represents a value not present in one of the <select />'s options
if (self.isSelect && !$('option[value="' + escape(itemValue) + '"]',self.$element)[0]) {
var $option = $('<option selected>' + htmlEncode(itemText) + '</option>');
$option.data('item', item);
$option.attr('value', itemValue);
self.$element.append($option);
}
if (!dontPushVal)
self.pushVal();
// Add class when reached maxTags
if (self.options.maxTags === self.itemsArray.length)
self.$container.addClass('bootstrap-tagsinput-max');
self.$element.trigger($.Event('itemAdded', { item: item }));
},
/**
* Removes the given item. Pass true to dontPushVal to prevent updating the
* elements val()
*/
remove: function(item, dontPushVal) {
var self = this;
if (self.objectItems) {
if (typeof item === "object")
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } )[0];
else
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } )[0];
}
if (item) {
$('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove();
$('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove();
self.itemsArray.splice($.inArray(item, self.itemsArray), 1);
}
if (!dontPushVal)
self.pushVal();
// Remove class when reached maxTags
if (self.options.maxTags > self.itemsArray.length)
self.$container.removeClass('bootstrap-tagsinput-max');
self.$element.trigger($.Event('itemRemoved', { item: item }));
},
/**
* Removes all items
*/
removeAll: function() {
var self = this;
$('.tag', self.$container).remove();
$('option', self.$element).remove();
while(self.itemsArray.length > 0)
self.itemsArray.pop();
self.pushVal();
if (self.options.maxTags && !this.isEnabled())
this.enable();
},
/**
* Refreshes the tags so they match the text/value of their corresponding
* item.
*/
refresh: function() {
var self = this;
$('.tag', self.$container).each(function() {
var $tag = $(this),
item = $tag.data('item'),
itemValue = self.options.itemValue(item),
itemText = self.options.itemText(item),
tagClass = self.options.tagClass(item);
// Update tag's class and inner text
$tag.attr('class', null);
$tag.addClass('tag ' + htmlEncode(tagClass));
$tag.contents().filter(function() {
return this.nodeType == 3;
})[0].nodeValue = htmlEncode(itemText);
if (self.isSelect) {
var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; });
option.attr('value', itemValue);
}
});
},
/**
* Returns the items added as tags
*/
items: function() {
return this.itemsArray;
},
/**
* Assembly value by retrieving the value of each item, and set it on the
* element.
*/
pushVal: function() {
var self = this,
val = $.map(self.items(), function(item) {
return self.options.itemValue(item).toString();
});
self.$element.val(val, true).trigger('change');
},
/**
* Initializes the tags input behaviour on the element
*/
build: function(options) {
var self = this;
self.options = $.extend({}, defaultOptions, options);
var typeahead = self.options.typeahead || {};
// When itemValue is set, freeInput should always be false
if (self.objectItems)
self.options.freeInput = false;
makeOptionItemFunction(self.options, 'itemValue');
makeOptionItemFunction(self.options, 'itemText');
makeOptionItemFunction(self.options, 'tagClass');
// for backwards compatibility, self.options.source is deprecated
if (self.options.source)
typeahead.source = self.options.source;
if (typeahead.source && $.fn.typeahead) {
makeOptionFunction(typeahead, 'source');
self.$input.typeahead({
source: function (query, process) {
function processItems(items) {
var texts = [];
for (var i = 0; i < items.length; i++) {
var text = self.options.itemText(items[i]);
map[text] = items[i];
texts.push(text);
}
process(texts);
}
this.map = {};
var map = this.map,
data = typeahead.source(query);
if ($.isFunction(data.success)) {
// support for Angular promises
data.success(processItems);
} else {
// support for functions and jquery promises
$.when(data)
.then(processItems);
}
},
updater: function (text) {
self.add(this.map[text]);
},
matcher: function (text) {
return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1);
},
sorter: function (texts) {
return texts.sort();
},
highlighter: function (text) {
var regex = new RegExp( '(' + this.query + ')', 'gi' );
return text.replace( regex, "<strong>$1</strong>" );
}
});
}
self.$container.on('click', $.proxy(function(event) {
self.$input.focus();
}, self));
self.$container.on('keydown', 'input', $.proxy(function(event) {
var $input = $(event.target),
$inputWrapper = self.findInputWrapper();
switch (event.which) {
// BACKSPACE
case 8:
if (doGetCaretPosition($input[0]) === 0) {
var prev = $inputWrapper.prev();
if (prev) {
self.remove(prev.data('item'));
}
}
break;
// DELETE
case 46:
if (doGetCaretPosition($input[0]) === 0) {
var next = $inputWrapper.next();
if (next) {
self.remove(next.data('item'));
}
}
break;
// LEFT ARROW
case 37:
// Try to move the input before the previous tag
var $prevTag = $inputWrapper.prev();
if ($input.val().length === 0 && $prevTag[0]) {
$prevTag.before($inputWrapper);
$input.focus();
}
break;
// RIGHT ARROW
case 39:
// Try to move the input after the next tag
var $nextTag = $inputWrapper.next();
if ($input.val().length === 0 && $nextTag[0]) {
$nextTag.after($inputWrapper);
$input.focus();
}
break;
default:
// When key corresponds one of the confirmKeys, add current input
// as a new tag
if (self.options.freeInput && $.inArray(event.which, self.options.confirmKeys) >= 0) {
self.add($input.val());
$input.val('');
event.preventDefault();
}
}
// Reset internal input's size
$input.attr('size', Math.max(this.inputSize, $input.val().length));
}, self));
// Remove icon clicked
self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
self.remove($(event.target).closest('.tag').data('item'));
}, self));
// Only add existing value as tags when using strings as tags
if (self.options.itemValue === defaultOptions.itemValue) {
if (self.$element[0].tagName === 'INPUT') {
self.add(self.$element.val());
} else {
$('option', self.$element).each(function() {
self.add($(this).attr('value'), true);
});
}
}
},
/**
* Removes all tagsinput behaviour and unregsiter all event handlers
*/
destroy: function() {
var self = this;
// Unbind events
self.$container.off('keypress', 'input');
self.$container.off('click', '[role=remove]');
self.$container.remove();
self.$element.removeData('tagsinput');
self.$element.show();
},
/**
* Sets focus on the tagsinput
*/
focus: function() {
this.$input.focus();
},
/**
* Returns the internal input element
*/
input: function() {
return this.$input;
},
/**
* Returns the element which is wrapped around the internal input. This
* is normally the $container, but typeahead.js moves the $input element.
*/
findInputWrapper: function() {
var elt = this.$input[0],
container = this.$container[0];
while(elt && elt.parentNode !== container)
elt = elt.parentNode;
return $(elt);
}
};
/**
* Register JQuery plugin
*/
$.fn.tagsinput = function(arg1, arg2) {
var results = [];
this.each(function() {
var tagsinput = $(this).data('tagsinput');
// Initialize a new tags input
if (!tagsinput) {
tagsinput = new TagsInput(this, arg1);
$(this).data('tagsinput', tagsinput);
results.push(tagsinput);
if (this.tagName === 'SELECT') {
$('option', $(this)).attr('selected', 'selected');
}
// Init tags from $(this).val()
$(this).val($(this).val());
} else {
// Invoke function on existing tags input
var retVal = tagsinput[arg1](arg2);
if (retVal !== undefined)
results.push(retVal);
}
});
if ( typeof arg1 == 'string') {
// Return the results from the invoked function calls
return results.length > 1 ? results : results[0];
} else {
return results;
}
};
$.fn.tagsinput.Constructor = TagsInput;
/**
* Most options support both a string or number as well as a function as
* option value. This function makes sure that the option with the given
* key in the given options is wrapped in a function
*/
function makeOptionItemFunction(options, key) {
if (typeof options[key] !== 'function') {
var propertyName = options[key];
options[key] = function(item) { return item[propertyName]; };
}
}
function makeOptionFunction(options, key) {
if (typeof options[key] !== 'function') {
var value = options[key];
options[key] = function() { return value; };
}
}
/**
* HtmlEncodes the given value
*/
var htmlEncodeContainer = $('<div />');
function htmlEncode(value) {
if (value) {
return htmlEncodeContainer.text(value).html();
} else {
return '';
}
}
/**
* Returns the position of the caret in the given input field
* http://flightschool.acylt.com/devnotes/caret-position-woes/
*/
function doGetCaretPosition(oField) {
var iCaretPos = 0;
if (document.selection) {
oField.focus ();
var oSel = document.selection.createRange();
oSel.moveStart ('character', -oField.value.length);
iCaretPos = oSel.text.length;
} else if (oField.selectionStart || oField.selectionStart == '0') {
iCaretPos = oField.selectionStart;
}
return (iCaretPos);
}
/**
* Initialize tagsinput behaviour on inputs and selects which have
* data-role=tagsinput
*/
$(function() {
$("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput();
});
})(window.jQuery);

View File

@@ -4,7 +4,7 @@ module.exports = function(grunt) {
grunt.registerTask('build', [
'jshint:source',
'clean:on_start',
'less:dist',
'less:src',
'copy:everything_but_less_to_temp',
'htmlmin:build',
'cssmin:build',
@@ -32,6 +32,10 @@ module.exports = function(grunt) {
{
pattern: /@REV@/,
replacement: desc.object
},
{
pattern: /@grafanaVersion@/,
replacement: 'v<%= pkg.version %>'
}
]
}

View File

@@ -4,7 +4,7 @@ module.exports = function(config) {
everything_but_less_to_temp: {
cwd: '<%= srcDir %>',
expand: true,
src: ['**/*', '!**/*.less'],
src: ['**/*', '!**/*.less', '!config.js'],
dest: '<%= tempDir %>'
}
};

View File

@@ -12,7 +12,7 @@ module.exports = function(config) {
// Compile in place when not building
src:{
options: {
paths: ["<%= srcDir %>/vendor/bootstrap/less"],
paths: ["<%= srcDir %>/vendor/bootstrap/less", "<%= srcDir %>/css/less"],
yuicompress:true
},
files: {

View File

@@ -12,6 +12,8 @@ module.exports = function(config,grunt) {
optimizeCss: 'none',
optimizeAllPluginResources: false,
paths: { config: '../config.sample' }, // fix, fallbacks need to be specified
removeCombined: true,
findNestedDependencies: true,
normalizeDirDefines: 'all',

View File

@@ -2,7 +2,7 @@ module.exports = function(config) {
return {
dest: {
expand: true,
src: ['**/*.js', '!config.js', '!app/dashboards/*.js', '!app/dashboards/**/*.js',],
src: ['**/*.js', '!config.sample.js', '!app/dashboards/*.js', '!app/dashboards/**/*.js',],
dest: '<%= destDir %>',
cwd: '<%= destDir %>',
options: {