Compare commits

..

43 Commits

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
65 changed files with 6208 additions and 196 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

View File

@@ -13,7 +13,7 @@ A beautiful, easy to use and feature rich Graphite dashboard replacement and gra
- 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.
@@ -33,7 +33,7 @@ A beautiful, easy to use and feature rich Graphite dashboard replacement and gra
- 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.

View File

@@ -4,7 +4,7 @@
"company": "Coding Instinct AB"
},
"name": "grafana",
"version": "1.0.4",
"version": "1.2.0",
"repository": {
"type": "git",
"url": "http://github.com/torkelo/grafana.git"

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

@@ -5,7 +5,7 @@ require.config({
baseUrl: 'app',
// urlArgs: 'r=@REV@',
paths: {
config: '../config',
config: ['../config', '../config.sample'],
settings: 'components/settings',
kbn: 'components/kbn',
@@ -22,6 +22,7 @@ require.config({
datepicker: '../vendor/angular/datepicker',
bindonce: '../vendor/angular/bindonce',
crypto: '../vendor/crypto.min',
spectrum: '../vendor/spectrum',
underscore: 'components/underscore.extended',
'underscore-src': '../vendor/underscore',
@@ -44,12 +45,18 @@ 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'
},
@@ -99,6 +106,7 @@ require.config({
elasticjs: ['angular', '../vendor/elasticjs/elastic'],
'bootstrap-tagsinput': ['jquery'],
},
waitSeconds: 60,
});

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, 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;

View File

@@ -103,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

@@ -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,25 +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.elasticsearch_dblist = function(queryStr) {
$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.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;
}
@@ -81,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: [] };
}
});
};
@@ -95,7 +147,8 @@ function (angular, _, config, $) {
}
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
$scope.elasticsearch_dblist("");
$scope.query.query = 'title:';
$scope.search();
};
$scope.addMetricToCurrentDashboard = function (metricId) {
@@ -125,8 +178,6 @@ function (angular, _, config, $) {
});
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

@@ -42,7 +42,6 @@
"span": 12,
"editable": true,
"type": "graphite",
"loadingEditor": false,
"x-axis": true,
"y-axis": true,
"scale": 1,

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

@@ -9,5 +9,7 @@ define([
'./tip',
'./confirmClick',
'./configModal',
'./grafanaGraph'
'./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

@@ -64,6 +64,8 @@ function (angular, $, kbn, moment, _) {
return;
}
var panel = scope.panel;
_.each(_.keys(scope.hiddenSeries), function(seriesAlias) {
var dataSeries = _.find(data, function(series) {
return series.info.alias === seriesAlias;
@@ -77,40 +79,40 @@ function (angular, $, kbn, moment, _) {
// Set barwidth based on specified interval
var barwidth = kbn.interval_to_ms(scope.interval);
var stack = scope.panel.stack ? true : null;
var stack = 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,
stackpercent: panel.stack ? panel.percentage : false,
stack: 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
show: panel.lines,
zero: false,
fill: panel.fill === 0 ? 0.001 : panel.fill/10,
lineWidth: panel.linewidth,
steps: panel.steppedLine
},
bars: {
show: scope.panel.bars,
show: panel.bars,
fill: 1,
barWidth: barwidth/1.5,
zero: false,
lineWidth: 0
},
points: {
show: scope.panel.points,
show: panel.points,
fill: 1,
fillColor: false,
radius: scope.panel.pointradius
radius: panel.pointradius
},
shadowSize: 1
},
yaxes: [],
xaxis: {
timezone: dashboard.current.timezone,
show: scope.panel['x-axis'],
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(),
@@ -130,10 +132,32 @@ function (angular, $, kbn, moment, _) {
}
};
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(scope.panel.nullPointMode);
var _d = data[i].getFlotPairs(panel.nullPointMode);
data[i].data = _d;
}
@@ -186,25 +210,26 @@ function (angular, $, kbn, moment, _) {
elem.html('<img src="' + url + '"></img>');
}
function addAnnotations(options) {
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"
}
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() {
@@ -246,7 +271,7 @@ function (angular, $, kbn, moment, _) {
}
if (format === 'short') {
axis.tickFormatter = function(val) {
return kbn.shortFormat(val,0);
return kbn.shortFormat(val, 1);
};
}
if (format === 'ms') {
@@ -320,4 +345,4 @@ function (angular, $, kbn, moment, _) {
};
});
});
});

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

@@ -10,11 +10,11 @@
</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[1]" ng-options="f for f in ['none','short','bytes', 'ms']" ng-change="render()"></select>
<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[2]" ng-options="f for f in ['none','short','bytes', 'ms']" ng-change="render()"></select>
<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">
@@ -45,6 +45,29 @@
</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">

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" />

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

@@ -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 : [],
@@ -109,7 +109,11 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
*/
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)'
},
annotate : {
@@ -221,7 +225,6 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
$scope.panel.tooltip.query_as_alias = true;
$scope.get_data();
};
$scope.remove_panel_from_row = function(row, panel) {
@@ -240,6 +243,8 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
$scope.updateTimeRange = function () {
$scope.range = filterSrv.timeRange();
$scope.rangeUnparsed = filterSrv.timeRange(false);
$scope.interval = '10m';
if ($scope.range) {
@@ -276,12 +281,14 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
$scope.updateTimeRange();
var graphiteQuery = {
range: filterSrv.timeRange(false),
range: $scope.rangeUnparsed,
targets: $scope.panel.targets,
renderer: $scope.panel.renderer,
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) {
@@ -324,7 +331,13 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
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() {

View File

@@ -10,13 +10,14 @@ function (_) {
this.datapoints = opts.datapoints;
this.info = opts.info;
this.label = opts.info.alias;
this.color = opts.info.color;
this.yaxis = opts.info.yaxis;
};
ts.ZeroFilled.prototype.getFlotPairs = function (fillStyle) {
var result = [];
this.color = this.info.color;
this.yaxis = this.info.yaxis;
this.info.total = 0;
this.info.max = null;
this.info.min = 212312321312;

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,13 +1,15 @@
<!-- 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>

View File

@@ -2,7 +2,7 @@
<div class="pull-right editor-title">Dashboard settings</div>
<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
<div ng-repeat="tab in ['General', 'Rows','Controls', 'Import']" data-title="{{tab}}">
<div ng-repeat="tab in ['General', 'Rows','Controls', 'Metrics', 'Import']" data-title="{{tab}}">
</div>
<div ng-repeat="tab in dashboard.current.nav" data-title="{{tab.type}}">
</div>
@@ -30,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">
@@ -133,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>
@@ -141,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,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

@@ -28,18 +28,36 @@
<i class="icon-th-large"></i>
New
</button>
<span>
<span class="position: relative;">
<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)" />
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="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"
<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>
@@ -54,10 +72,17 @@
</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>

View File

@@ -6,5 +6,6 @@ define([
'./panelMove',
'./graphite/graphiteSrv',
'./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,13 +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: {
@@ -119,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;
};
@@ -138,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() {
@@ -353,6 +365,7 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
user: 'guest',
group: 'guest',
title: save.title,
tags: save.tags,
dashboard: angular.toJson(save)
});
@@ -386,26 +399,6 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
);
};
this.elasticsearch_list = function(query, count) {
var request = ejs.Request().indices(config.grafana_index).types('dashboard');
// if elasticsearch has disabled _all field we need
// need to specifiy field here
var q = 'title:' + (query || '*');
return request.query(
ejs.QueryStringQuery(q)
).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

@@ -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

@@ -130,6 +130,11 @@ function (_) {
defaultParams: [10]
});
addFuncDef({
name: 'cactiStyle',
category: categories.Special,
});
addFuncDef({
name: 'scale',
category: categories.Transform,
@@ -137,6 +142,13 @@ function (_) {
defaultParams: [1]
});
addFuncDef({
name: 'offset',
category: categories.Transform,
params: [ { name: "amount", type: "int", } ],
defaultParams: [10]
});
addFuncDef({
name: 'integral',
category: categories.Transform,
@@ -164,8 +176,8 @@ 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({

View File

@@ -20,13 +20,13 @@ function (angular, _, $, config, kbn, moment) {
from: this.translateTime(options.range.from),
until: this.translateTime(options.range.to),
targets: options.targets,
renderer: options.renderer,
format: options.format,
maxDataPoints: options.maxDataPoints
};
var params = buildGraphiteParams(graphOptions);
if (options.renderer === 'png') {
if (options.format === 'png') {
return $q.when(graphiteRenderUrl + '?' + params.join('&'));
}
@@ -132,7 +132,7 @@ function (angular, _, $, config, kbn, moment) {
var clean_options = [];
var graphite_options = ['target', 'targets', 'from', 'until', 'rawData', 'format', 'maxDataPoints'];
if (options.renderer !== 'png') {
if (options.format !== 'png') {
options['format'] = 'json';
}

View File

@@ -402,13 +402,20 @@ define([
(ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z");
}
// 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

@@ -172,7 +172,7 @@ define([
return {
type: 'number',
value: parseInt(this.consumeToken().value, 10)
value: parseFloat(this.consumeToken().value)
};
},

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

@@ -1,3 +1,5 @@
@import "submenu.less";
@import "bootstrap-tagsinput.less";
.navbar-static-top {
border-bottom: 1px solid black;
@@ -10,6 +12,8 @@
}
}
// Search
.grafana-search-panel {
padding: 6px 10px;
@@ -35,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 {
@@ -402,4 +407,31 @@ input[type=text].func-param {
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

@@ -230,20 +230,6 @@ 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;
@@ -564,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,5 +1,5 @@
define([
'app/services/graphite/gfunc'
'services/graphite/gfunc'
], function(gfunc) {
describe('when creating func instance from func names', function() {
@@ -60,7 +60,7 @@ define([
it('should return function categories', function() {
var catIndex = gfunc.getCategories();
expect(catIndex.Special.length).to.equal(8);
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,6 +21,21 @@ define([
expect(tokens[4].value).to.be('se1-server-*');
});
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('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();

View File

@@ -1,5 +1,5 @@
define([
'app/services/graphite/parser'
'services/graphite/parser'
], function(Parser) {
describe('when parsing', function() {
@@ -49,6 +49,14 @@ 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();
@@ -125,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();
});

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

File diff suppressed because it is too large Load Diff

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);

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: {