Merge branch 'master' into feature/influxdb-add-where-conditions

Conflicts:
	src/app/partials/influxdb/editor.html
	src/app/services/influxdb/influxdbDatasource.js
This commit is contained in:
Marco Vito Moscaritolo 2014-04-21 16:32:05 +02:00
commit f98db943a5
61 changed files with 1255 additions and 352 deletions

View File

@ -1,5 +1,45 @@
# 1.5.0 (2013-03-09)
###New Features and improvements
# vNext
- InfluxDB enhancement: support for multiple hosts (with retries) and raw queries (Issue #318, thx @toddboom)
- Added rounding for graphites from and to time range filters
for very short absolute ranges (Issue #320)
# 1.5.3 (2014-04-17)
- Add support for async scripted dashboards (Issue #274)
- Text panel now accepts html (for links to other dashboards, etc) (Issue #236)
- Fix for Text panel, now changes take effect directly (Issue #251)
- Fix when adding functions without params that did not cause graph to update (Issue #267)
- Graphite errors are now much easier to see and troubleshoot with the new inspector (Issue #265)
- Use influxdb aliases to distinguish between multiple columns (Issue #283)
- Correction to ms axis formater, now formats days correctly. (Issue #189)
- Css fix for Firefox and using top menu dropdowns in panel fullscren / edit mode (Issue #106)
- Browser page title is now Grafana - {{dashboard title}} (Issue #294)
- Disable auto refresh zooming in (every time you change to an absolute time range), refresh will be restored when you change time range back to relative (Issue #282)
- More graphite functions
# 1.5.2 (2014-03-24)
### New Features and improvements
- Support for second optional params for functions like aliasByNode (Issue #167). Read the wiki on the [Function Editor](https://github.com/torkelo/grafana/wiki/Graphite-Function-Editor) for more info.
- More functions added to InfluxDB query editor (Issue #218)
- Filters can now be used inside other filters (templated segments) (Issue #128)
- More graphite functions added
### Fixes
- Float arguments now work for functions like scale (Issue #223)
- Fix for graphite function editor, the graph & target was not updated after adding a function and leaving default params as is #191
The zip files now contains a sub folder with project name and version prefix. (Issue #209)
# 1.5.1 (2014-03-10)
### Fixes
- maxDataPoints must be an integer #184 (thanks @frejsoya for fixing this)
For people who are find Grafana slow for large time spans or high resolution metrics. This is most likely due to graphite returning a large number of datapoints. The maxDataPoints parameter solves this issue. For maxDataPoints to work you need to run the latest graphite-web (some builds of 0.9.12 does not include this feature).
Read this for more info:
[Performance for large time spans](https://github.com/torkelo/grafana/wiki/Performance-for-large-time-spans)
# 1.5.0 (2014-03-09)
### New Features and improvements
- New function editor [video demo](http://youtu.be/I90WHRwE1ZM) (Issue #178)
- Links to function documentation from function editor (Issue #3)
- Reorder functions (Issue #130)
@ -17,8 +57,8 @@
- Basic Auth fix (Issue #152)
- Fix to annotations with graphite source & null values (Issue #138)
# 1.4.0 (2013-02-21)
###New Features
# 1.4.0 (2014-02-21)
### New Features
- #44 Annotations! Required a lot of work to get right. Read wiki article for more info. Supported annotations data sources are graphite metrics and graphite events. Support for more will be added in the future!
- #35 Support for multiple graphite servers! (Read wiki article for more)
- #116 Back to dashboard link in top menu to easily exist full screen / edit mode.
@ -35,7 +75,7 @@
- #104 Improvement to graphite target editor, select wildcard now gives you a "select metric" link for the next node.
- #105 Added zero as a possible node value in groupByAlias function
# 1.3.0 (2013-02-13)
# 1.3.0 (2014-02-13)
### New features or improvements
- #86 Dashboard tags and search (see wiki article for details)
- #54 Enhancement to filter / template. "Include All" improvement
@ -48,7 +88,7 @@
- #85 Added all parameters to summarize function
- #83 Stack as percent should now work a lot better!
# 1.2.0 (2013-02-10)
# 1.2.0 (2014-02-10)
### New features
- #70 Grid Thresholds (warning and error regions or lines in graph)
- #72 Added an example of a scripted dashboard and a short wiki article documenting scripted dashboards.
@ -62,7 +102,7 @@
- #67 Allow decimal input for scale function
- #68 Bug when trying to open dashboard while in edit mode
# 1.1.0 (2013-02-06)
# 1.1.0 (2014-02-06)
### New features:
- #22 Support for native graphite png renderer, does not support click and select zoom yet
@ -80,24 +120,24 @@
Thanks to everyone who contributed fixes and provided feedback :+1:
# 1.0.4 (2013-01-24)
# 1.0.4 (2014-01-24)
- Fixes #28 - Relative time range caused 500 graphite error in some cases (thx rsommer for the fix)
# 1.0.3 (2013-01-23)
# 1.0.3 (2014-01-23)
- #9 Add Y-axis format for milliseconds
- #16 Add support for Basic Auth (use http://username:password@yourgraphitedomain.com)
- #13 Relative time ranges now uses relative time ranges when issuing graphite query
# 1.0.2 (2013-01-21)
# 1.0.2 (2014-01-21)
- Fixes #12, should now work ok without ElasticSearch
# 1.0.1 (2013-01-21)
# 1.0.1 (2014-01-21)
- Resize fix
- Improvements to drag & drop
- Added a few graphite function definitions
- Fixed duplicate panel bug
- Updated default dashboard with welcome message and randomWalk graph
# 1.0.0 (2013-01-19)
# 1.0.0 (2014-01-19)
First public release
First public release

View File

@ -46,7 +46,7 @@ Grafana is very easy to install. It is a client side web app with no backend. An
# 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.
- Rename `config.sample.js` to `config.js`, then change `graphiteUrl` and `elasticsearch` to point to the correct urls. The urls entered here must be reachable by your browser.
- Point your browser to the installation.
To run from master:
@ -54,7 +54,9 @@ To run from master:
- Start a web server in src folder
- Or create a optimized & minified build:
- npm install (requires nodejs)
- grunt build
- grunt build (requires grunt-cli)
If you use ansible for provisioning and deployment [ansible-grafana](https://github.com/bobrik/ansible-grafana) should get you started.
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 and/or watch [this video](https://www.youtube.com/watch?v=OUvJamHeMpw) for a guide in creating a new dashboard and for creating
@ -67,6 +69,7 @@ Header set Access-Control-Allow-Origin "*"
Header set Access-Control-Allow-Methods "GET, OPTIONS"
Header set Access-Control-Allow-Headers "origin, authorization, accept"
```
Note that using "\*" leaves your graphite instance quite open so you might want to consider using "http://my.graphite-dom.ain" in place of "\*"
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:
@ -92,7 +95,6 @@ Header set Access-Control-Allow-Credentials true
- Use elasticsearch to search for metrics
- 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 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!
@ -106,4 +108,4 @@ Clone repository:
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": "1.5.1",
"version": "1.5.3",
"repository": {
"type": "git",
"url": "http://github.com/torkelo/grafana.git"
@ -13,13 +13,12 @@
"rjs-build-analysis": "0.0.3",
"grunt": "~0.4.0",
"grunt-ngmin": "0.0.3",
"grunt-contrib": "~0.8.0",
"grunt-contrib-less": "~0.7.0",
"grunt-contrib-copy": "~0.4.1",
"grunt-git-describe": "~2.3.2",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-cssmin": "~0.6.1",
"grunt-contrib-jshint": "~0.6.0",
"grunt-contrib-jshint": "~0.10.0",
"grunt-string-replace": "~0.2.4",
"grunt-contrib-htmlmin": "~0.1.3",
"grunt-contrib-requirejs": "~0.4.1",
@ -44,7 +43,8 @@
"grunt-karma": "~0.6.2",
"karma-mocha": "~0.1.1",
"karma-expect": "~1.0.0",
"grunt-cli": "~0.1.13"
"grunt-cli": "~0.1.13",
"jshint-stylish": "~0.1.5"
},
"engines": {
"node": "0.10.x",

View File

@ -430,28 +430,75 @@ function($, _, moment) {
ext = " B";
break;
case 1:
ext = " KB";
ext = " KiB";
break;
case 2:
ext = " MB";
ext = " MiB";
break;
case 3:
ext = " GB";
ext = " GiB";
break;
case 4:
ext = " TB";
ext = " TiB";
break;
case 5:
ext = " PB";
ext = " PiB";
break;
case 6:
ext = " EB";
ext = " EiB";
break;
case 7:
ext = " ZB";
ext = " ZiB";
break;
case 8:
ext = " YB";
ext = " YiB";
break;
}
return (size.toFixed(decimals) + ext);
};
kbn.bitFormat = function(size, decimals) {
var ext, steps = 0;
if(_.isUndefined(decimals)) {
decimals = 2;
} else if (decimals === 0) {
decimals = undefined;
}
while (Math.abs(size) >= 1024) {
steps++;
size /= 1024;
}
switch (steps) {
case 0:
ext = " b";
break;
case 1:
ext = " Kib";
break;
case 2:
ext = " Mib";
break;
case 3:
ext = " Gib";
break;
case 4:
ext = " Tib";
break;
case 5:
ext = " Pib";
break;
case 6:
ext = " Eib";
break;
case 7:
ext = " Zib";
break;
case 8:
ext = " Yib";
break;
}
@ -515,6 +562,10 @@ function($, _, moment) {
return function(val) {
return kbn.byteFormat(val, decimals);
};
case 'bits':
return function(val) {
return kbn.bitFormat(val, decimals);
};
case 'ms':
return function(val) {
return kbn.msFormat(val, decimals);
@ -546,18 +597,12 @@ function($, _, moment) {
else if (size < 86400000) {
return (size / 3600000).toFixed(decimals) + " hour";
}
// Less than one week, devide in days
else if (size < 604800000) {
// Less than one year, devide in days
else if (size < 31536000000) {
return (size / 86400000).toFixed(decimals) + " day";
}
// Less than one month, devide in weeks
else if (size < 2.62974e9) {
return (size / 604800000).toFixed(decimals) + " week";
}
// Less than one year, devide in weeks
else if (size < 3.15569e10) {
return (size / 2.62974e9).toFixed(decimals) + " year";
}
return (size / 31536000000).toFixed(decimals) + " year";
};
kbn.microsFormat = function(size, decimals) {

View File

@ -49,6 +49,11 @@ function (_, crypto) {
return datasource;
};
var parseMultipleHosts = function(datasource) {
datasource.urls = _.map(datasource.url.split(","), function (url) { return url.trim(); });
return datasource;
};
if (options.graphiteUrl) {
settings.datasources = {
graphite: {
@ -62,6 +67,7 @@ function (_, crypto) {
_.each(settings.datasources, function(datasource, key) {
datasource.name = key;
parseBasicAuth(datasource);
if (datasource.type === 'influxdb') { parseMultipleHosts(datasource); }
});
var elasticParsed = parseBasicAuth({ url: settings.elasticsearch });

View File

@ -10,4 +10,5 @@ define([
'./graphiteImport',
'./influxTargetCtrl',
'./playlistCtrl',
], function () {});
'./inspectCtrl',
], function () {});

View File

@ -49,7 +49,7 @@ function (angular, _, moment) {
$scope.set_default = function() {
if(dashboard.set_default($location.path())) {
alertSrv.set('Home Set','This page has been set as your default Kibana dashboard','success',5000);
alertSrv.set('Home Set','This page has been set as your default dashboard','success',5000);
} else {
alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000);
}
@ -57,7 +57,7 @@ function (angular, _, moment) {
$scope.purge_default = function() {
if(dashboard.purge_default()) {
alertSrv.set('Local Default Clear','Your Kibana default dashboard has been reset to the default',
alertSrv.set('Local Default Clear','Your default dashboard has been reset to the default',
'success',5000);
} else {
alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000);

View File

@ -12,12 +12,25 @@ function (angular, app, _) {
$scope.init = function() {
console.log('hej!');
$scope.datasources = datasourceSrv.listOptions();
$scope.setDatasource(null);
};
$scope.setDatasource = function(datasource) {
$scope.datasource = datasourceSrv.get(datasource);
if (!$scope.datasource) {
$scope.error = "Cannot find datasource " + datasource;
return;
}
};
$scope.listAll = function(query) {
delete $scope.error;
datasourceSrv.default.listDashboards(query)
$scope.datasource.listDashboards(query)
.then(function(results) {
$scope.dashboards = results;
})
@ -29,20 +42,20 @@ function (angular, app, _) {
$scope.import = function(dashName) {
delete $scope.error;
datasourceSrv.default.loadDashboard(dashName)
$scope.datasource.loadDashboard(dashName)
.then(function(results) {
if (!results.data || !results.data.state) {
throw { message: 'no dashboard state received from graphite' };
}
graphiteToGrafanaTranslator(results.data.state);
graphiteToGrafanaTranslator(results.data.state, $scope.datasource.name);
})
.then(null, function(err) {
$scope.error = err.message || 'Failed to import dashboard';
});
};
function graphiteToGrafanaTranslator(state) {
function graphiteToGrafanaTranslator(state, datasource) {
var graphsPerRow = 2;
var rowHeight = 300;
var rowTemplate;
@ -72,7 +85,8 @@ function (angular, app, _) {
type: 'graphite',
span: 12 / graphsPerRow,
title: graph[1].title,
targets: []
targets: [],
datasource: datasource
};
_.each(graph[1].target, function(target) {

View File

@ -234,7 +234,7 @@ function (angular, _, config, gfunc, Parser) {
$scope.moveAliasFuncLast();
$scope.smartlyHandleNewAliasByNode(newFunc);
if (!funcDef.params && newFunc.added) {
if (!funcDef.params.length && newFunc.added) {
$scope.targetChanged();
}
};

View File

@ -15,12 +15,23 @@ function (angular) {
$scope.target.function = 'mean';
}
$scope.rawQuery = false;
$scope.functions = ['count', 'mean', 'sum', 'min', 'max', 'mode', 'distinct', 'median', 'derivative', 'stddev', 'first', 'last'];
$scope.oldSeries = $scope.target.series;
$scope.$on('typeahead-updated', function(){
$timeout($scope.get_data);
});
};
$scope.showQuery = function () {
$scope.target.rawQuery = true;
};
$scope.hideQuery = function () {
$scope.target.rawQuery = false;
};
// Cannot use typeahead and ng-change on blur at the same time
$scope.seriesBlur = function() {
if ($scope.oldSeries !== $scope.target.series) {

View File

@ -0,0 +1,75 @@
define([
'angular'
],
function (angular) {
'use strict';
var module = angular.module('kibana.controllers');
module.controller('InspectCtrl', function($scope) {
var model = $scope.inspector;
function getParametersFromQueryString(queryString) {
var result = [];
var parameters = queryString.split("&");
for (var i = 0; i < parameters.length; i++) {
var keyValue = parameters[i].split("=");
if (keyValue[1].length > 0) {
result.push({ key: keyValue[0], value: window.unescape(keyValue[1]) });
}
}
return result;
}
$scope.init = function () {
$scope.editor = { index: 0 };
if (!model.error) {
return;
}
if (model.error.stack) {
$scope.editor.index = 2;
$scope.stack_trace = model.error.stack;
$scope.message = model.error.message;
}
else if (model.error.config && model.error.config.data) {
$scope.editor.index = 1;
$scope.request_parameters = getParametersFromQueryString(model.error.config.data);
if (model.error.data.indexOf('DOCTYPE') !== -1) {
$scope.response_html = model.error.data;
}
}
};
});
angular
.module('kibana.directives')
.directive('iframeContent', function($parse) {
return {
restrict: 'A',
link: function($scope, elem, attrs) {
var getter = $parse(attrs.iframeContent), value = getter($scope);
$scope.$on("$destroy",function() {
elem.remove();
});
var iframe = document.createElement('iframe');
iframe.width = '100%';
iframe.height = '400px';
iframe.style.border = 'none';
iframe.src = 'about:blank';
elem.append(iframe);
iframe.contentWindow.document.open('text/html', 'replace');
iframe.contentWindow.document.write(value);
iframe.contentWindow.document.close();
}
};
});
});

View File

@ -0,0 +1,134 @@
define([
'angular',
'underscore',
'jquery'
],
function (angular, _, $) {
'use strict';
// This function needs $inject annotations, update below
// when changing arguments to this function
function PanelBaseCtrl($scope, $rootScope, $timeout) {
var menu = [
{
text: 'Edit',
configModal: "app/partials/paneleditor.html",
condition: !$scope.panelMeta.fullscreenEdit
},
{
text: 'Edit',
click: "toggleFullscreenEdit()",
condition: $scope.panelMeta.fullscreenEdit
},
{
text: "Fullscreen",
click: 'toggleFullscreen()',
condition: $scope.panelMeta.fullscreenView
},
{
text: 'Duplicate',
click: 'duplicatePanel(panel)',
condition: true
},
{
text: 'Span',
submenu: [
{ text: '1', click: 'updateColumnSpan(1)' },
{ text: '2', click: 'updateColumnSpan(2)' },
{ text: '3', click: 'updateColumnSpan(3)' },
{ text: '4', click: 'updateColumnSpan(4)' },
{ text: '5', click: 'updateColumnSpan(5)' },
{ text: '6', click: 'updateColumnSpan(6)' },
{ text: '7', click: 'updateColumnSpan(7)' },
{ text: '8', click: 'updateColumnSpan(8)' },
{ text: '9', click: 'updateColumnSpan(9)' },
{ text: '10', click: 'updateColumnSpan(10)' },
{ text: '11', click: 'updateColumnSpan(11)' },
{ text: '12', click: 'updateColumnSpan(12)' },
],
condition: true
},
{
text: 'Remove',
click: 'remove_panel_from_row(row, panel)',
condition: true
}
];
$scope.inspector = {};
$scope.panelMeta.menu = _.where(menu, { condition: true });
$scope.updateColumnSpan = function(span) {
$scope.panel.span = span;
$timeout(function() {
$scope.$emit('render');
});
};
$scope.enterFullscreenMode = function(options) {
var docHeight = $(window).height();
var editHeight = Math.floor(docHeight * 0.3);
var fullscreenHeight = Math.floor(docHeight * 0.7);
var oldTimeRange = $scope.range;
$scope.height = options.edit ? editHeight : fullscreenHeight;
$scope.editMode = options.edit;
if (!$scope.fullscreen) {
var closeEditMode = $rootScope.$on('panel-fullscreen-exit', function() {
$scope.editMode = false;
$scope.fullscreen = false;
delete $scope.height;
closeEditMode();
$timeout(function() {
if (oldTimeRange !== $scope.range) {
$scope.dashboard.refresh();
}
else {
$scope.$emit('render');
}
});
});
}
$(window).scrollTop(0);
$scope.fullscreen = true;
$rootScope.$emit('panel-fullscreen-enter');
$timeout(function() {
$scope.$emit('render');
});
};
$scope.toggleFullscreenEdit = function() {
if ($scope.editMode) {
$rootScope.$emit('panel-fullscreen-exit');
return;
}
$scope.enterFullscreenMode({edit: true});
};
$scope.toggleFullscreen = function() {
if ($scope.fullscreen && !$scope.editMode) {
$rootScope.$emit('panel-fullscreen-exit');
return;
}
$scope.enterFullscreenMode({ edit: false });
};
}
PanelBaseCtrl['$inject'] = ['$scope', '$rootScope', '$timeout'];
return PanelBaseCtrl;
});

View File

@ -124,6 +124,18 @@ function (angular, app, _) {
*/
type : type
};
function fixRowHeight(height) {
if (!height) {
return '200px';
}
if (!_.isString(height)) {
return height + 'px';
}
return height;
}
$scope.row.height = fixRowHeight($scope.row.height);
};
/** @scratch /panels/2

View File

@ -5,18 +5,25 @@
* This script generates a dashboard object that Grafana can load. It also takes a number of user
* supplied URL parameters (int ARGS variable)
*
* Return a dashboard object, or a function
*
* For async scripts, return a function, this function must take a single callback function as argument,
* call this callback function with the dashboard object (look at scripted_async.js for an example)
*/
'use strict';
// accessable variables in this scope
var window, document, ARGS, $, jQuery, moment, kbn;
// Setup some variables
var dashboard, _d_timespan;
var dashboard, timspan;
// All url parameters are available via the ARGS object
var ARGS;
// Set a default timespan if one isn't specified
_d_timespan = '1d';
timspan = '1d';
// Intialize a skeleton with nothing but a rows array and service object
dashboard = {
@ -28,7 +35,7 @@ dashboard = {
dashboard.title = 'Scripted dash';
dashboard.services.filter = {
time: {
from: "now-"+(ARGS.from || _d_timespan),
from: "now-" + (ARGS.from || timspan),
to: "now"
}
};
@ -67,8 +74,7 @@ for (var i = 0; i < rows; i++) {
}
]
});
}
// Now return the object and we're good!
return dashboard;

View File

@ -0,0 +1,81 @@
/* 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)
*
* Global accessable variables
* window, document, $, jQuery, ARGS, moment
*
* Return a dashboard object, or a function
*
* For async scripts, return a function, this function must take a single callback function,
* call this function with the dasboard object
*/
'use strict';
// accessable variables in this scope
var window, document, ARGS, $, jQuery, moment, kbn;
return function(callback) {
// Setup some variables
var dashboard, timspan;
// Set a default timespan if one isn't specified
timspan = '1d';
// Intialize a skeleton with nothing but a rows array and service object
dashboard = {
rows : [],
services : {}
};
// Set a title
dashboard.title = 'Scripted dash';
dashboard.services.filter = {
time: {
from: "now-" + (ARGS.from || timspan),
to: "now"
}
};
var rows = 1;
var seriesName = 'argName';
if(!_.isUndefined(ARGS.rows)) {
rows = parseInt(ARGS.rows, 10);
}
if(!_.isUndefined(ARGS.name)) {
seriesName = ARGS.name;
}
$.ajax({
method: 'GET',
url: '/'
})
.done(function(result) {
dashboard.rows.push({
title: 'Chart',
height: '300px',
panels: [
{
title: 'Async dashboard test',
type: 'text',
span: 12,
fill: 1,
content: '# Async test'
}
]
});
// when dashboard is composed call the callback
// function and pass the dashboard
callback(dashboard);
});
}

View File

@ -45,21 +45,45 @@ function (angular, $, kbn, moment, _) {
function setElementHeight() {
try {
elem.css({ height: scope.height || scope.panel.height || scope.row.height });
var height = scope.height || scope.panel.height || scope.row.height;
if (_.isString(height)) {
height = parseInt(height.replace('px', ''), 10);
}
height = height - 32; // subtract panel title bar
if (scope.panel.legend.show) {
height = height - 21; // subtract one line legend
}
elem.css('height', height + 'px');
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; }
function shouldAbortRender() {
if (!data) {
return true;
}
if ($rootScope.fullscreen && !scope.fullscreen) {
return true;
}
if (!setElementHeight()) { return true; }
if (_.isString(data)) {
render_panel_as_graphite_png(data);
return true;
}
}
// Function for rendering panel
function render_panel() {
if (shouldAbortRender()) {
return;
}
@ -248,10 +272,7 @@ function (angular, $, kbn, moment, _) {
}
function configureAxisMode(axis, format) {
if (format === 'bytes') {
axis.mode = 'byte';
}
else if (format !== 'none') {
if (format !== 'none') {
axis.tickFormatter = kbn.getFormatFunction(format, 1);
}
}
@ -326,9 +347,9 @@ function (angular, $, kbn, moment, _) {
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.legend.show ? '&hideLegend=false' : '&hideLegend=true';
url += scope.panel.grid.min !== null ? '&yMin=' + scope.panel.grid.min : '';
url += scope.panel.grid.max !== null ? '&yMax=' + scope.panel.grid.max : '';
url += scope.panel['x-axis'] ? '' : '&hideAxes=true';
url += scope.panel['y-axis'] ? '' : '&hideYAxis=true';
@ -336,6 +357,9 @@ function (angular, $, kbn, moment, _) {
case 'bytes':
url += '&yUnitSystem=binary';
break;
case 'bits':
url += '&yUnitSystem=binary';
break;
case 'short':
url += '&yUnitSystem=si';
break;

View File

@ -29,6 +29,8 @@ function (angular, _, $) {
var $funcControls = $(funcControlsTemplate);
var func = $scope.func;
var funcDef = func.def;
var scheduledRelink = false;
var paramCountAtLink = 0;
function clickFuncParam(paramIndex) {
/*jshint validthis:true */
@ -51,17 +53,33 @@ function (angular, _, $) {
}
}
function scheduledRelinkIfNeeded() {
if (paramCountAtLink === func.params.length) {
return;
}
if (!scheduledRelink) {
scheduledRelink = true;
setTimeout(function() {
relink();
scheduledRelink = false;
}, 200);
}
}
function inputBlur(paramIndex) {
/*jshint validthis:true */
var $input = $(this);
var $link = $input.prev();
if ($input.val() !== '') {
if ($input.val() !== '' || func.def.params[paramIndex].optional) {
$link.text($input.val());
func.updateParam($input.val(), paramIndex);
$scope.$apply($scope.targetChanged);
scheduledRelinkIfNeeded();
$scope.$apply($scope.targetChanged);
}
$input.hide();
@ -129,9 +147,19 @@ function (angular, _, $) {
$funcLink.appendTo(elem);
_.each(funcDef.params, function(param, index) {
if (param.optional && !func.params[index]) {
return;
}
if (index > 0) {
$('<span>, </span>').appendTo(elem);
}
var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + func.params[index] + '</a>');
var $input = $(paramTemplate);
paramCountAtLink++;
$paramLink.appendTo(elem);
$input.appendTo(elem);
@ -140,10 +168,6 @@ function (angular, _, $) {
$input.keypress(_.partial(inputKeyPress, index));
$paramLink.click(_.partial(clickFuncParam, index));
if (index !== funcDef.params.length - 1) {
$('<span>, </span>').appendTo(elem);
}
if (funcDef.params[index].options) {
addTypeahead($input, index);
}
@ -200,10 +224,16 @@ function (angular, _, $) {
});
}
addElementsAndCompile();
ifJustAddedFocusFistParam();
registerFuncControlsToggle();
registerFuncControlsActions();
function relink() {
elem.children().remove();
addElementsAndCompile();
ifJustAddedFocusFistParam();
registerFuncControlsToggle();
registerFuncControlsActions();
}
relink();
}
};

View File

@ -1,14 +1,15 @@
define([
'angular',
'jquery',
'underscore'
'underscore',
'../controllers/panelBaseCtrl'
],
function (angular, $, _) {
function (angular, $, _, PanelBaseCtrl) {
'use strict';
angular
.module('kibana.directives')
.directive('kibanaPanel', function($compile, $timeout, $rootScope) {
.directive('kibanaPanel', function($compile, $timeout, $rootScope, $injector) {
var container = '<div class="panel-container"></div>';
var content = '<div class="panel-content"></div>';
@ -16,9 +17,10 @@ function (angular, $, _) {
var panelHeader =
'<div class="panel-header">'+
'<div class="row-fluid">' +
'<div class="span12 alert-error panel-error" ng-hide="!panel.error">' +
'<div class="span12 alert-error panel-error small" ng-show="panel.error">' +
'<a class="close" ng-click="panel.error=false">&times;</a>' +
'<i class="icon-exclamation-sign"></i> <strong>Oops!</strong> {{panel.error}}' +
'<span><i class="icon-exclamation-sign"></i> <strong>Oops!</strong> {{panel.error}} </span>' +
'<span class="pointer panel-error-inspector-link" config-modal="app/partials/inspector.html">View details</span>' +
'</div>' +
'</div>\n' +
@ -73,11 +75,15 @@ function (angular, $, _) {
elem.removeClass("ng-cloak");
}
newScope.$on('$destroy',function(){
newScope.$on('$destroy',function() {
elem.unbind();
elem.remove();
});
newScope.initBaseController = function(self, scope) {
$injector.invoke(PanelBaseCtrl, self, { $scope: scope });
};
$scope.$watch(attr.type, function (name) {
elem.addClass("ng-cloak");
// load the panels module file, then render it in the dom.
@ -106,124 +112,6 @@ function (angular, $, _) {
});
/*
/* Panel base functionality
/* */
newScope.initPanel = function(scope) {
scope.updateColumnSpan = function(span) {
scope.panel.span = span;
$timeout(function() {
scope.$emit('render');
});
};
function enterFullscreenMode(options) {
var docHeight = $(window).height();
var editHeight = Math.floor(docHeight * 0.3);
var fullscreenHeight = Math.floor(docHeight * 0.7);
var oldTimeRange = scope.range;
scope.height = options.edit ? editHeight : fullscreenHeight;
scope.editMode = options.edit;
if (!scope.fullscreen) {
var closeEditMode = $rootScope.$on('panel-fullscreen-exit', function() {
scope.editMode = false;
scope.fullscreen = false;
delete scope.height;
closeEditMode();
$timeout(function() {
if (oldTimeRange !== $scope.range) {
scope.dashboard.refresh();
}
else {
scope.$emit('render');
}
});
});
}
$(window).scrollTop(0);
scope.fullscreen = true;
$rootScope.$emit('panel-fullscreen-enter');
$timeout(function() {
scope.$emit('render');
});
}
scope.toggleFullscreenEdit = function() {
if (scope.editMode) {
$rootScope.$emit('panel-fullscreen-exit');
return;
}
enterFullscreenMode({edit: true});
};
$scope.toggleFullscreen = function() {
if (scope.fullscreen && !scope.editMode) {
$rootScope.$emit('panel-fullscreen-exit');
return;
}
enterFullscreenMode({ edit: false });
};
var menu = [
{
text: 'Edit',
configModal: "app/partials/paneleditor.html",
condition: !scope.panelMeta.fullscreenEdit
},
{
text: 'Edit',
click: "toggleFullscreenEdit()",
condition: scope.panelMeta.fullscreenEdit
},
{
text: "Fullscreen",
click: 'toggleFullscreen()',
condition: scope.panelMeta.fullscreenView
},
{
text: 'Duplicate',
click: 'duplicatePanel(panel)',
condition: true
},
{
text: 'Span',
submenu: [
{ text: '1', click: 'updateColumnSpan(1)' },
{ text: '2', click: 'updateColumnSpan(2)' },
{ text: '3', click: 'updateColumnSpan(3)' },
{ text: '4', click: 'updateColumnSpan(4)' },
{ text: '5', click: 'updateColumnSpan(5)' },
{ text: '6', click: 'updateColumnSpan(6)' },
{ text: '7', click: 'updateColumnSpan(7)' },
{ text: '8', click: 'updateColumnSpan(8)' },
{ text: '9', click: 'updateColumnSpan(9)' },
{ text: '10', click: 'updateColumnSpan(10)' },
{ text: '11', click: 'updateColumnSpan(11)' },
{ text: '12', click: 'updateColumnSpan(12)' },
],
condition: true
},
{
text: 'Remove',
click: 'remove_panel_from_row(row, panel)',
condition: true
}
];
scope.panelMeta.menu = _.where(menu, { condition: true });
};
}
};
});

View File

@ -2,7 +2,7 @@
<div class='filtering-container'>
<div ng-repeat="filter in filterList" class="small filter-panel-filter">
<div ng-repeat="filter in filterSrv.list" class="small filter-panel-filter">
<div>
<i class="filter-action pointer icon-remove" bs-tooltip="'Remove'" ng-click="remove(filter)"></i>
<i class="filter-action pointer icon-edit" ng-hide="filter.editing" bs-tooltip="'Edit'" ng-click="filter.editing = true"></i>

View File

@ -27,7 +27,7 @@ function (angular, app, _) {
_.defaults($scope.panel,_d);
$scope.init = function() {
$scope.filterList = filterSrv.list;
$scope.filterSrv = filterSrv;
};
$scope.remove = function(filter) {
@ -40,7 +40,7 @@ function (angular, app, _) {
};
$scope.applyFilterToOtherFilters = function(updatedFilter) {
_.each($scope.filterList, function(filter) {
_.each(filterSrv.list, function(filter) {
if (filter === updatedFilter) {
return;
}

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[0]" ng-options="f for f in ['none','short','bytes', 'ms', 'µs']" ng-change="render()"></select>
<select class="input-small" ng-model="panel.y_formats[0]" ng-options="f for f in ['none','short','bytes', 'bits', 'ms', 'µs']" 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', 'µs']" ng-change="render()"></select>
<select class="input-small" ng-model="panel.y_formats[1]" ng-options="f for f in ['none','short','bytes', 'bits', 'ms', 'µs']" ng-change="render()"></select>
</div>
<div class="editor-option">
@ -102,4 +102,4 @@
</div>
</div>
</div>

View File

@ -18,6 +18,7 @@
<div ng-if="panel.legend" class="grafana-legend-container">
<div ng-include="'app/panels/graphite/legend.html'"></div>
</div>
<div class="clearfix"></div>
<div class="panel-full-edit-tabs" ng-if="editMode">
<div ng-model="editor.index" bs-tabs>

View File

@ -84,7 +84,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
*/
scale : 1,
/** @scratch /panels/histogram/3
* y_formats :: 'none','bytes','short', 'ms'
* y_formats :: 'none','bytes','bits','short', 'ms'
*/
y_formats : ['short', 'short'],
/** @scratch /panels/histogram/5
@ -140,7 +140,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
*/
stack : false,
/** @scratch /panels/histogram/3
* legend:: Display the legond
* legend:: Display the legend
*/
legend: {
show: true, // disable/enable legend
@ -199,7 +199,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
}
$scope.init = function() {
$scope.initPanel($scope);
$scope.initBaseController(this, $scope);
$scope.fullscreen = false;
$scope.editor = { index: 1 };
@ -261,7 +261,10 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
return $scope.datasource.query(graphiteQuery)
.then($scope.dataHandler)
.then(null, function(err) {
$scope.panelMeta.loading = false;
$scope.panel.error = err.message || "Graphite HTTP Request Error";
$scope.inspector.error = err;
$scope.render([]);
});
};
@ -318,9 +321,9 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
if (last - from < -10000) {
$scope.datapointsOutside = true;
}
}
$scope.datapointsCount += datapoints.length;
$scope.datapointsCount += datapoints.length;
}
return series;
};

View File

@ -8,9 +8,11 @@
</div>
</div>
<label class=small>Content
<label class=small>Content
<span ng-show="panel.mode == 'html'">(This area uses HTML sanitized via AngularJS's <a href='http://docs.angularjs.org/api/ngSanitize.$sanitize'>$sanitize</a> service)</span>
<span ng-show="panel.mode == 'markdown'">(This area uses <a target="_blank" href="http://en.wikipedia.org/wiki/Markdown">Markdown</a>. HTML is not supported)</span>
</label>
<textarea ng-model="panel.content" rows="6" style="width:95%"></textarea>
<textarea ng-model="panel.content" rows="6" style="width:95%" ng-change="render()" ng-model-onblur>
</textarea>
</div>

View File

@ -1,10 +1,10 @@
<div ng-controller='text' ng-init="init()">
<div ng-controller='text' ng-init="init()" style="min-height:{{panel.height || row.height}}" ng-dblclick="openEditor()">
<!--<p ng-style="panel.style" ng-bind-html-unsafe="panel.content | striphtml | newlines"></p>-->
<markdown ng-show="ready && panel.mode == 'markdown'">
{{panel.content}}
</markdown>
<p ng-show="panel.mode == 'text'" ng-style='panel.style' ng-bind-html="panel.content | striphtml | newlines">
<p ng-show="panel.mode == 'text'" ng-style='panel.style' ng-bind-html-unsafe="panel.content | striphtml | newlines">
</p>
<p ng-show="panel.mode == 'html'" ng-bind-html="panel.content">
<p ng-show="panel.mode == 'html'" ng-bind-html-unsafe="panel.content">
</p>
</div>
</div>

View File

@ -23,31 +23,35 @@ function (angular, app, _, require) {
app.useModule(module);
module.controller('text', function($scope) {
$scope.panelMeta = {
description : "A static text panel that can use plain text, markdown, or (sanitized) HTML"
};
// Set and populate defaults
var _d = {
/** @scratch /panels/text/5
* === Parameters
*
* mode:: `html', `markdown' or `text'
*/
mode : "markdown", // 'html','markdown','text'
/** @scratch /panels/text/5
* content:: The content of your panel, written in the mark up specified in +mode+
*/
mode : "markdown", // 'html', 'markdown', 'text'
content : "",
style: {},
};
_.defaults($scope.panel,_d);
$scope.init = function() {
$scope.initPanel($scope);
$scope.initBaseController(this, $scope);
$scope.ready = false;
};
$scope.render = function() {
$scope.$emit('render');
};
$scope.openEditor = function() {
//$scope.$emit('open-modal','paneleditor');
console.log('scope id', $scope.$id);
};
});
module.directive('markdown', function() {

View File

@ -45,7 +45,7 @@
<a class="link" ng-click="removeAsFavorite()">Remove as favorite</a>
</li>
<li ng-show="dashboard.current.loader.save_local">
<a class="link" ng-click="dashboard.to_file()">Export schema</a>
<a class="link" ng-click="dashboard.to_file()">Export dashboard</a>
</li>
<li ng-show="showDropdown('share')"><a bs-tooltip="'Share'" data-placement="bottom" ng-click="elasticsearch_save('temp',dashboard.current.loader.save_temp_ttl)" config-modal="app/partials/dashLoaderShare.html">Share temp copy</i></a></li>

View File

@ -156,5 +156,5 @@
</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>
<button type="button" class="btn btn-info" ng-click="editor.index=0;dismiss();reset_panel();dashboard.refresh()">Close</button>
</div>

View File

@ -1,8 +1,17 @@
<div ng-controller="GraphiteImportCtrl" ng-init="init()">
<div ng-controller="GraphiteImportCtrl" ng-init="init()" style="height: 400px">
<h5>Import dashboards from graphite web</h5>
<div class="editor-row">
<div class="section">
<div class="btn-group">
<button class="btn btn-info dropdown-toggle" data-toggle="dropdown" bs-tooltip="'Datasource'">{{datasource.name}} <span class="caret"></span></button>
<ul class="dropdown-menu" role="menu">
<li ng-repeat="datasource in datasources" role="menuitem">
<a ng-click="setDatasource(datasource.value);">{{datasource.name}}</a>
</li>
</ul>
</div>
<button ng-click="listAll()" class="btn btn-primary">List all dashboards</button>
</div>
</div>

View File

@ -11,19 +11,18 @@
<div class="grafana-target-inner">
<ul class="grafana-target-controls">
<li class="dropdown">
<a class="pointer dropdown-toggle"
data-toggle="dropdown"
tabindex="1">
<a class="pointer dropdown-toggle"
data-toggle="dropdown"
tabindex="1">
<i class="icon-cog"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem">
<a tabindex="1"
ng-click="duplicate()">
Duplicate
</a>
<a tabindex="1" ng-click="duplicate()">Duplicate</a>
<a tabindex="2" ng-click="showQuery()" ng-hide="target.rawQuery">Show Query</a>
<a tabindex="2" ng-click="hideQuery()" ng-show="target.rawQuery">Hide Query</a>
</li>
</ul>
</ul>
</li>
<li>
<a class="pointer" tabindex="1" ng-click="removeTarget(target)">
@ -34,19 +33,29 @@
<ul class="grafana-target-controls-left">
<li>
<a class="grafana-target-segment"
ng-click="target.hide = !target.hide; get_data();"
role="menuitem">
<a class="grafana-target-segment"
ng-click="target.hide = !target.hide; get_data();"
role="menuitem">
<i class="icon-eye-open"></i>
</a>
</li>
</ul>
<ul class="grafana-segment-list" role="menu">
<li class="grafana-target-segment">
<li ng-show="target.rawQuery">
<input type="text"
class="input-large grafana-target-segment-input span10"
ng-model="target.query"
placeholder="select ..."
data-min-length=0 data-items=100
ng-blur="get_data()">
</li>
<li class="grafana-target-segment" ng-hide="target.rawQuery">
from series
</li>
<li>
<li ng-hide="target.rawQuery">
<input type="text"
class="input-medium grafana-target-segment-input"
ng-model="target.series"
@ -54,13 +63,14 @@
bs-typeahead="listSeries"
placeholder="series name"
data-min-length=0 data-items=100
ng-blur="seriesBlur()"
>
ng-blur="seriesBlur()">
</li>
<li class="grafana-target-segment">
<li class="grafana-target-segment" ng-hide="target.rawQuery">
select
</li>
<li>
<li ng-hide="target.rawQuery">
<input type="text"
class="input-medium grafana-target-segment-input"
ng-model="target.column"
@ -70,10 +80,11 @@
data-min-length=0
ng-blur="get_data()">
</li>
<li class="grafana-target-segment">
<li class="grafana-target-segment" ng-hide="target.rawQuery">
function
</li>
<li>
<li ng-hide="target.rawQuery">
<select class="input-medium grafana-target-segment-input"
ng-change="get_data()"
ng-model="target.function"
@ -92,10 +103,12 @@
data-min-length=0
ng-blur="get_data()">
</li>
<li class="grafana-target-segment">
<li class="grafana-target-segment" ng-hide="target.rawQuery">
group by time
</li>
<li>
<li ng-hide="target.rawQuery">
<input type="text"
class="input-mini grafana-target-segment-input"
ng-model="target.interval"

View File

@ -1,15 +1,68 @@
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3>Last Elasticsearch Query</h3>
</div>
<div class="modal-body">
<div class="modal-body" ng-controller="InspectCtrl" ng-init="init()">
<div class="pull-right editor-title">Inspector</div>
<div>
<pre>curl -XGET '{{config.elasticsearch}}/{{dashboard.indices|stringify}}/_search?pretty' -d '{{inspector}}'
</pre>
<div ng-model="editor.index" bs-tabs>
<div ng-repeat="tab in ['Request', 'Response', 'JS Error']" data-title="{{tab}}">
</div>
</div>
<div ng-if="editor.index == 0">
<h5>Request details</h5>
<table class="table table-striped small inspector-request-table">
<tr>
<td>Url</td>
<td>{{inspector.error.config.url}}</td>
</tr>
<tr>
<td>Method</td>
<td>{{inspector.error.config.method}}</td>
</tr>
<tr ng-repeat="(key, value) in inspector.error.config.headers">
<td>
{{key}}
</td>
<td>
{{value}}
</td>
</tr>
</table>
<h5>Request parameters</h5>
<table class="table table-striped small inspector-request-table">
<tr ng-repeat="param in request_parameters">
<td>
{{param.key}}
</td>
<td>
{{param.value}}
</td>
</tr>
</table>
</div>
<div ng-if="editor.index == 1">
<div ng-if="response_html">
<div iframe-content="response_html"></div>
</div>
</div>
<div ng-if="editor.index == 2">
<label>Message:</label>
<pre>
{{message}}
</pre>
<label>Stack trace:</label>
<pre>
{{stack_trace}}
</pre>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" ng-click="dismiss()">Close</button>
<button type="button" class="btn btn-info" ng-click="dismiss()">Close</button>
</div>

View File

@ -19,5 +19,5 @@
<div class="modal-footer">
<!-- close_edit() is provided here to allow for a scope to perform action on dismiss -->
<button type="button" class="btn btn-danger" ng-click="editor.index=0;dismiss()">Cancel</button>
<button type="button" class="btn btn-info" ng-click="editor.index=0;dismiss()">Close</button>
</div>

View File

@ -7,14 +7,5 @@
<div class="editor-option" ng-hide="panel.sizeable == false">
<label class="small">Span</label> <select class="input-mini" ng-model="panel.span" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10,11,12]"></select>
</div>
<div class="editor-option">
<label class="small">Editable</label><input type="checkbox" ng-model="panel.editable" ng-checked="panel.editable">
</div>
<div class="editor-option" ng-show="!_.isUndefined(panel.spyable)">
<label class="small">
Inspect <i class="icon-question-sign" bs-tooltip="'Allow query reveal via <i class=icon-eye-open></i>'"></i>
</label>
<input type="checkbox" ng-model="panel.spyable" ng-checked="panel.spyable">
</div>
</div>
</div>

View File

@ -62,5 +62,5 @@
<div class="modal-footer">
<button ng-show="editor.index == 1" ng-click="editor.index = 2;" class="btn btn-success" ng-disabled="panel.loadingEditor">Add Panel</button>
<button ng-show="panel.type && editor.index == 2" ng-click="add_panel(row,panel); reset_panel(); editor.index = 1;" class="btn btn-success" ng-disabled="panel.loadingEditor">Add Panel</button>
<button type="button" class="btn btn-danger" ng-click="editor.index=0;dismiss();reset_panel();close_edit()">Close</button>
<button type="button" class="btn btn-info" ng-click="editor.index=0;dismiss();reset_panel();close_edit()">Close</button>
</div>

View File

@ -135,7 +135,7 @@ define([
}
tooltip += '<i>' + moment(options.time).format('YYYY-MM-DD HH:mm:ss') + '</i><br/>';
if (options.data) {
tooltip += options.data;
tooltip += options.data.replace(/\n/g, '<br/>');
}
tooltip += "</small>";

View File

@ -15,7 +15,7 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
module.service('dashboard', function(
$routeParams, $http, $rootScope, $injector, $location, $timeout,
ejsResource, timer, alertSrv
ejsResource, timer, alertSrv, $q
) {
// A hash of defaults to use when loading a dashboard
@ -152,6 +152,8 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
// Make sure the dashboard being loaded has everything required
dashboard = dash_defaults(dashboard);
window.document.title = 'Grafana - ' + dashboard.title;
// Set the current dashboard
self.current = angular.copy(dashboard);
@ -330,13 +332,27 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
this.script_load = function(file) {
return $http({
url: "app/dashboards/"+file.replace(/\.(?!js)/,"/"),
method: "GET",
transformResponse: function(response) {
/*jshint -W054 */
var _f = new Function('ARGS','kbn','_','moment','window','document','angular','require','define','$','jQuery',response);
return _f($routeParams,kbn,_,moment);
method: "GET"
})
.then(function(result) {
/*jshint -W054 */
var script_func = new Function('ARGS','kbn','_','moment','window','document','$','jQuery', result.data);
var script_result = script_func($routeParams,kbn,_,moment, window, document, $, $);
// Handle async dashboard scripts
if (_.isFunction(script_result)) {
var deferred = $q.defer();
script_result(function(dashboard) {
$rootScope.$apply(function() {
deferred.resolve({ data: dashboard });
});
});
return deferred.promise;
}
}).then(function(result) {
return { data: script_result };
})
.then(function(result) {
if(!result) {
return false;
}

View File

@ -87,6 +87,16 @@ define([
this.setTime = function(time) {
_.extend(self.time, time);
// disable refresh if we have an absolute time
if (time.to !== 'now') {
self.old_refresh = dashboard.current.refresh;
dashboard.set_interval(false);
}
else if (self.old_refresh && self.old_refresh !== dashboard.current.refresh) {
dashboard.set_interval(self.old_refresh);
self.old_refresh = null;
}
$timeout(function(){
dashboard.refresh();
},0);

View File

@ -69,6 +69,11 @@ function (_) {
category: categories.Combine,
});
addFuncDef({
name: 'rangeOfSeries',
category: categories.Combine
});
addFuncDef({
name: 'percentileOfSeries',
category: categories.Combine,
@ -83,6 +88,18 @@ function (_) {
defaultParams: [3]
});
addFuncDef({
name: 'maxSeries',
shortName: 'max',
category: categories.Combine,
});
addFuncDef({
name: 'minSeries',
shortName: 'min',
category: categories.Combine,
});
addFuncDef({
name: 'averageSeriesWithWildcards',
category: categories.Combine,
@ -111,6 +128,19 @@ function (_) {
defaultParams: ['stacked']
});
addFuncDef({
name: "consolidateBy",
category: categories.Special,
params: [
{
name: 'function',
type: 'string',
options: ['sum', 'average', 'min', 'max']
}
],
defaultParams: ['max']
});
addFuncDef({
name: "groupByNode",
category: categories.Special,
@ -132,15 +162,43 @@ function (_) {
addFuncDef({
name: 'aliasByNode',
category: categories.Special,
params: [ { name: "node", type: "int", options: [0,1,2,3,4,5,6,7,8,9,10,12] } ],
params: [
{ name: "node", type: "int", options: [0,1,2,3,4,5,6,7,8,9,10,12] },
{ name: "node", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
],
defaultParams: [3]
});
addFuncDef({
name: 'substr',
category: categories.Special,
params: [
{ name: "start", type: "int", options: [-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,12] },
{ name: "stop", type: "int", options: [-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,12] },
],
defaultParams: [0, 0]
});
addFuncDef({
name: 'sortByName',
category: categories.Special
});
addFuncDef({
name: 'sortByMaxima',
category: categories.Special
});
addFuncDef({
name: 'sortByMinima',
category: categories.Special
});
addFuncDef({
name: 'sortByTotal',
category: categories.Special
});
addFuncDef({
name: 'aliasByMetric',
category: categories.Special,
@ -189,6 +247,13 @@ function (_) {
defaultParams: [10]
});
addFuncDef({
name: 'transformNull',
category: categories.Transform,
params: [ { name: "amount", type: "int", } ],
defaultParams: [0]
});
addFuncDef({
name: 'integral',
category: categories.Transform,
@ -220,6 +285,13 @@ function (_) {
defaultParams: ['1h', 'sum']
});
addFuncDef({
name: 'smartSummarize',
category: categories.Transform,
params: [ { name: "interval", type: "string" }, { name: "func", type: "select", options: ['sum', 'avg', 'min', 'max', 'last'] }],
defaultParams: ['1h', 'sum']
});
addFuncDef({
name: 'absolute',
category: categories.Transform,
@ -268,6 +340,41 @@ function (_) {
defaultParams: [25]
});
addFuncDef({
name: 'maximumAbove',
category: categories.Filter,
params: [ { name: "value", type: "int" } ],
defaultParams: [0]
});
addFuncDef({
name: 'maximumBelow',
category: categories.Filter,
params: [ { name: "value", type: "int" } ],
defaultParams: [0]
});
addFuncDef({
name: 'minimumAbove',
category: categories.Filter,
params: [ { name: "value", type: "int" } ],
defaultParams: [0]
});
addFuncDef({
name: 'limit',
category: categories.Filter,
params: [ { name: "n", type: "int" } ],
defaultParams: [5]
});
addFuncDef({
name: 'mostDeviant',
category: categories.Filter,
params: [ { name: "n", type: "int" } ],
defaultParams: [10]
});
addFuncDef({
name: "exclude",
category: categories.Filter,
@ -303,6 +410,20 @@ function (_) {
defaultParams: [10]
});
addFuncDef({
name: 'movingMedian',
category: categories.Filter,
params: [ { name: "windowSize", type: "select", options: ['1min', '5min', '15min', '30min', '1hour'] } ],
defaultParams: ['1min']
});
addFuncDef({
name: 'stdev',
category: categories.Filter,
params: [ { name: "n", type: "int" }, { name: "tolerance", type: "int" } ],
defaultParams: [5,0.1]
});
addFuncDef({
name: 'highestAverage',
category: categories.Filter,
@ -317,6 +438,34 @@ function (_) {
defaultParams: [5]
});
addFuncDef({
name: 'removeAbovePercentile',
category: categories.Filter,
params: [ { name: "n", type: "int" } ],
defaultParams: [5]
});
addFuncDef({
name: 'removeAboveValue',
category: categories.Filter,
params: [ { name: "n", type: "int" } ],
defaultParams: [5]
});
addFuncDef({
name: 'removeBelowPercentile',
category: categories.Filter,
params: [ { name: "n", type: "int" } ],
defaultParams: [5]
});
addFuncDef({
name: 'removeBelowValue',
category: categories.Filter,
params: [ { name: "n", type: "int" } ],
defaultParams: [5]
});
_.each(categories, function(funcList, catName) {
categories[catName] = _.sortBy(funcList, 'name');
});
@ -340,9 +489,29 @@ function (_) {
return str + parameters.join(',') + ')';
};
FuncInstance.prototype._hasMultipleParamsInString = function(strValue, index) {
if (strValue.indexOf(',') === -1) {
return false;
}
return this.def.params[index + 1] && this.def.params[index + 1].optional;
};
FuncInstance.prototype.updateParam = function(strValue, index) {
if (this.def.params[index].type === 'int') {
this.params[index] = parseInt(strValue, 10);
// handle optional parameters
// if string contains ',' and next param is optional, split and update both
if (this._hasMultipleParamsInString(strValue, index)) {
_.each(strValue.split(','), function(partVal, idx) {
this.updateParam(partVal.trim(), idx);
}, this);
return;
}
if (strValue === '' && this.def.params[index].optional) {
this.params.splice(index, 1);
}
else if (this.def.params[index].type === 'int') {
this.params[index] = parseFloat(strValue, 10);
}
else {
this.params[index] = strValue;
@ -359,6 +528,10 @@ function (_) {
var text = this.def.name + '(';
_.each(this.def.params, function(param, index) {
if (param.optional && this.params[index] === undefined) {
return;
}
text += this.params[index] + ', ';
}, this);
text = text.substring(0, text.length - 2);

View File

@ -24,8 +24,8 @@ function (angular, _, $, config, kbn, moment) {
GraphiteDatasource.prototype.query = function(options) {
try {
var graphOptions = {
from: this.translateTime(options.range.from),
until: this.translateTime(options.range.to),
from: this.translateTime(options.range.from, 'round-down'),
until: this.translateTime(options.range.to, 'round-up'),
targets: options.targets,
format: options.format,
maxDataPoints: options.maxDataPoints,
@ -68,7 +68,7 @@ function (angular, _, $, config, kbn, moment) {
}
};
GraphiteDatasource.prototype.translateTime = function(date) {
GraphiteDatasource.prototype.translateTime = function(date, rounding) {
if (_.isString(date)) {
if (date === 'now') {
return 'now';
@ -85,6 +85,21 @@ function (angular, _, $, config, kbn, moment) {
date = moment.utc(date);
if (rounding === 'round-up') {
if (date.get('s')) {
date.add('m', 1);
}
}
else if (rounding === 'round-down') {
// graphite' s from filter is exclusive
// here we step back one minute in order
// to guarantee that we get all the data that
// exists for the specified range
if (date.get('s')) {
date.subtract('m', 1);
}
}
if (dashboard.current.timezone === 'browser') {
date = date.local();
}

View File

@ -13,7 +13,7 @@ function (angular, _, kbn) {
function InfluxDatasource(datasource) {
this.type = 'influxDB';
this.editorSrc = 'app/partials/influxdb/editor.html';
this.url = datasource.url;
this.urls = datasource.urls;
this.username = datasource.username;
this.password = datasource.password;
this.name = datasource.name;
@ -26,15 +26,12 @@ function (angular, _, kbn) {
InfluxDatasource.prototype.query = function(options) {
var promises = _.map(options.targets, function(target) {
if (!target.series || !target.column || target.hide) {
var query;
if (target.hide || !((target.series && target.column) || target.query)) {
return [];
}
// var template = "select [[func]]([[column]]) as [[column]]_[[func]] from [[series]] where [[timeFilter]] group by time([[interval]]) order asc";
var template = "select [[func]]([[column]]) from [[series]] where [[condition]] [[timeFilter]] group by time([[interval]]) order asc";
target.condition_joined = (target.condition !== undefined ? target.condition + ' AND ' : '');
var templateData = {
series: target.series,
column: target.column,
@ -44,60 +41,122 @@ function (angular, _, kbn) {
interval: target.interval || options.interval
};
var query = _.template(template, templateData, this.templateSettings);
var timeFilter = getTimeFilter(options);
if (target.rawQuery) {
query = target.query;
query = query.replace(";", "");
var queryElements = query.split(" ");
var lowerCaseQueryElements = query.toLowerCase().split(" ");
var whereIndex = lowerCaseQueryElements.indexOf("where");
var groupByIndex = lowerCaseQueryElements.indexOf("group");
var orderIndex = lowerCaseQueryElements.indexOf("order");
if (whereIndex !== -1) {
queryElements.splice(whereIndex+1, 0, timeFilter, "and");
}
else {
if (groupByIndex !== -1) {
queryElements.splice(groupByIndex, 0, "where", timeFilter);
}
else if (orderIndex !== -1) {
queryElements.splice(orderIndex, 0, "where", timeFilter);
}
else {
queryElements.push("where");
queryElements.push(timeFilter);
}
}
query = queryElements.join(" ");
}
else {
var template = "select [[func]]([[column]]) as [[column]]_[[func]] from [[series]] " +
"where [[condition]] [[timeFilter]]" +
" group by time([[interval]]) order asc";
target.condition_joined = (target.condition !== undefined ? target.condition + ' AND ' : '');
var templateData = {
series: target.series,
column: target.column,
func: target.function,
timeFilter: timeFilter,
interval: target.interval || options.interval
};
query = _.template(template, templateData, this.templateSettings);
target.query = query;
}
return this.doInfluxRequest(query).then(handleInfluxQueryResponse);
}, this);
return $q.all(promises).then(function(results) {
return { data: _.flatten(results) };
});
};
InfluxDatasource.prototype.listColumns = function(seriesName) {
return this.doInfluxRequest('select * from ' + seriesName + ' limit 1').then(function(results) {
console.log('response!');
if (!results.data) {
return this.doInfluxRequest('select * from ' + seriesName + ' limit 1').then(function(data) {
if (!data) {
return [];
}
return results.data[0].columns;
return data[0].columns;
});
};
InfluxDatasource.prototype.listSeries = function() {
return this.doInfluxRequest('list series').then(function(results) {
if (!results.data) {
return [];
}
return _.map(results.data, function(series) {
return this.doInfluxRequest('list series').then(function(data) {
return _.map(data, function(series) {
return series.name;
});
});
};
function retry(deferred, callback, delay) {
return callback().then(undefined, function(reason) {
if (reason.status !== 0) {
deferred.reject(reason);
}
setTimeout(function() {
return retry(deferred, callback, Math.min(delay * 2, 30000));
}, delay);
});
}
InfluxDatasource.prototype.doInfluxRequest = function(query) {
var params = {
u: this.username,
p: this.password,
q: query
};
var _this = this;
var deferred = $q.defer();
var options = {
method: 'GET',
url: this.url + '/series',
params: params,
};
retry(deferred, function() {
var currentUrl = _this.urls.shift();
_this.urls.push(currentUrl);
console.log(query);
return $http(options);
var params = {
u: _this.username,
p: _this.password,
q: query
};
var options = {
method: 'GET',
url: currentUrl + '/series',
params: params,
};
return $http(options).success(function (data) {
deferred.resolve(data);
});
}, 10);
return deferred.promise;
};
function handleInfluxQueryResponse(results) {
function handleInfluxQueryResponse(data) {
var output = [];
var getKey = function (str) {
@ -106,7 +165,7 @@ function (angular, _, kbn) {
return (key2[0] !== key1[1] ? '.' + key2[0] : '');
}
_.each(results.data, function(series) {
_.each(data, function(series) {
var timeCol = series.columns.indexOf('time');
_.each(series.columns, function(column, index) {

View File

@ -67,6 +67,7 @@ function (angular, _, kbn) {
timerInstance = setInterval(function() {
$rootScope.$apply(function() {
angular.element(window).unbind('resize');
$location.path(dashboards[index % dashboards.length].url);
index++;
});
@ -82,4 +83,4 @@ function (angular, _, kbn) {
});
});
});

View File

@ -36,9 +36,16 @@ function (Settings) {
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)
* If you experiance problems with zoom, it is probably caused by timezone diff between
* your browser and the graphite-web application. timezoneOffset setting can be used to have Grafana
* translate absolute time ranges to the graphite-web timezone.
* Example:
* If TIME_ZONE in graphite-web config file local_settings.py is set to America/New_York, then set
* timezoneOffset to "-0500" (for UTC - 5 hours)
* Example:
* If TIME_ZONE is set to UTC, set this to "0000"
*/
timezoneOffset: null,
grafana_index: "grafana-dash",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -130,7 +130,7 @@
}
.panel-fullscreen {
z-index: 1500;
z-index: 100;
display: block !important;
position: fixed;
left: 0px;
@ -147,7 +147,6 @@
}
}
.dashboard-fullscreen .container-fluid.main {
height: 0px;
width: 0px;
@ -158,8 +157,10 @@
// Graphite Graph Legends
.grafana-legend-container {
margin: 4px 15px;
margin: 0 15px;
text-align: left;
position: relative;
top: 2px;
}
.grafana-legend-container .popover-content {

View File

@ -77,7 +77,7 @@ code, pre {
}
.panel-content {
padding: 0px 10px 10px 10px;
padding: 0px 10px 0 10px;
}
.panel-title {
@ -112,7 +112,12 @@ code, pre {
.panel-error {
color: @white;
padding: 3px 10px 0px 10px;
padding: 5px 10px 0px 10px;
}
.panel-error-inspector-link {
float: right;
margin-right: 10px;
}
@ -583,3 +588,21 @@ div.flot-text {
.save-dashboard-dropdown-save-form {
margin-bottom: 5px;
}
// inspector
.inspector-request-table {
td {
padding: 5px;
}
td:first-child {
white-space: nowrap;
}
}
// pre
code, pre {
background-color: @kibanaPanelBackground;
color: @textColor;
}

38
src/test/.jshintrc Normal file
View File

@ -0,0 +1,38 @@
{
"browser": true,
"bitwise":false,
"curly": true,
"eqnull": true,
"globalstrict": true,
"devel": true,
"eqeqeq": true,
"forin": false,
"immed": true,
"supernew": true,
"expr": true,
"indent": 2,
"latedef": true,
"newcap": true,
"noarg": true,
"noempty": true,
"undef": true,
"boss": true,
"trailing": false,
"laxbreak": true,
"laxcomma": true,
"sub": true,
"unused": true,
"maxlen": 140,
"globals": {
"expect": true,
"it": true,
"describe": true,
"define": true,
"module": true,
"beforeEach": true,
"inject": true,
"require": true,
"setImmediate": true
}
}

View File

@ -1,4 +1,6 @@
module.exports = function(config) {
'use strict';
config.set({
basePath: '../',

View File

@ -1,10 +1,12 @@
define([],
function() {
'use strict';
return {
create: function() {
return {
refresh: function() {},
set_interval: function(value) { this.current.refresh = value; },
current: {
title: "",
@ -32,9 +34,9 @@ define([],
load_local: false,
hide: false
},
refresh: false
refresh: true
}
}
};
}
}
};
});

View File

@ -3,13 +3,16 @@ define([
'underscore',
'services/filterSrv'
], function(dashboardMock, _) {
'use strict';
describe('filterSrv', function() {
var _filterSrv;
var _dashboard;
beforeEach(module('kibana.services'));
beforeEach(module(function($provide){
$provide.value('dashboard', dashboardMock.create());
_dashboard = dashboardMock.create();
$provide.value('dashboard', _dashboard);
}));
beforeEach(inject(function(filterSrv) {
@ -55,6 +58,23 @@ define([
});
});
describe('setTime', function() {
it('should return disable refresh for absolute times', function() {
_dashboard.current.refresh = true;
_filterSrv.setTime({from: '2011-01-01', to: '2015-01-01' });
expect(_dashboard.current.refresh).to.be(false);
});
it('should restore refresh after relative time range is set', function() {
_dashboard.current.refresh = true;
_filterSrv.setTime({from: '2011-01-01', to: '2015-01-01' });
expect(_dashboard.current.refresh).to.be(false);
_filterSrv.setTime({from: '2011-01-01', to: 'now' });
expect(_dashboard.current.refresh).to.be(true);
});
});
});
});

View File

@ -1,6 +1,7 @@
define([
'services/graphite/gfunc'
], function(gfunc) {
'use strict';
describe('when creating func instance from func names', function() {
@ -20,8 +21,8 @@ define([
it('should return func instance from funcDef', function() {
var func = gfunc.createFuncInstance('sum');
var func = gfunc.createFuncInstance(func.def);
expect(func).to.be.ok();
var func2 = gfunc.createFuncInstance(func.def);
expect(func2).to.be.ok();
});
it('func instance should have text representation', function() {
@ -57,12 +58,51 @@ define([
});
describe('when requesting function categories', function() {
it('should return function categories', function() {
var catIndex = gfunc.getCategories();
expect(catIndex.Special.length).to.be.greaterThan(8);
});
});
describe('when updating func param', function() {
it('should update param value and update text representation', function() {
var func = gfunc.createFuncInstance('summarize');
func.updateParam('1h', 0);
expect(func.params[0]).to.be('1h');
expect(func.text).to.be('summarize(1h, sum)');
});
it('should parse numbers as float', function() {
var func = gfunc.createFuncInstance('scale');
func.updateParam('0.001', 0);
expect(func.params[0]).to.be(0.001);
});
});
describe('when updating func param with optional second parameter', function() {
it('should update value and text', function() {
var func = gfunc.createFuncInstance('aliasByNode');
func.updateParam('1', 0);
expect(func.params[0]).to.be(1);
});
it('should slit text and put value in second param', function() {
var func = gfunc.createFuncInstance('aliasByNode');
func.updateParam('4,-5', 0);
expect(func.params[0]).to.be(4);
expect(func.params[1]).to.be(-5);
expect(func.text).to.be('aliasByNode(4, -5)');
});
it('should remove second param when empty string is set', function() {
var func = gfunc.createFuncInstance('aliasByNode');
func.updateParam('4,-5', 0);
func.updateParam('', 1);
expect(func.params[0]).to.be(4);
expect(func.params[1]).to.be(undefined);
expect(func.text).to.be('aliasByNode(4)');
});
});
});

View File

@ -1,11 +1,9 @@
define([
'mocks/dashboard-mock',
'underscore',
'services/filterSrv'
], function(dashboardMock, _) {
], function() {
'use strict';
describe('graphiteTargetCtrl', function() {
var _filterSrv;
var _targetCtrl;
beforeEach(module('kibana.services'));
beforeEach(module(function($provide){
@ -20,8 +18,6 @@ define([
describe('init', function() {
beforeEach(function() {
_filterSrv.add({ name: 'test', current: { value: 'oogle' } });
_filterSrv.init();
});
});
});

View File

@ -0,0 +1,24 @@
define([
'kbn'
], function(kbn) {
'use strict';
describe('millisecond formating', function() {
it('should translate 4378634603 as 1.67 years', function() {
var str = kbn.msFormat(4378634603, 2);
expect(str).to.be('50.68 day');
});
it('should translate 3654454 as 1.02 hour', function() {
var str = kbn.msFormat(3654454, 2);
expect(str).to.be('1.02 hour');
});
it('should translate 365445 as 6.09 min', function() {
var str = kbn.msFormat(365445, 2);
expect(str).to.be('6.09 min');
});
});
});

View File

@ -1,6 +1,7 @@
define([
'services/graphite/lexer'
], function(Lexer) {
'use strict';
describe('when lexing graphite expression', function() {
@ -88,6 +89,12 @@ define([
expect(tokens[4].pos).to.be(20);
});
it('should handle float parameters', function() {
var lexer = new Lexer("alias(metric, 0.002)");
var tokens = lexer.tokenize();
expect(tokens[4].type).to.be('number');
expect(tokens[4].value).to.be('0.002');
});
});

View File

@ -1,6 +1,7 @@
define([
'services/graphite/parser'
], function(Parser) {
'use strict';
describe('when parsing', function() {
@ -103,7 +104,7 @@ define([
var parser = new Parser("sum(test.[[server]].count)");
var rootNode = parser.getAst();
expect(rootNode.message).to.be(undefined)
expect(rootNode.message).to.be(undefined);
expect(rootNode.params[0].type).to.be('metric');
expect(rootNode.params[0].segments[1].type).to.be('template');
expect(rootNode.params[0].segments[1].value).to.be('server');
@ -139,6 +140,13 @@ define([
expect(rootNode.type).to.be('function');
});
it('handle float function arguments', function() {
var parser = new Parser('scale(test, 0.002)');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('function');
expect(rootNode.params[1].type).to.be('number');
expect(rootNode.params[1].value).to.be(0.002);
});
});

View File

@ -8,7 +8,6 @@ require.config({
kbn: 'components/kbn',
settings: 'components/settings',
crypto: '../vendor/crypto.min',
underscore: 'components/underscore.extended',
'underscore-src': '../vendor/underscore',
@ -29,7 +28,6 @@ require.config({
jquery: '../vendor/jquery/jquery-1.8.0',
bootstrap: '../vendor/bootstrap/bootstrap',
bindonce: '../vendor/angular/bindonce',
'jquery-ui': '../vendor/jquery/jquery-ui-1.10.3',
@ -106,6 +104,7 @@ require([
'angular',
'angularMocks',
], function(angular) {
'use strict';
angular.module('kibana', []);
angular.module('kibana.services', []);
@ -115,6 +114,7 @@ require([
'specs/parser-specs',
'specs/gfunc-specs',
'specs/filterSrv-specs',
'specs/kbn-format-specs',
], function () {
window.__karma__.start();
});

View File

@ -3,6 +3,7 @@ module.exports = function(grunt) {
// Concat and Minify the src directory into dist
grunt.registerTask('build', [
'jshint:source',
'jshint:tests',
'clean:on_start',
'less:src',
'copy:everything_but_less_to_temp',

View File

@ -1,5 +1,5 @@
// Lint and build CSS
module.exports = function(grunt) {
grunt.registerTask('default', ['jshint:source', 'less:src']);
grunt.registerTask('default', ['jshint:source', 'jshint:tests', 'less:src']);
grunt.registerTask('test', ['default', 'karma:test']);
};

View File

@ -9,9 +9,11 @@ module.exports = function(config) {
expand: true,
cwd: '<%= destDir %>',
src: ['**/*'],
dest: '<%= pkg.name %>/',
},
{
expand: true,
dest: '<%= pkg.name %>/',
src: ['LICENSE.md', 'README.md', 'NOTICE.md'],
}
]
@ -25,10 +27,12 @@ module.exports = function(config) {
expand: true,
cwd: '<%= destDir %>',
src: ['**/*'],
dest: '<%= pkg.name %>/',
},
{
expand: true,
src: ['LICENSE.md', 'README.md', 'NOTICE.md'],
dest: '<%= pkg.name %>/',
}
]
},
@ -41,10 +45,12 @@ module.exports = function(config) {
expand: true,
cwd: '<%= destDir %>',
src: ['**/*'],
dest: '<%= pkg.name %>-<%= pkg.version %>/',
},
{
expand: true,
src: ['LICENSE.md', 'README.md', 'NOTICE.md'],
dest: '<%= pkg.name %>-<%= pkg.version %>/',
}
]
},
@ -57,10 +63,12 @@ module.exports = function(config) {
expand: true,
cwd: '<%= destDir %>',
src: ['**/*'],
dest: '<%= pkg.name %>-<%= pkg.version %>/',
},
{
expand: true,
src: ['LICENSE.md', 'README.md', 'NOTICE.md'],
dest: '<%= pkg.name %>-<%= pkg.version %>/',
}
]
}

View File

@ -3,6 +3,7 @@ module.exports = function(config) {
dev: {
options: {
port: 5601,
hostname: '*',
base: config.srcDir,
keepalive: true
}

View File

@ -1,19 +1,25 @@
module.exports = function(config) {
return {
// just lint the source dir
source: {
files: {
src: ['Gruntfile.js', '<%= srcDir %>/app/**/*.js']
src: ['Gruntfile.js', '<%= srcDir %>/app/**/*.js'],
}
},
tests: {
files: {
src: ['<%= srcDir %>/test/**/*.js'],
}
},
options: {
jshintrc: '<%= baseDir %>/.jshintrc',
jshintrc: true,
reporter: require('jshint-stylish'),
ignores: [
'node_modules/*',
'dist/*',
'sample/*',
'<%= srcDir %>/vendor/*',
'<%= srcDir %>/app/panels/*/{lib,leaflet}/*'
'<%= srcDir %>/app/panels/*/{lib,leaflet}/*',
'<%= srcDir %>/app/dashboards/*'
]
}
};