Merge branch 'master' of github.com:torkelo/grafana-private into pro

Conflicts:
	src/test/test-main.js
This commit is contained in:
Torkel Ödegaard
2014-08-21 11:16:08 +02:00
82 changed files with 2564 additions and 1192 deletions
+15
View File
@@ -3,12 +3,27 @@
**New features and improvements**
- [Issue #578](https://github.com/grafana/grafana/issues/578). Dashboard: Row option to display row title even when the row is visible
- [Issue #672](https://github.com/grafana/grafana/issues/672). Dashboard: panel fullscreen & edit state is present in url, can now link to graph in edit & fullscreen mode.
- [Issue #709](https://github.com/grafana/grafana/issues/709). Dashboard: Small UI look polish to search results, made dashboard title link are larger
- [Issue #425](https://github.com/grafana/grafana/issues/425). Graph: New section in 'Display Styles' tab to override any display setting on per series bases (mix and match lines, bars, points, fill, stack, line width etc)
**Fixes**
- [Issue #696](https://github.com/grafana/grafana/issues/696). Graph: Fix for y-axis format 'none' when values are in scientific notation (ex 2.3e-13)
- [Issue #697](https://github.com/grafana/grafana/issues/697). Graphite: Fix for Glob syntax in graphite queries ([1-9] and ?) that made the query editor / parser bail and fallback to a text box.
- [Issue #277](https://github.com/grafana/grafana/issues/277). Dashboard: Fix for timepicker date & tooltip when UTC timezone selected. Closes #277
**Tech**
- Upgraded from angularjs 1.1.5 to 1.3 beta 17;
- Switch from underscore to lodash
- helpers to easily unit test angularjs controllers and services
- Test coverage through coveralls
- Upgrade from jquery 1.8.0 to 2.1.1 (**Removes support for IE7 & IE8**)
# 1.7.1 (unreleased)
**Fixes**
- [Issue #691](https://github.com/grafana/grafana/issues/691). Dashboard: tooltip fixes, sometimes they would not show, and sometimes they would get stuck.
- [Issue #695](https://github.com/grafana/grafana/issues/695). Dashboard: Tooltip on goto home menu icon would get stuck after clicking on it
# 1.7.0 (2014-08-11)
+21 -23
View File
@@ -38,7 +38,7 @@ Graphite, InfluxDB & OpenTSDB.
- Import dashboard from Graphite
- Templating
- [Scripted dashboards](http://grafana.org/docs/features/scripted_dashboards)
- [Dashboard playlists](http://grafana.org/docs/docs/features/playlist)
- [Dashboard playlists](http://grafana.org/docs/features/playlist)
- [Time range controls](http://grafana.org/docs/features/time_range)
### InfluxDB
@@ -49,40 +49,38 @@ Graphite, InfluxDB & OpenTSDB.
- Use as metric data source
- Query editor with metric name typeahead and tag filtering
# Requirements
## Requirements
There are no dependencies, Grafana is a client side application that runs in your browser. It only needs a time series store where it can fetch metrics. If you use InfluxDB Grafana can use it to store dashboards. If you use Graphite or OpenTSDB you can use Elasticsearch to store dashboards or just use json files stored on disk.
# Installation
## Installation
Head to [grafana.org](http://grafana.org) and [download](http://grafana.org/download/)
the latest release.
Then follow the quick [setup & config guide](http://grafana.org/docs/). If you have any problems please
read the [troubleshooting guide](http://grafana.org/docs/troubleshooting).
# Documentation & Support
## Documentation & Support
Be sure to read the [getting started guide](http://grafana.org/docs/features/intro) and the other
feature guides.
# Roadmap
- Improve graphite query editor to handle all types of queries
- Refine and simplify common tasks
- More panel types (not just graphs)
- Improve templating support
- Alerting
- Optional backend component
- Much much more! (what ever gets votes on github issues!)
## Run from master
Grafana uses nodejs and grunt for asset management (css & javascript), unit test runner and javascript syntax verification.
- clone repository
- install nodejs
- npm install (in project root)
- npm install -g grunt-cli
- grunt (runt default task that will generate css files)
- grunt build (creates optimized & minified release)
- grunt release (same as grunt build but will also create tar & zip package)
- grunt test (executes jshint and unit tests)
# 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!
## Contribute
If you have any idea for an improvement or found a bug do not hesitate to open an issue.
And if you have time clone this repo and submit a pull request and help me make Grafana
the kickass metrics & devops dashboard we all dream about!
Clone repository:
- npm install
- grunt server (starts development web server in src folder)
- grunt (runs jshint and less -> css compilation)
- npm test runs jshint, and unit tests
Before creating a pull request be sure that "grunt test" runs without any style or unit test errors, also
please [sign the CLA](http://grafana.org/docs/contributing/cla.html)
Before submitting a PR be sure that there are no jshint or unit test failures.
And [sign the CLA](http://grafana.org/docs/contributing/cla.html)
# License
## License
Grafana is distributed under Apache 2.0 License.
+1 -1
View File
@@ -34,7 +34,7 @@
"grunt-string-replace": "~0.2.4",
"grunt-usemin": "^2.1.1",
"jshint-stylish": "~0.1.5",
"karma": "~0.12.16",
"karma": "~0.12.21",
"karma-chrome-launcher": "~0.1.4",
"karma-coffee-preprocessor": "~0.1.2",
"karma-coverage": "^0.2.5",
+16 -7
View File
@@ -78,14 +78,23 @@ function (angular, $, _, appLevelRequire, config) {
apps_deps.push(module_name);
});
var preBootRequires = [
'controllers/all',
'directives/all',
'filters/all',
'components/partials',
'routes/all',
];
_.each(config.plugins.dependencies, function(dep) {
preBootRequires.push('../plugins/' + dep);
});
app.boot = function() {
require([
'controllers/all',
'directives/all',
'filters/all',
'components/partials',
'routes/all',
], function () {
require(preBootRequires, function () {
// disable tool tip animation
$.fn.tooltip.defaults.animation = false;
// bootstrap the app
angular
+44 -17
View File
@@ -1,4 +1,8 @@
define(['jquery','lodash','moment'],
define([
'jquery',
'lodash',
'moment'
],
function($, _, moment) {
'use strict';
@@ -227,36 +231,36 @@ function($, _, moment) {
if (type === 0) {
roundUp ? dateTime.endOf('year') : dateTime.startOf('year');
} else if (type === 1) {
dateTime.add('years',num);
dateTime.add(num, 'years');
} else if (type === 2) {
dateTime.subtract('years',num);
dateTime.subtract(num, 'years');
}
break;
case 'M':
if (type === 0) {
roundUp ? dateTime.endOf('month') : dateTime.startOf('month');
} else if (type === 1) {
dateTime.add('months',num);
dateTime.add(num, 'months');
} else if (type === 2) {
dateTime.subtract('months',num);
dateTime.subtract(num, 'months');
}
break;
case 'w':
if (type === 0) {
roundUp ? dateTime.endOf('week') : dateTime.startOf('week');
} else if (type === 1) {
dateTime.add('weeks',num);
dateTime.add(num, 'weeks');
} else if (type === 2) {
dateTime.subtract('weeks',num);
dateTime.subtract(num, 'weeks');
}
break;
case 'd':
if (type === 0) {
roundUp ? dateTime.endOf('day') : dateTime.startOf('day');
} else if (type === 1) {
dateTime.add('days',num);
dateTime.add(num, 'days');
} else if (type === 2) {
dateTime.subtract('days',num);
dateTime.subtract(num, 'days');
}
break;
case 'h':
@@ -264,27 +268,27 @@ function($, _, moment) {
if (type === 0) {
roundUp ? dateTime.endOf('hour') : dateTime.startOf('hour');
} else if (type === 1) {
dateTime.add('hours',num);
dateTime.add(num, 'hours');
} else if (type === 2) {
dateTime.subtract('hours',num);
dateTime.subtract(num,'hours');
}
break;
case 'm':
if (type === 0) {
roundUp ? dateTime.endOf('minute') : dateTime.startOf('minute');
} else if (type === 1) {
dateTime.add('minutes',num);
dateTime.add(num, 'minutes');
} else if (type === 2) {
dateTime.subtract('minutes',num);
dateTime.subtract(num, 'minutes');
}
break;
case 's':
if (type === 0) {
roundUp ? dateTime.endOf('second') : dateTime.startOf('second');
} else if (type === 1) {
dateTime.add('seconds',num);
dateTime.add(num, 'seconds');
} else if (type === 2) {
dateTime.subtract('seconds',num);
dateTime.subtract(num, 'seconds');
}
break;
default:
@@ -525,12 +529,35 @@ function($, _, moment) {
return kbn.nanosFormat(val, decimals);
};
default:
return function(val) {
return val % 1 === 0 ? val : val.toFixed(decimals);
return function(val, axis) {
return kbn.noneFormat(val, axis ? axis.tickDecimals : null);
};
}
};
kbn.noneFormat = function(value, decimals) {
var factor = decimals ? Math.pow(10, decimals) : 1;
var formatted = String(Math.round(value * factor) / factor);
// if exponent return directly
if (formatted.indexOf('e') !== -1 || value === 0) {
return formatted;
}
// If tickDecimals was specified, ensure that we have exactly that
// much precision; otherwise default to the value's own precision.
if (decimals != null) {
var decimalPos = formatted.indexOf(".");
var precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1;
if (precision < decimals) {
return (precision ? formatted : formatted + ".") + (String(factor)).substr(1, decimals - precision);
}
}
return formatted;
};
kbn.msFormat = function(size, decimals) {
// Less than 1 milli, downscale to micro
if (Math.abs(size) < 1) {
+1 -1
View File
@@ -33,4 +33,4 @@ function () {
});
return _;
});
});
+2 -1
View File
@@ -8,6 +8,7 @@ require.config({
config: ['../config', '../config.sample'],
settings: 'components/settings',
kbn: 'components/kbn',
store: 'components/store',
css: '../vendor/require/css',
text: '../vendor/require/text',
@@ -27,7 +28,7 @@ require.config({
'lodash-src': '../vendor/lodash',
bootstrap: '../vendor/bootstrap/bootstrap',
jquery: '../vendor/jquery/jquery-1.8.0',
jquery: '../vendor/jquery/jquery-2.1.1.min',
'jquery-ui': '../vendor/jquery/jquery-ui-1.10.3',
'extend-jquery': 'components/extend-jquery',
+6 -2
View File
@@ -19,7 +19,7 @@ function (_, crypto) {
default_route : '/dashboard/file/default.json',
playlist_timespan : "1m",
unsaved_changes_warning : true,
search : { max_results: 20 },
search : { max_results: 16 },
admin : {}
};
@@ -70,7 +70,7 @@ function (_, crypto) {
_.each(settings.datasources, function(datasource, key) {
datasource.name = key;
parseBasicAuth(datasource);
if (datasource.url) { parseBasicAuth(datasource); }
if (datasource.type === 'influxdb') { parseMultipleHosts(datasource); }
});
@@ -78,6 +78,10 @@ function (_, crypto) {
settings.panels = _.union(settings.panels, settings.plugins.panels);
}
if (!settings.plugins.dependencies) {
settings.plugins.dependencies = [];
}
return settings;
};
});
+20
View File
@@ -0,0 +1,20 @@
define([], function() {
'use strict';
return {
get: function(key) {
return window.localStorage[key];
},
set: function(key, value) {
window.localStorage[key] = value;
},
getBool: function(key) {
return window.localStorage[key] === 'true' ? true : false;
},
delete: function(key) {
window.localStorage.removeItem(key);
}
};
});
@@ -5,15 +5,57 @@ define([
function (_, kbn) {
'use strict';
var ts = {};
ts.ZeroFilled = function (opts) {
function TimeSeries(opts) {
this.datapoints = opts.datapoints;
this.info = opts.info;
this.label = opts.info.alias;
}
function matchSeriesOverride(aliasOrRegex, seriesAlias) {
if (!aliasOrRegex) { return false; }
if (aliasOrRegex[0] === '/') {
var match = aliasOrRegex.match(new RegExp('^/(.*?)/(g?i?m?y?)$'));
var regex = new RegExp(match[1], match[2]);
return seriesAlias.match(regex) != null;
}
return aliasOrRegex === seriesAlias;
}
function translateFillOption(fill) {
return fill === 0 ? 0.001 : fill/10;
}
TimeSeries.prototype.applySeriesOverrides = function(overrides) {
this.lines = {};
this.points = {};
this.bars = {};
this.info.yaxis = 1;
this.zindex = 0;
delete this.stack;
for (var i = 0; i < overrides.length; i++) {
var override = overrides[i];
if (!matchSeriesOverride(override.alias, this.info.alias)) {
continue;
}
if (override.lines !== void 0) { this.lines.show = override.lines; }
if (override.points !== void 0) { this.points.show = override.points; }
if (override.bars !== void 0) { this.bars.show = override.bars; }
if (override.fill !== void 0) { this.lines.fill = translateFillOption(override.fill); }
if (override.stack !== void 0) { this.stack = override.stack; }
if (override.linewidth !== void 0) { this.lines.lineWidth = override.linewidth; }
if (override.pointradius !== void 0) { this.points.radius = override.pointradius; }
if (override.steppedLine !== void 0) { this.lines.steps = override.steppedLine; }
if (override.zindex !== void 0) { this.zindex = override.zindex; }
if (override.yaxis !== void 0) {
this.info.yaxis = override.yaxis;
}
}
};
ts.ZeroFilled.prototype.getFlotPairs = function (fillStyle, yFormats) {
TimeSeries.prototype.getFlotPairs = function (fillStyle, yFormats) {
var result = [];
this.color = this.info.color;
@@ -74,5 +116,6 @@ function (_, kbn) {
return result;
};
return ts;
});
return TimeSeries;
});
+2 -2
View File
@@ -1,7 +1,7 @@
define([
'./grafanaCtrl',
'./dash',
'./dashLoader',
'./dashboardCtrl',
'./dashboardNavCtrl',
'./row',
'./submenuCtrl',
'./pulldown',
+3 -16
View File
@@ -2,12 +2,13 @@ define([
'angular',
'lodash',
'moment',
'store'
],
function (angular, _, moment) {
function (angular, _, moment, store) {
'use strict';
var module = angular.module('grafana.controllers');
var consoleEnabled = window.localStorage && window.localStorage.grafanaConsole === 'true';
var consoleEnabled = store.getBool('grafanaConsole');
if (!consoleEnabled) {
return;
@@ -15,20 +16,6 @@ function (angular, _, moment) {
var events = [];
var oldLog = console.log;
console.log = function (message) {
try {
if (_.isObject(message)) {
message = angular.toJson(message);
if (message.length > 50) {
message = message.substring(0, 50);
}
}
events.push(new ConsoleEvent('log', message, {}));
oldLog.apply(console, arguments);
} catch (e) { }
};
function ConsoleEvent(type, title, data) {
this.type = type;
this.title = title;
@@ -10,8 +10,10 @@ function (angular, $, config, _) {
var module = angular.module('grafana.controllers');
module.controller('DashCtrl', function(
$scope, $rootScope, dashboardKeybindings, filterSrv, dashboardSrv, panelMoveSrv, timer) {
module.controller('DashboardCtrl', function(
$scope, $rootScope, dashboardKeybindings,
filterSrv, dashboardSrv, dashboardViewStateSrv,
panelMoveSrv, timer, $timeout) {
$scope.editor = { index: 0 };
$scope.panelNames = config.panels;
@@ -19,14 +21,25 @@ function (angular, $, config, _) {
$scope.init = function() {
$scope.availablePanels = config.panels;
$scope.onAppEvent('setup-dashboard', $scope.setupDashboard);
angular.element(window).bind('resize', function() {
$timeout(function() {
$scope.$broadcast('render');
});
});
};
$scope.setupDashboard = function(event, dashboardData) {
timer.cancel_all();
$rootScope.fullscreen = false;
$rootScope.performance.dashboardLoadStart = new Date().getTime();
$rootScope.performance.panelsInitialized = 0;
$rootScope.performance.panelsRendered= 0;
$scope.dashboard = dashboardSrv.create(dashboardData);
$scope.dashboardViewState = dashboardViewStateSrv.create($scope);
$scope.grafana.style = $scope.dashboard.style;
$scope.filter = filterSrv;
@@ -78,10 +91,6 @@ function (angular, $, config, _) {
};
};
$scope.row_style = function(row) {
return { 'min-height': row.collapse ? '5px' : row.height };
};
$scope.panel_path =function(type) {
if(type) {
return 'app/panels/'+type.replace(".","/");
@@ -3,17 +3,19 @@ define([
'lodash',
'moment',
'config',
'store',
'filesaver'
],
function (angular, _, moment, config) {
function (angular, _, moment, config, store) {
'use strict';
var module = angular.module('grafana.controllers');
module.controller('dashLoader', function($scope, $rootScope, $http, alertSrv, $location, playlistSrv, datasourceSrv) {
module.controller('DashboardNavCtrl', function($scope, $rootScope, alertSrv, $location, playlistSrv, datasourceSrv) {
$scope.init = function() {
$scope.db = datasourceSrv.getGrafanaDB();
$scope.onAppEvent('save-dashboard', function() {
$scope.saveDashboard();
});
@@ -21,19 +23,16 @@ function (angular, _, moment, config) {
$scope.onAppEvent('zoom-out', function() {
$scope.zoom(2);
});
};
$scope.exitFullscreen = function() {
$scope.emitAppEvent('panel-fullscreen-exit');
};
$scope.set_default = function() {
window.localStorage.grafanaDashboardDefault = $location.path();
store.set('grafanaDashboardDefault', $location.path());
alertSrv.set('Home Set','This page has been set as your default dashboard','success',5000);
};
$scope.purge_default = function() {
delete window.localStorage.grafanaDashboardDefault;
store.delete('grafanaDashboardDefault');
alertSrv.set('Local Default Clear','Your default dashboard has been reset to the default','success', 5000);
};
@@ -78,6 +77,7 @@ function (angular, _, moment, config) {
.then(function(result) {
alertSrv.set('Dashboard Saved', 'Dashboard has been saved as "' + result.title + '"','success', 5000);
$location.search({});
$location.path(result.url);
$rootScope.$emit('dashboard-saved', $scope.dashboard);
@@ -87,7 +87,9 @@ function (angular, _, moment, config) {
});
};
$scope.deleteDashboard = function(id) {
$scope.deleteDashboard = function(id, $event) {
$event.stopPropagation();
if (!confirm('Are you sure you want to delete dashboard?')) {
return;
}
@@ -135,6 +137,7 @@ function (angular, _, moment, config) {
$scope.openSaveDropdown = function() {
$scope.isFavorite = playlistSrv.isCurrentFavorite($scope.dashboard);
$scope.saveDropdownOpened = true;
};
$scope.markAsFavorite = function() {
+67 -7
View File
@@ -2,8 +2,10 @@ define([
'angular',
'config',
'lodash',
'jquery',
'store',
],
function (angular, config, _) {
function (angular, config, _, $, store) {
"use strict";
var module = angular.module('grafana.controllers');
@@ -11,20 +13,23 @@ function (angular, config, _) {
module.controller('GrafanaCtrl', function($scope, alertSrv, grafanaVersion, $rootScope) {
$scope.grafanaVersion = grafanaVersion[0] === '@' ? 'master' : grafanaVersion;
$scope.consoleEnabled = store.getBool('grafanaConsole');
$rootScope.profilingEnabled = store.getBool('profilingEnabled');
$rootScope.performance = { loadStart: new Date().getTime() };
$scope.init = function() {
$scope._ = _;
$scope.dashAlerts = alertSrv;
$scope.grafana = {
style: 'dark'
};
$scope.consoleEnabled = (window.localStorage && window.localStorage.grafanaConsole === 'true');
if ($rootScope.profilingEnabled) { $scope.initProfiling(); }
$scope.dashAlerts = alertSrv;
$scope.grafana = { style: 'dark' };
};
$scope.toggleConsole = function() {
$scope.consoleEnabled = !$scope.consoleEnabled;
window.localStorage.grafanaConsole = $scope.consoleEnabled ? 'true' : 'false';
store.set('grafanaConsole', $scope.consoleEnabled);
};
$rootScope.onAppEvent = function(name, callback) {
@@ -46,6 +51,61 @@ function (angular, config, _) {
"#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7" //7
];
$scope.getTotalWatcherCount = function() {
var count = 0;
var scopes = 0;
var root = $(document.getElementsByTagName('body'));
var f = function (element) {
if (element.data().hasOwnProperty('$scope')) {
scopes++;
angular.forEach(element.data().$scope.$$watchers, function () {
count++;
});
}
angular.forEach(element.children(), function (childElement) {
f($(childElement));
});
};
f(root);
$rootScope.performance.scopeCount = scopes;
return count;
};
$scope.initProfiling = function() {
var count = 0;
$scope.$watch(function digestCounter() {
count++;
}, function() {
});
$scope.onAppEvent('setup-dashboard', function() {
count = 0;
setTimeout(function() {
console.log("Dashboard::Performance Total Digests: " + count);
console.log("Dashboard::Performance Total Watchers: " + $scope.getTotalWatcherCount());
console.log("Dashboard::Performance Total ScopeCount: " + $rootScope.performance.scopeCount);
var timeTaken = $rootScope.performance.allPanelsInitialized - $rootScope.performance.dashboardLoadStart;
console.log("Dashboard::Performance - All panels initialized in " + timeTaken + " ms");
// measure digest performance
var rootDigestStart = window.performance.now();
for (var i = 0; i < 30; i++) {
$rootScope.$apply();
}
console.log("Dashboard::Performance Root Digest " + ((window.performance.now() - rootDigestStart) / 30));
}, 3000);
});
};
$scope.init();
});
+1 -1
View File
@@ -289,7 +289,7 @@ function (angular, _, config, gfunc, Parser) {
this.expandable = options.expandable;
if (options.type === 'template') {
this.html = $sce.trustAsHtml("<span style='color: #ECEC09'>" + options.value + "</span>");
this.html = $sce.trustAsHtml(options.value);
}
else {
this.html = $sce.trustAsHtml(this.value);
+33 -59
View File
@@ -32,36 +32,13 @@ function (angular, app, _) {
}
};
$scope.rowSpan = function(row) {
return _.reduce(row.panels, function(p,v) {
return p + v.span;
},0);
};
// This can be overridden by individual panels
$scope.close_edit = function() {
$scope.$broadcast('render');
};
$scope.add_panel = function(panel) {
var rowSpan = $scope.rowSpan($scope.row);
var panelCount = $scope.row.panels.length;
var space = (12 - rowSpan) - panel.span;
// try to make room of there is no space left
if (space <= 0) {
if (panelCount === 1) {
$scope.row.panels[0].span = 6;
panel.span = 6;
}
else if (panelCount === 2) {
$scope.row.panels[0].span = 4;
$scope.row.panels[1].span = 4;
panel.span = 4;
}
}
$scope.row.panels.push(panel);
$scope.dashboard.add_panel(panel, $scope.row);
};
$scope.delete_row = function() {
@@ -100,45 +77,17 @@ function (angular, app, _) {
};
$scope.duplicatePanel = function(panel, row) {
row = row || $scope.row;
var currentRowSpan = $scope.rowSpan(row);
if (currentRowSpan <= 9) {
row.panels.push(angular.copy(panel));
}
else {
var rowsList = $scope.dashboard.rows;
var rowIndex = _.indexOf(rowsList, row);
if (rowIndex === rowsList.length - 1) {
var newRow = angular.copy($scope.row);
newRow.panels = [];
$scope.dashboard.rows.push(newRow);
$scope.duplicatePanel(panel, newRow);
}
else {
$scope.duplicatePanel(panel, rowsList[rowIndex+1]);
}
}
$scope.dashboard.duplicatePanel(panel, row || $scope.row);
};
$scope.reset_panel = function(type) {
var
defaultSpan = 12,
_as = 12-$scope.rowSpan($scope.row);
var defaultSpan = 12;
var _as = 12 - $scope.dashboard.rowSpan($scope.row);
$scope.panel = {
error : false,
/** @scratch /panels/1
* span:: A number, 1-12, that describes the width of the panel.
*/
span : _as < defaultSpan && _as > 0 ? _as : defaultSpan,
/** @scratch /panels/1
* editable:: Enable or disable the edit button the the panel
*/
editable: true,
/** @scratch /panels/1
* type:: The type of panel this object contains. Each panel type will require additional
* properties. See the panel types list to the right.
*/
type : type
};
@@ -155,12 +104,37 @@ function (angular, app, _) {
$scope.row.height = fixRowHeight($scope.row.height);
};
/** @scratch /panels/2
* --
*/
$scope.init();
});
module.directive('rowHeight', function() {
return function(scope, element) {
scope.$watchGroup(['row.collapse', 'row.height'], function() {
element[0].style.minHeight = scope.row.collapse ? '5px' : scope.row.height;
});
};
});
module.directive('panelWidth', function() {
return function(scope, element) {
scope.$watch('panel.span', function() {
element[0].style.width = ((scope.panel.span / 1.2) * 10) + '%';
});
};
});
module.directive('panelDropZone', function() {
return function(scope, element) {
scope.$watch('dashboard.$$panelDragging', function(newVal) {
if (newVal && scope.dashboard.rowSpan(scope.row) < 10) {
element.show();
}
else {
element.hide();
}
});
};
});
});
+8 -1
View File
@@ -41,6 +41,7 @@ function (angular, _, config, $) {
var selectedDash = $scope.results.dashboards[$scope.selectedIndex];
if (selectedDash) {
$location.search({});
$location.path("/dashboard/db/" + selectedDash.id);
setTimeout(function() {
$('body').click(); // hack to force dropdown to close;
@@ -49,7 +50,12 @@ function (angular, _, config, $) {
}
};
$scope.shareDashboard = function(title, id) {
$scope.goToDashboard = function(id) {
$location.path("/dashboard/db/" + id);
};
$scope.shareDashboard = function(title, id, $event) {
$event.stopPropagation();
var baseUrl = window.location.href.replace(window.location.hash,'');
$scope.share = {
@@ -98,6 +104,7 @@ function (angular, _, config, $) {
$element.next().find('.dropdown-toggle').dropdown('toggle');
}
$scope.searchOpened = true;
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
$scope.query.query = 'title:';
$scope.search();
+1 -1
View File
@@ -97,4 +97,4 @@ function (angular, app, _, $, gfunc) {
};
});
}
});
});
-35
View File
@@ -1,35 +0,0 @@
define([
'angular',
'app',
'lodash'
],
function (angular, app, _) {
'use strict';
angular
.module('grafana.directives')
.directive('addPanel', function($compile) {
return {
restrict: 'A',
link: function($scope, elem) {
$scope.$on("$destroy",function() {
elem.remove();
});
$scope.$watch('panel.type', function() {
var _type = $scope.panel.type;
$scope.reset_panel(_type);
if(!_.isUndefined($scope.panel.type)) {
$scope.panel.loadingEditor = true;
$scope.require(['panels/'+$scope.panel.type.replace(".","/") +'/module'], function () {
var template = '<div ng-controller="'+$scope.panel.type+'" ng-include="\'app/partials/paneladd.html\'"></div>';
elem.html($compile(angular.element(template))($scope));
$scope.panel.loadingEditor = false;
});
}
});
}
};
});
});
+1 -2
View File
@@ -1,5 +1,4 @@
define([
'./addPanel',
'./arrayJoin',
'./dashUpload',
'./grafanaPanel',
@@ -17,4 +16,4 @@ define([
'./graphiteFuncEditor',
'./grafanaVersionCheck',
'./influxdbFuncEditor'
], function () {});
], function () {});
+3 -3
View File
@@ -15,7 +15,7 @@ function (angular, app, _) {
var lastPulldownVal;
var lastHideControlsVal;
$scope.$watch('dashboard.pulldowns', function() {
$scope.$watchCollection('dashboard.pulldowns', function() {
if (!$scope.dashboard) {
return;
}
@@ -26,7 +26,7 @@ function (angular, app, _) {
elem.toggleClass('submenu-controls-visible', panelEnabled);
lastPulldownVal = panelEnabled;
}
}, true);
});
$scope.$watch('dashboard.hideControls', function() {
if (!$scope.dashboard) {
@@ -49,4 +49,4 @@ function (angular, app, _) {
};
});
});
});
+1 -1
View File
@@ -45,4 +45,4 @@ function (angular, _, $) {
}
};
});
});
});
+19 -19
View File
@@ -21,7 +21,6 @@ function (angular, $, kbn, moment, _) {
var legendSideLastValue = null;
scope.$on('refresh',function() {
if (scope.otherPanelInFullscreenMode()) { return; }
scope.get_data();
});
@@ -39,15 +38,14 @@ function (angular, $, kbn, moment, _) {
// Receive render events
scope.$on('render',function(event, renderData) {
data = renderData || data;
if (!data) {
scope.get_data();
return;
}
annotations = data.annotations || annotations;
render_panel();
});
// Re-render if the window is resized
angular.element(window).bind('resize', function() {
render_panel();
});
function setElementHeight() {
try {
var height = scope.height || scope.panel.height || scope.row.height;
@@ -115,7 +113,7 @@ function (angular, $, kbn, moment, _) {
lines: {
show: panel.lines,
zero: false,
fill: panel.fill === 0 ? 0.001 : panel.fill/10,
fill: translateFillOption(panel.fill),
lineWidth: panel.linewidth,
steps: panel.steppedLine
},
@@ -151,11 +149,12 @@ function (angular, $, kbn, moment, _) {
};
for (var i = 0; i < data.length; i++) {
var _d = data[i].getFlotPairs(panel.nullPointMode, panel.y_formats);
data[i].data = _d;
var series = data[i];
series.applySeriesOverrides(panel.seriesOverrides);
series.data = series.getFlotPairs(panel.nullPointMode, panel.y_formats);
}
if (panel.bars && data.length && data[0].info.timeStep) {
if (data.length && data[0].info.timeStep) {
options.series.bars.barWidth = data[0].info.timeStep / 1.5;
}
@@ -164,21 +163,27 @@ function (angular, $, kbn, moment, _) {
addAnnotations(options);
configureAxisOptions(data, options);
var sortedSeries = _.sortBy(data, function(series) { return series.zindex; });
// if legend is to the right delay plot draw a few milliseconds
// so the legend width calculation can be done
if (shouldDelayDraw(panel)) {
legendSideLastValue = panel.legend.rightSide;
setTimeout(function() {
plot = $.plot(elem, data, options);
plot = $.plot(elem, sortedSeries, options);
addAxisLabels();
}, 50);
}
else {
plot = $.plot(elem, data, options);
plot = $.plot(elem, sortedSeries, options);
addAxisLabels();
}
}
function translateFillOption(fill) {
return fill === 0 ? 0.001 : fill/10;
}
function shouldDelayDraw(panel) {
if (panel.legend.rightSide) {
return true;
@@ -300,9 +305,7 @@ function (angular, $, kbn, moment, _) {
}
function configureAxisMode(axis, format) {
if (format !== 'none') {
axis.tickFormatter = kbn.getFormatFunction(format, 1);
}
axis.tickFormatter = kbn.getFormatFunction(format, 1);
}
function time_format(interval, ticks, min, max) {
@@ -353,10 +356,7 @@ function (angular, $, kbn, moment, _) {
}
value = kbn.getFormatFunction(format, 2)(value);
timestamp = dashboard.timezone === 'browser' ?
moment(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss') :
moment.utc(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss');
timestamp = dashboard.formatDate(item.datapoint[0]);
$tooltip.html(group + value + " @ " + timestamp).place_tt(pos.pageX, pos.pageY);
} else {
+25 -30
View File
@@ -8,7 +8,7 @@ function (angular, $) {
angular
.module('grafana.directives')
.directive('grafanaPanel', function($compile) {
.directive('grafanaPanel', function($compile, $parse) {
var container = '<div class="panel-container"></div>';
var content = '<div class="panel-content"></div>';
@@ -18,11 +18,13 @@ function (angular, $) {
'<div class="row-fluid panel-extra">' +
'<div class="panel-extra-container">' +
'<span class="alert-error panel-error small pointer"' +
'config-modal="app/partials/inspector.html" ng-show="panel.error" data-placement="right" bs-tooltip="panel.error">' +
'config-modal="app/partials/inspector.html" ng-if="panel.error">' +
'<span data-placement="right" bs-tooltip="panel.error">' +
'<i class="icon-exclamation-sign"></i><span class="panel-error-arrow"></span>' +
'</span>' +
'</span>' +
'<span class="panel-loading" ng-show="panelMeta.loading == true">' +
'<span class="panel-loading" ng-show="panelMeta.loading">' +
'<i class="icon-spinner icon-spin icon-large"></i>' +
'</span>' +
@@ -36,7 +38,7 @@ function (angular, $) {
'index:{{$index}},'+
'onStart:\'panelMoveStart\','+
'onStop:\'panelMoveStop\''+
'}" ng-model="row.panels" ' +
'}" ng-model="panel" ' +
'>' +
'{{panel.title || "No title"}}' +
'</span>' +
@@ -49,8 +51,7 @@ function (angular, $) {
return {
restrict: 'E',
link: function($scope, elem, attr) {
// once we have the template, scan it for controllers and
// load the module.js if we have any
var getter = $parse(attr.type), panelType = getter($scope);
var newScope = $scope.$new();
$scope.kbnJqUiDraggableOptions = {
@@ -68,6 +69,14 @@ function (angular, $) {
/* jshint indent:false */
$compile(elem.contents())(newScope);
elem.removeClass("ng-cloak");
var panelCtrlElem = $(elem.children()[0]);
var panelCtrlScope = panelCtrlElem.data().$scope;
panelCtrlScope.$watchGroup(['fullscreen', 'panel.height', 'row.height'], function() {
panelCtrlElem.css({ minHeight: panelCtrlScope.panel.height || panelCtrlScope.row.height });
panelCtrlElem.toggleClass('panel-fullscreen', panelCtrlScope.fullscreen ? true : false);
});
}
newScope.$on('$destroy',function() {
@@ -75,31 +84,17 @@ function (angular, $) {
elem.remove();
});
$scope.$watch(attr.type, function (name) {
elem.addClass("ng-cloak");
// load the panels module file, then render it in the dom.
var nameAsPath = name.replace(".", "/");
$scope.require([
'jquery',
'text!panels/'+nameAsPath+'/module.html'
], function ($, moduleTemplate) {
var $module = $(moduleTemplate);
// top level controllers
var $controllers = $module.filter('ngcontroller, [ng-controller], .ng-controller');
// add child controllers
$controllers = $controllers.add($module.find('ngcontroller, [ng-controller], .ng-controller'));
elem.addClass('ng-cloak');
if ($controllers.length) {
$controllers.first().prepend(panelHeader);
$controllers.first().find('.panel-header').nextAll().wrapAll(content);
$scope.require(['panels/' + nameAsPath + '/module'], function() {
loadModule($module);
});
} else {
loadModule($module);
}
});
$scope.require([
'jquery',
'text!panels/'+panelType+'/module.html',
'panels/' + panelType + "/module",
], function ($, moduleTemplate) {
var $module = $(moduleTemplate);
$module.prepend(panelHeader);
$module.first().find('.panel-header').nextAll().wrapAll(content);
loadModule($module);
});
}
+2 -13
View File
@@ -1,8 +1,7 @@
define([
'angular',
'lodash'
],
function (angular, _) {
function (angular) {
'use strict';
angular
@@ -60,18 +59,8 @@ function (angular, _) {
loadController(name);
});
if(attr.panel) {
$scope.$watch(attr.panel, function (panel) {
// If the panel attribute is specified, create a new scope. This ruins configuration
// so don't do it with anything that needs to use editor.html
if(!_.isUndefined(panel)) {
$scope = $scope.$new();
$scope.panel = angular.fromJson(panel);
}
});
}
}
};
});
});
});
+1 -1
View File
@@ -14,7 +14,7 @@ function (angular) {
return;
}
$http({ method: 'GET', url: 'http://grafanarel.s3.amazonaws.com/latest.json' })
$http({ method: 'GET', url: 'https://grafanarel.s3.amazonaws.com/latest.json' })
.then(function(response) {
if (!response.data || !response.data.version) {
return;
+3 -2
View File
@@ -7,13 +7,14 @@ function (angular) {
.directive('ngModelOnblur', function() {
return {
restrict: 'A',
priority: 1,
require: 'ngModel',
link: function(scope, elm, attr, ngModelCtrl) {
if (attr.type === 'radio' || attr.type === 'checkbox') {
return;
}
elm.unbind('input').unbind('keydown').unbind('change');
elm.off('input keydown change');
elm.bind('blur', function() {
scope.$apply(function() {
ngModelCtrl.$setViewValue(elm.val());
@@ -22,4 +23,4 @@ function (angular) {
}
};
});
});
});
+2 -2
View File
@@ -34,12 +34,12 @@
<div class="editor-row small" style="padding-bottom: 0;">
<label>Axis:</label>
<button ng-click="toggleYAxis(series)"
<button ng-click="toggleYAxis(series);dismiss();"
class="btn btn-mini"
ng-class="{'btn-success': series.yaxis === 1 }">
Left
</button>
<button ng-click="toggleYAxis(series)"
<button ng-click="toggleYAxis(series);dismiss();"
class="btn btn-mini"
ng-class="{'btn-success': series.yaxis === 2 }">
Right
+2 -4
View File
@@ -1,6 +1,4 @@
<div ng-controller='GraphCtrl'
style="min-height:{{panel.height || row.height}}"
ng-class="{'panel-fullscreen': fullscreen}">
<div ng-controller='GraphCtrl'>
<div class="graph-wrapper" ng-class="{'graph-legend-rightside': panel.legend.rightSide}">
<div class="graph-canvas-wrapper">
@@ -29,7 +27,7 @@
</div>
</div>
<div class="tab-content" ng-repeat="tab in panelMeta.fullEditorTabs" ng-show="editorTabs[editor.index] == tab.title">
<div class="tab-content" ng-repeat="tab in panelMeta.fullEditorTabs" ng-if="editorTabs[editor.index] == tab.title">
<div ng-include src="tab.src"></div>
</div>
</div>
+25 -41
View File
@@ -1,16 +1,3 @@
/** @scratch /panels/5
* include::panels/histogram.asciidoc[]
*/
/** @scratch /panels/histogram/0
* == Histogram
* Status: *Stable*
*
* The histogram panel allow for the display of time charts. It includes several modes and tranformations
* to display event counts, mean, min, max and total of numeric fields, and derivatives of counter
* fields.
*
*/
define([
'angular',
'app',
@@ -18,7 +5,8 @@ define([
'lodash',
'kbn',
'moment',
'./timeSeries',
'components/timeSeries',
'./seriesOverridesCtrl',
'services/panelSrv',
'services/annotationsSrv',
'services/datasourceSrv',
@@ -29,11 +17,10 @@ define([
'jquery.flot.stack',
'jquery.flot.stackpercent'
],
function (angular, app, $, _, kbn, moment, timeSeries) {
function (angular, app, $, _, kbn, moment, TimeSeries) {
'use strict';
var module = angular.module('grafana.panels.graph', []);
var module = angular.module('grafana.panels.graph');
app.useModule(module);
module.controller('GraphCtrl', function($scope, $rootScope, $timeout, panelSrv, annotationsSrv) {
@@ -179,7 +166,8 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
targets: [{}],
aliasColors: {},
aliasYAxis: {},
seriesOverrides: [],
};
_.defaults($scope.panel,_d);
@@ -188,13 +176,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
_.defaults($scope.panel.grid, _d.grid);
_.defaults($scope.panel.legend, _d.legend);
$scope.init = function() {
panelSrv.init($scope);
$scope.hiddenSeries = {};
if (!$scope.skipDataOnInit) {
$scope.get_data();
}
};
$scope.hiddenSeries = {};
$scope.updateTimeRange = function () {
$scope.range = $scope.filter.timeRange();
@@ -210,10 +192,6 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
};
$scope.get_data = function() {
delete $scope.panel.error;
$scope.panelMeta.loading = true;
$scope.updateTimeRange();
var metricsQuery = {
@@ -253,7 +231,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
var data = _.map(results.data, $scope.seriesHandler);
$scope.datapointsWarning = $scope.datapointsCount || !$scope.datapointsOutside;
$scope.datapointsWarning = $scope.datapointsCount === 0 || $scope.datapointsOutside;
$scope.annotationsPromise
.then(function(annotations) {
@@ -268,18 +246,15 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
var datapoints = seriesData.datapoints;
var alias = seriesData.target;
var color = $scope.panel.aliasColors[alias] || $rootScope.colors[index];
var yaxis = $scope.panel.aliasYAxis[alias] || 1;
var seriesInfo = {
alias: alias,
color: color,
enable: true,
yaxis: yaxis
};
$scope.legend.push(seriesInfo);
var series = new timeSeries.ZeroFilled({
var series = new TimeSeries({
datapoints: datapoints,
info: seriesInfo,
});
@@ -297,10 +272,6 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
return series;
};
$scope.otherPanelInFullscreenMode = function() {
return $rootScope.fullscreen && !$scope.fullscreen;
};
$scope.render = function(data) {
$scope.$emit('render', data);
};
@@ -361,8 +332,12 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
};
$scope.toggleYAxis = function(info) {
info.yaxis = info.yaxis === 2 ? 1 : 2;
$scope.panel.aliasYAxis[info.alias] = info.yaxis;
var override = _.findWhere($scope.panel.seriesOverrides, { alias: info.alias });
if (!override) {
override = { alias: info.alias };
$scope.panel.seriesOverrides.push(override);
}
override.yaxis = info.yaxis === 2 ? 1 : 2;
$scope.render();
};
@@ -371,7 +346,16 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
$scope.render();
};
$scope.init();
$scope.addSeriesOverride = function() {
$scope.panel.seriesOverrides.push({});
};
$scope.removeSeriesOverride = function(override) {
$scope.panel.seriesOverrides = _.without($scope.panel.seriesOverrides, override);
$scope.render();
};
panelSrv.init($scope);
});
});
@@ -0,0 +1,80 @@
define([
'angular',
'app',
'lodash',
], function(angular, app, _) {
'use strict';
var module = angular.module('grafana.panels.graph', []);
app.useModule(module);
module.controller('SeriesOverridesCtrl', function($scope) {
$scope.overrideMenu = [];
$scope.currentOverrides = [];
$scope.override = $scope.override || {};
$scope.addOverrideOption = function(name, propertyName, values) {
var option = {};
option.text = name;
option.propertyName = propertyName;
option.index = $scope.overrideMenu.length;
option.values = values;
option.submenu = _.map(values, function(value, index) {
return {
text: String(value),
click: 'setOverride(' + option.index + ',' + index + ')'
};
});
$scope.overrideMenu.push(option);
};
$scope.setOverride = function(optionIndex, valueIndex) {
var option = $scope.overrideMenu[optionIndex];
var value = option.values[valueIndex];
$scope.override[option.propertyName] = value;
$scope.updateCurrentOverrides();
$scope.render();
};
$scope.removeOverride = function(option) {
delete $scope.override[option.propertyName];
$scope.updateCurrentOverrides();
$scope.render();
};
$scope.getSeriesNames = function() {
return _.map($scope.legend, function(info) {
return info.alias;
});
};
$scope.updateCurrentOverrides = function() {
$scope.currentOverrides = [];
_.each($scope.overrideMenu, function(option) {
var value = $scope.override[option.propertyName];
if (_.isUndefined(value)) { return; }
$scope.currentOverrides.push({
name: option.text,
propertyName: option.propertyName,
value: String(value)
});
});
};
$scope.addOverrideOption('Bars', 'bars', [true, false]);
$scope.addOverrideOption('Lines', 'lines', [true, false]);
$scope.addOverrideOption('Line fill', 'fill', [0,1,2,3,4,5,6,7,8,9,10]);
$scope.addOverrideOption('Line width', 'linewidth', [0,1,2,3,4,5,6,7,8,9,10]);
$scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]);
$scope.addOverrideOption('Points', 'points', [true, false]);
$scope.addOverrideOption('Points Radius', 'pointradius', [1,2,3,4,5]);
$scope.addOverrideOption('Stack', 'stack', [true, false]);
$scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]);
$scope.addOverrideOption('Z-index', 'zindex', [-1,-2,-3,0,1,2,3]);
$scope.updateCurrentOverrides();
});
});
+46 -2
View File
@@ -1,5 +1,3 @@
<div class="editor-row">
<div class="section">
<h5>Chart Options</h5>
@@ -64,3 +62,49 @@
</div>
</div>
</div>
<div class="editor-row">
<div class="section">
<h5>Series specific overrides <tip>Regex match example: /server[0-3]/i </tip></h5>
<div>
<div class="grafana-target" ng-repeat="override in panel.seriesOverrides" ng-controller="SeriesOverridesCtrl">
<div class="grafana-target-inner-wrapper">
<div class="grafana-target-inner">
<ul class="grafana-target-controls-left">
<li class="grafana-target-segment">
<i class="icon-remove pointer" ng-click="removeSeriesOverride(override)"></i>
</li>
</ul>
<ul class="grafana-segment-list">
<li class="grafana-target-segment">
alias or regex
</li>
<li>
<input type="text"
ng-model="override.alias"
bs-typeahead="getSeriesNames"
ng-blur="render()"
data-min-length=0 data-items=100
class="input-medium grafana-target-segment-input" >
</li>
<li class="grafana-target-segment" ng-repeat="option in currentOverrides">
<i class="pointer icon-remove" ng-click="removeOverride(option)"></i>
{{option.name}}: {{option.value}}
</li>
<li class="dropdown">
<a class="dropdown-toggle grafana-target-segment" data-toggle="dropdown" gf-dropdown="overrideMenu" bs-tooltip="'set option to override'">
<i class="icon-plus"></i>
</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
</div>
<button class="btn btn-success" style="margin-top: 20px" ng-click="addSeriesOverride()">Add series override rule</button>
</div>
</div>
+1 -1
View File
@@ -1,4 +1,4 @@
<div ng-controller='text' style="min-height:{{panel.height || row.height}}">
<div ng-controller='text'>
<p ng-bind-html="content">
</p>
</div>
+15 -20
View File
@@ -1,15 +1,3 @@
/** @scratch /panels/5
* include::panels/text.asciidoc[]
*/
/** @scratch /panels/text/0
* == text
* Status: *Stable*
*
* The text panel is used for displaying static text formated as markdown, sanitized html or as plain
* text.
*
*/
define([
'angular',
'app',
@@ -23,6 +11,8 @@ function (angular, app, _, require) {
var module = angular.module('grafana.panels.text', []);
app.useModule(module);
var converter;
module.controller('text', function($scope, filterSrv, $sce, panelSrv) {
$scope.panelMeta = {
@@ -68,15 +58,20 @@ function (angular, app, _, require) {
};
$scope.renderMarkdown = function(content) {
require(['./lib/showdown'], function (Showdown) {
var converter = new Showdown.converter();
var text = content
.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;');
var text = content
.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;');
if (converter) {
$scope.updateContent(converter.makeHtml(text));
});
}
else {
require(['./lib/showdown'], function (Showdown) {
converter = new Showdown.converter();
$scope.updateContent(converter.makeHtml(text));
});
}
};
$scope.updateContent = function(html) {
@@ -88,7 +83,7 @@ function (angular, app, _, require) {
}
if(!$scope.$$phase) {
$scope.$apply();
$scope.$digest();
}
};
+2 -9
View File
@@ -15,15 +15,8 @@
<ul class="nav nav-pills timepicker-dropdown">
<li class="dropdown">
<a class="dropdown-toggle timepicker-dropdown" data-toggle="dropdown" href="" bs-tooltip="time.from.date ? (time.from.date | date:'yyyy-MM-dd HH:mm:ss.sss') + ' <br>to<br>' +(time.to.date | date:'yyyy-MM-dd HH:mm:ss.sss') : 'Click to set a time filter'" data-placement="bottom" ng-click="dismiss();">
<span ng-show="filter.time">
<span class="pointer" ng-hide="panel.now">{{time.from.date | date:'MMM d, y HH:mm:ss'}}</span>
<span class="pointer" ng-show="panel.now">{{time.from.date | moment:'ago'}}</span>
to
<span class="pointer" ng-hide="panel.now" >{{time.to.date | date:'MMM d, y HH:mm:ss'}}</span>
<span class="pointer" ng-show="panel.now">{{time.to.date | moment:'ago'}}</span>
</span>
<a class="dropdown-toggle timepicker-dropdown" data-toggle="dropdown" href="" bs-tooltip="time.tooltip" data-placement="bottom" ng-click="dismiss();">
<span ng-show="filter.time" ng-bind="time.rangeString"></span>
<span ng-hide="filter.time">Time filter</span>
<span ng-show="dashboard.refresh" class="text-warning">refreshed every {{dashboard.refresh}} </span>
<i class="icon-caret-down"></i>
+29 -6
View File
@@ -80,9 +80,14 @@ function (angular, app, _, moment, kbn) {
$scope.temptime = cloneTime($scope.time);
$scope.tempnow = $scope.panel.now;
$scope.temptime.from.date.setHours(0,0,0,0);
$scope.temptime.to.date.setHours(0,0,0,0);
// Date picker needs the date to be at the start of the day
$scope.temptime.from.date.setHours(1,0,0,0);
$scope.temptime.to.date.setHours(1,0,0,0);
if(new Date().getTimezoneOffset() < 0) {
$scope.temptime.from.date = moment($scope.temptime.from.date).add('days',1).toDate();
$scope.temptime.to.date = moment($scope.temptime.to.date).add('days',1).toDate();
}
$q.when(customTimeModal).then(function(modalEl) {
modalEl.modal('show');
@@ -172,10 +177,28 @@ function (angular, app, _, moment, kbn) {
};
var getScopeTimeObj = function(from,to) {
return {
from: getTimeObj(from),
to: getTimeObj(to)
};
var model = { from: getTimeObj(from), to: getTimeObj(to), };
if (model.from.date) {
model.tooltip = $scope.dashboard.formatDate(model.from.date) + ' <br>to<br>';
model.tooltip += $scope.dashboard.formatDate(model.to.date);
}
else {
model.tooltip = 'Click to set time filter';
}
if ($scope.filter.time) {
if ($scope.panel.now) {
model.rangeString = moment(model.from.date).fromNow() + ' to ' +
moment(model.to.date).fromNow();
}
else {
model.rangeString = $scope.dashboard.formatDate(model.from.date, 'MMM D, YYYY HH:mm:ss') + ' to ' +
$scope.dashboard.formatDate(model.to.date, 'MMM D, YYYY HH:mm:ss');
}
}
return model;
};
var getTimeObj = function(date) {
-69
View File
@@ -1,69 +0,0 @@
<style>
.noarrow>a:after {
display: none !important;
}
</style>
<li ng-show="fullscreen">
<a ng-click="exitFullscreen()">
Back to dashboard
</a>
</li>
<li class="grafana-menu-zoom-out">
<a class='small' ng-click='zoom(2)'>
Zoom Out
</a>
</li>
<li ng-repeat="pulldown in dashboard.nav" ng-controller="PulldownCtrl" ng-show="pulldown.enable"><grafana-simple-panel type="pulldown.type" ng-cloak></grafana-simple-panel></li>
<li class="dropdown grafana-menu-save">
<a href="#" bs-tooltip="'Save'" data-placement="bottom" class="dropdown-toggle" data-toggle="dropdown" ng-click="openSaveDropdown()">
<i class='icon-save'></i>
</a>
<ul class="save-dashboard-dropdown dropdown-menu">
<li>
<form class="input-prepend nomargin save-dashboard-dropdown-save-form">
<input class='input-medium' ng-model="dashboard.title" type="text" />
<button class="btn" ng-click="saveDashboard()"><i class="icon-save"></i></button>
</form>
</li>
<li>
<a class="link" ng-click="set_default()">Save as Home</a>
</li>
<li>
<a class="link" ng-click="purge_default()">Reset Home</a>
</li>
<li ng-show="!isFavorite">
<a class="link" ng-click="markAsFavorite()">Mark as favorite</a>
</li>
<li ng-show="isFavorite">
<a class="link" ng-click="removeAsFavorite()">Remove as favorite</a>
</li>
<li>
<a class="link" ng-click="exportDashboard()">Export dashboard</a>
</li>
<li ng-show="db.saveTemp">
<a bs-tooltip="'Share'" data-placement="bottom" ng-click="saveForSharing()" config-modal="app/partials/dashLoaderShare.html">
Share temp copy
</a>
</li>
</ul>
</li>
<li class="dropdown grafana-menu-load" ng-controller="SearchCtrl" ng-init="init()" ng-include="'app/partials/search.html'">
</li>
<li class="grafana-menu-home"><a bs-tooltip="'Goto saved default'" data-placement="bottom" href='#/'><i class='icon-home'></i></a></li>
<li class="grafana-menu-edit" ng-show="dashboard.editable" bs-tooltip="'Configure dashboard'" data-placement="bottom"><a class="link" config-modal="app/partials/dasheditor.html"><i class='icon-cog pointer'></i></a></li>
<li class="grafana-menu-stop-playlist hide">
<a class='small' ng-click='stopPlaylist(2)'>
Stop playlist
</a>
</li>
+16 -18
View File
@@ -1,16 +1,9 @@
<div ng-controller="DashCtrl" body-class>
<div ng-controller="DashboardCtrl" body-class ng-class="{'dashboard-fullscreen': dashboardViewState.fullscreen}">
<div class="navbar navbar-static-top">
<div class="navbar-inner">
<div class="container-fluid">
<span class="brand"><img src="img/small.png" bs-tooltip="'Grafana'" data-placement="bottom"> {{dashboard.title}}</span>
<ul class="nav pull-right" ng-controller='dashLoader' ng-init="init()" ng-include="'app/partials/dashLoader.html'">
</ul>
</div>
</div>
</div>
<div ng-include="'app/partials/dashboard_topnav.html'">
</div>
<div class="submenu-controls">
<div class="submenu-controls">
<div class="submenu-panel" ng-controller="SubmenuCtrl" ng-repeat="pulldown in dashboard.pulldowns | filter:{ enable: true }">
<div class="submenu-panel-title">
<span class="small"><strong>{{pulldown.type}}:</strong></span>
@@ -27,7 +20,7 @@
<div>
<div class="grafana-container container">
<!-- Rows -->
<div class="grafana-row" ng-controller="RowCtrl" ng-repeat="(row_name, row) in dashboard.rows" ng-style="row_style(row)">
<div class="grafana-row" ng-controller="RowCtrl" ng-repeat="(row_name, row) in dashboard.rows" row-height>
<div class="row-control">
<div class="row-control-inner" style="padding:0px;margin:0px;position:relative;">
<div class="row-close" ng-show="row.collapse" data-placement="bottom" >
@@ -98,14 +91,19 @@
</div>
<!-- Panels -->
<div ng-repeat="(name, panel) in row.panels|filter:isPanel" ng-hide="panel.hide" class="panel nospace" ng-style="{'width':(panel.span/1.2)*10+'%'}" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:$index,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver(true)',onOut:'panelMoveOut'}" ng-class="{'dragInProgress':dashboard.$$panelDragging}">
<!-- Content Panel -->
<div style="position:relative">
<grafana-panel type="panel.type" ng-cloak></grafana-panel>
</div>
<div ng-repeat="(name, panel) in row.panels"
class="panel nospace"
style="position:relative"
data-drop="true"
panel-width
ng-model="panel"
data-jqyoui-options
jqyoui-droppable="{index:$index,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver(true)',onOut:'panelMoveOut'}"
ng-class="{'dragInProgress':dashboard.$$panelDragging}">
<grafana-panel type="panel.type" ng-cloak></grafana-panel>
</div>
<div ng-show="rowSpan(row) < 10 && dashboard.$$panelDragging" class="panel" style="margin:5px;width:30%;background:rgba(100,100,100,0.50)" ng-class="{'dragInProgress':dashboard.panelDragging}" ng-style="{height:row.height}" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:row.panels.length,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver',onOut:'panelMoveOut'}">
<div panel-drop-zone class="panel dragInProgress" style="margin:5px;width:30%;background:rgba(100,100,100,0.50)" ng-style="{height:row.height}" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:row.panels.length,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver',onOut:'panelMoveOut'}">
</div>
<div class="clearfix"></div>
+77
View File
@@ -0,0 +1,77 @@
<div class="navbar navbar-static-top">
<div class="navbar-inner">
<div class="container-fluid">
<span class="brand"><img src="img/small.png" bs-tooltip="'Grafana'" data-placement="bottom"> {{dashboard.title}}</span>
<ul class="nav pull-right" ng-controller='DashboardNavCtrl' ng-init="init()">
<li ng-show="dashboardViewState.fullscreen">
<a ng-click="exitFullscreen()">
Back to dashboard
</a>
</li>
<li class="grafana-menu-zoom-out">
<a class='small' ng-click='zoom(2)'>
Zoom Out
</a>
</li>
<li ng-repeat="pulldown in dashboard.nav" ng-controller="PulldownCtrl" ng-show="pulldown.enable">
<grafana-simple-panel type="pulldown.type" ng-cloak>
</grafana-simple-panel>
</li>
<li class="dropdown grafana-menu-save">
<a bs-tooltip="'Save'" data-placement="bottom" class="dropdown-toggle" data-toggle="dropdown" ng-click="openSaveDropdown()">
<i class='icon-save'></i>
</a>
<ul class="save-dashboard-dropdown dropdown-menu" ng-if="saveDropdownOpened">
<li>
<form class="input-prepend nomargin save-dashboard-dropdown-save-form">
<input class='input-medium' ng-model="dashboard.title" type="text" />
<button class="btn" ng-click="saveDashboard()"><i class="icon-save"></i></button>
</form>
</li>
<li>
<a class="link" ng-click="set_default()">Save as Home</a>
</li>
<li>
<a class="link" ng-click="purge_default()">Reset Home</a>
</li>
<li ng-show="!isFavorite">
<a class="link" ng-click="markAsFavorite()">Mark as favorite</a>
</li>
<li ng-show="isFavorite">
<a class="link" ng-click="removeAsFavorite()">Remove as favorite</a>
</li>
<li>
<a class="link" ng-click="exportDashboard()">Export dashboard</a>
</li>
<li ng-show="db.saveTemp">
<a bs-tooltip="'Share'" data-placement="bottom" ng-click="saveForSharing()" config-modal="app/partials/dashLoaderShare.html">
Share temp copy
</a>
</li>
</ul>
</li>
<li class="dropdown grafana-menu-load" ng-controller="SearchCtrl" ng-init="init()" ng-include="'app/partials/search.html'">
</li>
<li class="grafana-menu-home"><a bs-tooltip="'Goto saved default'" data-placement="bottom" href='#/'><i class='icon-home'></i></a></li>
<li class="grafana-menu-edit" ng-show="dashboard.editable" bs-tooltip="'Configure dashboard'" data-placement="bottom"><a class="link" config-modal="app/partials/dasheditor.html"><i class='icon-cog pointer'></i></a></li>
<li class="grafana-menu-stop-playlist hide">
<a class='small' ng-click='stopPlaylist(2)'>
Stop playlist
</a>
</li>
</ul>
</div>
</div>
</div>
+1 -1
View File
@@ -68,7 +68,7 @@
</div>
</div>
<div ng-if="editor.index == 2" ng-controller="dashLoader">
<div ng-if="editor.index == 2">
<div class="editor-row">
<div class="section">
<h5>Feature toggles</h5>
-4
View File
@@ -1,4 +0,0 @@
<div style="margin-top:50px" ng-controller="dashcontrol">
<strong>type: </strong>{{type}} <br>
<strong>id: </strong>{{id}} <br>
</div>
+2 -17
View File
@@ -2,7 +2,7 @@
<div class="pull-right editor-title">Row settings</div>
<div ng-model="editor.index" bs-tabs>
<div ng-repeat="tab in ['General','Panels','Add Panel']" data-title="{{tab}}">
<div ng-repeat="tab in ['General','Panels']" data-title="{{tab}}">
</div>
</div>
@@ -27,11 +27,10 @@
<thead>
<th>Title</th>
<th>Type</th>
<th>Span <span class="small">({{rowSpan(row)}}/12)</span></th>
<th>Span <span class="small">({{dashboard.rowSpan(row)}}/12)</span></th>
<th>Delete</th>
<th>Move</th>
<th></th>
<th>Hide</th>
</thead>
<tr ng-repeat="panel in row.panels">
<td>{{panel.title}}</td>
@@ -40,24 +39,10 @@
<td><i ng-click="row.panels = _.without(row.panels,panel)" class="pointer icon-remove"></i></td>
<td><i ng-click="_.move(row.panels,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
<td><i ng-click="_.move(row.panels,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
<td><input type="checkbox" ng-model="panel.hide" ng-checked="panel.hide"></td>
</tr>
</table>
</div>
</div>
<div class="row-fluid" ng-if="editor.index == 2">
<h4>Select Panel Type</h4>
<form class="form-inline">
<select class="input-medium" ng-model="panel.type" ng-options="panelType for panelType in availablePanels|stringSort"></select>
<small ng-show="rowSpan(row) > 11">
Note: This row is full, new panels will wrap to a new line. You should add another row.
</small>
</form>
<div ng-show="!(_.isUndefined(panel.type))">
<div add-panel="{{panel.type}}"></div>
</div>
</div>
</div>
<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>
+39 -43
View File
@@ -16,7 +16,7 @@
<i class='icon-folder-open'></i>
</a>
<ul class="dropdown-menu" id="grafana-search">
<ul class="dropdown-menu" id="grafana-search" ng-if="searchOpened">
<li ng-if="!showImport">
<div class="grafana-search-panel">
<div class="search-field-wrapper">
@@ -47,50 +47,46 @@
<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>
<div class="search-results-container" ng-if="tagsOnly">
<div ng-repeat="tag in results.tags"
class="search-result-item pointer"
ng-class="{'selected': $index === selectedIndex }"
ng-click="filterByTag(tag.term, $event)">
<a class="search-result-link" >
<i class="icon icon-tag"></i>
<span class="label label-tag">{{tag.term}} &nbsp;({{tag.count}})</span>
</a>
</div>
</div>
<table class="table table-condensed table-striped" ng-if="!tagsOnly">
<tbody style="max-height: 570px; overflow: auto; display: block">
<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>
<td class="grafana-search-metric-name">
{{row.id}}
</td>
<td style="width:100%;">
<div class="grafana-search-metric-actions">
<a ng-click="addMetricToCurrentDashboard(row.id)"><i class="icon-plus-sign"></i> Add to dashboard</a>
</div>
</td>
</tr>
<div class="search-results-container" ng-if="!tagsOnly">
<div class="search-result-item pointer"
bindonce ng-repeat="row in results.dashboards"
ng-class="{'selected': $index === selectedIndex }" ng-click="goToDashboard(row.id)">
<div class="search-result-actions">
<a ng-click="shareDashboard(row.id, row.id, $event)" config-modal="app/partials/dashLoaderShare.html">
<i class="icon-share"></i> share &nbsp;&nbsp;&nbsp;
</a>
<a ng-click="deleteDashboard(row.id, $event)">
<i class="icon-remove"></i> delete
</a>
</div>
<div class="search-result-tags">
<a ng-click="filterByTag(tag, $event)" ng-repeat="tag in row.tags" style="margin-right: 5px;" class="label label-tag">
{{tag}}
</a>
</div>
<a class="search-result-link">
<i class="icon icon-th-large"></i>
<span bo-text="row.id"></span>
</a>
</div>
</div>
<tr bindonce
ng-repeat="row in results.dashboards"
ng-class="{'selected': $index === selectedIndex }">
<td><a ng-click="deleteDashboard(row.id)"><i class="icon-remove"></i></a></td>
<td style="width:100%">
<a href="#/dashboard/db/{{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.tags" style="margin-right: 5px;" class="label label-tag">
{{tag}}
</a>
</td>
<td><a><i class="icon-share" ng-click="shareDashboard(row.id, row.id)" config-modal="app/partials/dashLoaderShare.html"></i></a></td>
</tr>
</tbody>
</table>
</div>
</li>
+4 -8
View File
@@ -1,8 +1,9 @@
define([
'angular',
'config'
'config',
'store'
],
function (angular, config) {
function (angular, config, store) {
"use strict";
var module = angular.module('grafana.routes');
@@ -11,12 +12,7 @@ function (angular, config) {
$routeProvider
.when('/', {
redirectTo: function() {
if (window.localStorage && window.localStorage.grafanaDashboardDefault) {
return window.localStorage.grafanaDashboardDefault;
}
else {
return config.default_route;
}
return store.get('grafanaDashboardDefault') || config.default_route;
}
});
});
+1
View File
@@ -11,6 +11,7 @@ function (angular) {
.when('/dashboard/db/:id', {
templateUrl: 'app/partials/dashboard.html',
controller : 'DashFromDBProvider',
reloadOnSearch: false,
})
.when('/dashboard/elasticsearch/:id', {
templateUrl: 'app/partials/dashboard.html',
+1
View File
@@ -11,5 +11,6 @@ define([
'./unsavedChangesSrv',
'./dashboard/dashboardKeyBindings',
'./dashboard/dashboardSrv',
'./dashboard/dashboardViewStateSrv',
],
function () {});
@@ -12,28 +12,14 @@ function(angular, $) {
this.shortcuts = function(scope) {
scope.onAppEvent('panel-fullscreen-enter', function() {
$rootScope.fullscreen = true;
});
scope.onAppEvent('panel-fullscreen-exit', function() {
$rootScope.fullscreen = false;
});
scope.onAppEvent('dashboard-saved', function() {
if ($rootScope.fullscreen) {
scope.emitAppEvent('panel-fullscreen-exit');
}
});
scope.$on('$destroy', function() {
keyboardManager.unbind('ctrl+f');
keyboardManager.unbind('ctrl+h');
keyboardManager.unbind('ctrl+s');
keyboardManager.unbind('ctrl+r');
keyboardManager.unbind('ctrl+z');
keyboardManager.unbind('esc');
});
keyboardManager.unbind('esc');
keyboardManager.bind('ctrl+f', function(evt) {
scope.emitAppEvent('open-search', evt);
@@ -67,7 +53,7 @@ function(angular, $) {
modalData.$scope.dismiss();
}
scope.emitAppEvent('panel-fullscreen-exit');
scope.exitFullscreen();
}, { inputDisabled: true });
};
});
+146 -40
View File
@@ -3,14 +3,15 @@ define([
'jquery',
'kbn',
'lodash',
'moment',
'../timer',
],
function (angular, $, kbn, _) {
function (angular, $, kbn, _, moment) {
'use strict';
var module = angular.module('grafana.services');
module.service('dashboardSrv', function(timer, $rootScope, $timeout) {
module.factory('dashboardSrv', function(timer, $rootScope, $timeout) {
function DashboardModel (data) {
@@ -29,6 +30,8 @@ function (angular, $, kbn, _) {
this.time = data.time || { from: 'now-6h', to: 'now' };
this.templating = data.templating || { list: [] };
this.refresh = data.refresh;
this.version = data.version || 0;
this.$state = data.$state;
if (this.nav.length === 0) {
this.nav.push({ type: 'timepicker' });
@@ -47,6 +50,73 @@ function (angular, $, kbn, _) {
var p = DashboardModel.prototype;
p.getNextPanelId = function() {
var i, j, row, panel, max = 0;
for (i = 0; i < this.rows.length; i++) {
row = this.rows[i];
for (j = 0; j < row.panels.length; j++) {
panel = row.panels[j];
if (panel.id > max) { max = panel.id; }
}
}
return max + 1;
};
p.rowSpan = function(row) {
return _.reduce(row.panels, function(p,v) {
return p + v.span;
},0);
};
p.add_panel = function(panel, row) {
var rowSpan = this.rowSpan(row);
var panelCount = row.panels.length;
var space = (12 - rowSpan) - panel.span;
panel.id = this.getNextPanelId();
// try to make room of there is no space left
if (space <= 0) {
if (panelCount === 1) {
row.panels[0].span = 6;
panel.span = 6;
}
else if (panelCount === 2) {
row.panels[0].span = 4;
row.panels[1].span = 4;
panel.span = 4;
}
}
row.panels.push(panel);
};
p.duplicatePanel = function(panel, row) {
var rowIndex = _.indexOf(this.rows, row);
var newPanel = angular.copy(panel);
newPanel.id = this.getNextPanelId();
while(rowIndex < this.rows.length) {
var currentRow = this.rows[rowIndex];
if (this.rowSpan(currentRow) <= 9) {
currentRow.panels.push(newPanel);
return;
}
rowIndex++;
}
var newRow = angular.copy(row);
newRow.panels = [newPanel];
this.rows.push(newRow);
};
p.formatDate = function(date, format) {
format = format || 'YYYY-MM-DD HH:mm:ss';
return this.timezone === 'browser' ?
moment(date).format(format) :
moment.utc(date).format(format);
};
p.emit_refresh = function() {
$rootScope.$broadcast('refresh');
};
@@ -74,61 +144,97 @@ function (angular, $, kbn, _) {
};
p.updateSchema = function(old) {
var i, j, row, panel;
var isChanged = false;
var oldVersion = this.version;
var panelUpgrades = [];
this.version = 4;
if (this.version === 2) {
if (oldVersion === 4) {
return;
}
if (old.services) {
if (old.services.filter) {
this.time = old.services.filter.time;
this.templating.list = old.services.filter.list;
}
delete this.services;
}
// version 2 schema changes
if (oldVersion < 2) {
for (i = 0; i < this.rows.length; i++) {
row = this.rows[i];
for (j = 0; j < row.panels.length; j++) {
panel = row.panels[j];
if (old.services) {
if (old.services.filter) {
this.time = old.services.filter.time;
this.templating.list = old.services.filter.list;
}
delete this.services;
}
panelUpgrades.push(function(panel) {
// rename panel type
if (panel.type === 'graphite') {
panel.type = 'graph';
isChanged = true;
}
if (panel.type === 'graph') {
if (_.isBoolean(panel.legend)) {
panel.legend = { show: panel.legend };
if (panel.type !== 'graph') {
return;
}
if (_.isBoolean(panel.legend)) { panel.legend = { show: panel.legend }; }
if (panel.grid) {
if (panel.grid.min) {
panel.grid.leftMin = panel.grid.min;
delete panel.grid.min;
}
if (panel.grid) {
if (panel.grid.min) {
panel.grid.leftMin = panel.grid.min;
delete panel.grid.min;
}
if (panel.grid.max) {
panel.grid.leftMax = panel.grid.max;
delete panel.grid.max;
}
if (panel.grid.max) {
panel.grid.leftMax = panel.grid.max;
delete panel.grid.max;
}
}
if (panel.y_format) {
panel.y_formats[0] = panel.y_format;
delete panel.y_format;
}
if (panel.y_format) {
panel.y_formats[0] = panel.y_format;
delete panel.y_format;
}
if (panel.y2_format) {
panel.y_formats[1] = panel.y2_format;
delete panel.y2_format;
}
if (panel.y2_format) {
panel.y_formats[1] = panel.y2_format;
delete panel.y2_format;
}
});
}
// schema version 3 changes
if (oldVersion < 3) {
// ensure panel ids
var maxId = this.getNextPanelId();
panelUpgrades.push(function(panel) {
if (!panel.id) {
panel.id = maxId;
maxId += 1;
}
});
}
// schema version 4 changes
if (oldVersion < 4) {
// move aliasYAxis changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'graph') { return; }
_.each(panel.aliasYAxis, function(value, key) {
panel.seriesOverrides = [{ alias: key, yaxis: value }];
});
delete panel.aliasYAxis;
});
}
if (panelUpgrades.length === 0) {
return;
}
for (var i = 0; i < this.rows.length; i++) {
var row = this.rows[i];
for (var j = 0; j < row.panels.length; j++) {
for (var k = 0; k < panelUpgrades.length; k++) {
panelUpgrades[k](row.panels[j]);
}
}
}
this.version = 2;
};
return {
@@ -0,0 +1,156 @@
define([
'angular',
'lodash',
'jquery',
],
function (angular, _, $) {
'use strict';
var module = angular.module('grafana.services');
module.factory('dashboardViewStateSrv', function($location, $timeout) {
// represents the transient view state
// like fullscreen panel & edit
function DashboardViewState($scope) {
var self = this;
$scope.exitFullscreen = function() {
self.update({ fullscreen: false });
};
$scope.onAppEvent('dashboard-saved', function() {
self.update({ fullscreen: false });
});
$scope.onAppEvent('$routeUpdate', function() {
var urlState = self.getQueryStringState();
if (self.needsSync(urlState)) {
self.update(urlState, true);
}
});
this.panelScopes = [];
this.$scope = $scope;
this.update(this.getQueryStringState(), true);
}
DashboardViewState.prototype.needsSync = function(urlState) {
if (urlState.fullscreen !== this.fullscreen) { return true; }
if (urlState.edit !== this.edit) { return true; }
if (urlState.panelId !== this.panelId) { return true; }
return false;
};
DashboardViewState.prototype.getQueryStringState = function() {
var queryParams = $location.search();
return {
panelId: parseInt(queryParams.panelId) || null,
fullscreen: queryParams.fullscreen ? true : false,
edit: queryParams.edit ? true : false
};
};
DashboardViewState.prototype.update = function(state, skipUrlSync) {
_.extend(this, state);
if (!this.fullscreen) {
this.panelId = null;
this.edit = false;
}
if (!skipUrlSync) {
$location.search({
fullscreen: this.fullscreen ? true : null,
panelId: this.panelId,
edit: this.edit ? true : null
});
}
this.syncState();
};
DashboardViewState.prototype.syncState = function() {
if (this.panelScopes.length === 0) { return; }
if (this.fullscreen) {
if (this.fullscreenPanel) {
this.leaveFullscreen(false);
}
var panelScope = this.getPanelScope(this.panelId);
this.enterFullscreen(panelScope);
return;
}
if (this.fullscreenPanel) {
this.leaveFullscreen(true);
}
};
DashboardViewState.prototype.getPanelScope = function(id) {
return _.find(this.panelScopes, function(panelScope) {
return panelScope.panel.id === id;
});
};
DashboardViewState.prototype.leaveFullscreen = function(render) {
var self = this;
self.fullscreenPanel.editMode = false;
self.fullscreenPanel.fullscreen = false;
delete self.fullscreenPanel.height;
if (!render) { return false;}
$timeout(function() {
if (self.oldTimeRange !== self.fullscreenPanel.range) {
self.$scope.dashboard.emit_refresh();
}
else {
self.fullscreenPanel.$emit('render');
}
delete self.fullscreenPanel;
});
};
DashboardViewState.prototype.enterFullscreen = function(panelScope) {
var docHeight = $(window).height();
var editHeight = Math.floor(docHeight * 0.3);
var fullscreenHeight = Math.floor(docHeight * 0.7);
this.oldTimeRange = panelScope.range;
panelScope.height = this.edit ? editHeight : fullscreenHeight;
panelScope.editMode = this.edit;
this.fullscreenPanel = panelScope;
$(window).scrollTop(0);
panelScope.fullscreen = true;
$timeout(function() {
panelScope.$emit('render');
});
};
DashboardViewState.prototype.registerPanel = function(panelScope) {
var self = this;
self.panelScopes.push(panelScope);
if (self.panelId === panelScope.panel.id) {
self.enterFullscreen(panelScope);
}
panelScope.$on('$destroy', function() {
self.panelScopes = _.without(self.panelScopes, panelScope);
});
};
return {
create: function($scope) {
return new DashboardViewState($scope);
}
};
});
});
+2
View File
@@ -68,6 +68,8 @@ function (angular, _, config) {
case 'grafana':
Datasource = $injector.get('GrafanaDatasource');
break;
default:
Datasource = $injector.get(ds.type);
}
return new Datasource(ds);
};
@@ -150,7 +150,7 @@ function (angular, _, $, config, kbn, moment) {
if (rounding === 'round-up') {
if (date.get('s')) {
date.add('m', 1);
date.add(1, 'm');
}
}
else if (rounding === 'round-down') {
@@ -159,7 +159,7 @@ function (angular, _, $, config, kbn, moment) {
// to guarantee that we get all the data that
// exists for the specified range
if (date.get('s')) {
date.subtract('m', 1);
date.subtract(1, 'm');
}
}
+3
View File
@@ -124,6 +124,9 @@ define([
i === 45 || // -
i === 42 || // *
i === 58 || // :
i === 91 || // templateStart [
i === 93 || // templateEnd ]
i === 63 || // ?
i === 37 || // %
i >= 97 && i <= 122; // a-z
}
+32 -58
View File
@@ -1,9 +1,8 @@
define([
'angular',
'lodash',
'jquery',
],
function (angular, _, $) {
function (angular, _) {
'use strict';
var module = angular.module('grafana.services');
@@ -22,12 +21,12 @@ function (angular, _, $) {
},
{
text: 'Edit',
click: "toggleFullscreenEdit()",
click: "toggleFullscreen(true)",
condition: $scope.panelMeta.fullscreenEdit
},
{
text: "Fullscreen",
click: 'toggleFullscreen()',
click: 'toggleFullscreen(false)',
condition: $scope.panelMeta.fullscreenView
},
{
@@ -71,46 +70,6 @@ function (angular, _, $) {
});
};
$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.emit_refresh();
}
else {
$scope.$emit('render');
}
});
});
}
$(window).scrollTop(0);
$scope.fullscreen = true;
$rootScope.$emit('panel-fullscreen-enter');
$timeout(function() {
$scope.$emit('render');
});
};
$scope.addDataQuery = function() {
$scope.panel.targets.push({target: ''});
};
@@ -135,22 +94,12 @@ function (angular, _, $) {
$scope.get_data();
};
$scope.toggleFullscreenEdit = function() {
if ($scope.editMode) {
$rootScope.$emit('panel-fullscreen-exit');
return;
}
$scope.enterFullscreenMode({edit: true});
$scope.toggleFullscreen = function(edit) {
$scope.dashboardViewState.update({ fullscreen: true, edit: edit, panelId: $scope.panel.id });
};
$scope.toggleFullscreen = function() {
if ($scope.fullscreen && !$scope.editMode) {
$rootScope.$emit('panel-fullscreen-exit');
return;
}
$scope.enterFullscreenMode({ edit: false });
$scope.otherPanelInFullscreenMode = function() {
return $scope.dashboardViewState.fullscreen && !$scope.fullscreen;
};
// Post init phase
@@ -162,6 +111,31 @@ function (angular, _, $) {
$scope.datasources = datasourceSrv.getMetricSources();
$scope.setDatasource($scope.panel.datasource);
$scope.dashboardViewState.registerPanel($scope);
if ($scope.get_data) {
var panel_get_data = $scope.get_data;
$scope.get_data = function() {
if ($scope.otherPanelInFullscreenMode()) { return; }
delete $scope.panel.error;
$scope.panelMeta.loading = true;
panel_get_data();
};
if (!$scope.skipDataOnInit) {
$scope.get_data();
}
}
if ($rootScope.profilingEnabled) {
$rootScope.performance.panelsInitialized++;
if ($rootScope.performance.panelsInitialized === $scope.dashboard.rows.length) {
$rootScope.performance.allPanelsInitialized = new Date().getTime();
}
}
};
});
+6 -4
View File
@@ -1,9 +1,10 @@
define([
'angular',
'lodash',
'kbn'
'kbn',
'store'
],
function (angular, _, kbn) {
function (angular, _, kbn, store) {
'use strict';
var module = angular.module('grafana.services');
@@ -13,14 +14,14 @@ function (angular, _, kbn) {
var favorites = { dashboards: [] };
this.init = function() {
var existingJson = window.localStorage["grafana-favorites"];
var existingJson = store.get("grafana-favorites");
if (existingJson) {
favorites = angular.fromJson(existingJson);
}
};
this._save = function() {
window.localStorage["grafana-favorites"] = angular.toJson(favorites);
store.set('grafana-favorites', angular.toJson(favorites));
};
this._find = function(title) {
@@ -68,6 +69,7 @@ function (angular, _, kbn) {
timerInstance = setInterval(function() {
$rootScope.$apply(function() {
angular.element(window).unbind('resize');
$location.search({});
$location.path(dashboards[index % dashboards.length].url);
index++;
});
+6
View File
@@ -28,10 +28,12 @@ function(angular, _, config) {
$rootScope.$on("dashboard-saved", function(event, savedDashboard) {
self.original = angular.copy(savedDashboard);
self.current = savedDashboard;
self.orignalPath = $location.path();
});
$rootScope.$on("$routeChangeSuccess", function() {
self.original = null;
self.originalPath = $location.path();
});
window.onbeforeunload = function() {
@@ -42,6 +44,10 @@ function(angular, _, config) {
this.init = function() {
$rootScope.$on("$locationChangeStart", function(event, next) {
if (self.originalPath === $location.path()) {
return;
}
if (self.has_unsaved_changes()) {
event.preventDefault();
self.next = next;
+5 -1
View File
@@ -96,7 +96,11 @@ function (Settings) {
// Add your own custom pannels
plugins: {
panels: []
// list of plugin panels
panels: [],
// requirejs modules in plugins folder that should be loaded
// for example custom datasources
dependencies: [],
}
});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+44 -12
View File
@@ -35,9 +35,8 @@
// Search
.grafana-search-panel {
padding: 6px 10px;
.search-field-wrapper {
padding: 6px 10px;
input {
width: 100%;
}
@@ -50,25 +49,58 @@
padding-right: 25px;
}
}
}
.selected td, tr.selected:nth-child(odd)>td {
background: @blue;
color: white;
text-shadow: -1px -1px 1px rgba(0,0,0,0.3);
a {
color: white;
.search-results-container {
max-height: 600px;
overflow: auto;
display: block;
.search-result-item a {
}
.search-result-item:hover, .search-result-item.selected {
.search-result-link, .icon {
color: @grafanaListHighlight;
}
.search-result-link .label {
background-color: @blue;
}
}
.selected-tag .label-tag {
background-color: @blue;
.search-result-link {
color: @grafanaListMainLinkColor;
.icon {
padding-right: 10px;
color: @grafanaListHighlightContrast;
}
}
.search-result-item:nth-child(odd) {
background-color: @grafanaListAccent;
}
.search-result-item {
padding: 6px 10px;
white-space: nowrap;
border-top: 1px solid @grafanaListBorderTop;
border-bottom: 1px solid @grafanaListBorderBottom;
}
.search-result-tags {
float: right;
}
.search-result-actions {
float: right;
padding-left: 10px;
}
}
.search-tagview-switch {
position: absolute;
top: 15px;
right: 263px;
right: 266px;
color: darken(@linkColor, 30%);
&.active {
color: @linkColor;
@@ -155,7 +187,7 @@
}
.panel-full-edit-tabs {
margin-top: 10px;
margin-top: 30px;
min-height: 250px;
margin-left: -10px;
margin-right: -10px;
+17 -1
View File
@@ -4,7 +4,7 @@
.graph-legend {
margin: 0 20px;
text-align: left;
text-align: center;
position: relative;
top: 2px;
@@ -25,6 +25,7 @@
.graph-legend-value {
float: left;
white-space: nowrap;
text-align: left;
&.current:before {
content: "Current: "
}
@@ -44,6 +45,7 @@
.graph-legend-series {
padding-left: 10px;
padding-top: 2px;
}
.graph-legend-value {
@@ -152,3 +154,17 @@
.annotation-tags {
color: @purple;
}
.graph-series-override {
input {
float: left;
margin-right: 10px;
}
.graph-series-override-option {
float: left;
padding: 2px 6px;
}
.graph-series-override-selector {
float: left;
}
}
+9 -1
View File
@@ -57,7 +57,7 @@
// Links
// -------------------------
@linkColor: darken(@white,5%);
@linkColor: darken(@white,11%);
@linkColorHover: @white;
@@ -93,6 +93,14 @@
@borderRadiusLarge: 4px;
@borderRadiusSmall: 2px;
// Lists
@grafanaListBackground: transparent;
@grafanaListAccent: #232323;
@grafanaListBorderTop: #3E3E3E;
@grafanaListBorderBottom: #1c1919;
@grafanaListHighlight: @blue;
@grafanaListHighlightContrast: #4F4F4F;
@grafanaListMainLinkColor: @linkColor;
// Tables
// -------------------------
+10 -1
View File
@@ -20,7 +20,7 @@
// Accent colors
// -------------------------
@blue: #01A6E6;
@blue: #007FFF;
@blueDark: #75CAEB;
@green: #28B62C;
@red: #FF4136;
@@ -97,6 +97,15 @@
@borderRadiusLarge: 4px;
@borderRadiusSmall: 2px;
// Lists
@grafanaListBackground: transparent;
@grafanaListAccent: #f9f9f9;
@grafanaListBorderTop: #eee;
@grafanaListBorderBottom: #efefef;
@grafanaListHighlight: @blue;
@grafanaListHighlightContrast: #ddd;
@grafanaListMainLinkColor: @textColor;
// Tables
// -------------------------
+1 -1
View File
@@ -27,7 +27,7 @@
<strong>{{alert.title}}</strong> <span ng-bind-html='alert.text'></span> <div style="padding-right:10px" class='pull-right small'> {{$index + 1}} alert(s) </div>
</div>
<div ng-view ng-class="{'dashboard-fullscreen': fullscreen}"></div>
<div ng-view></div>
</body>
</html>
+55
View File
@@ -0,0 +1,55 @@
define([
'angular',
'lodash',
'kbn',
'moment'
],
function (angular, _, kbn) {
'use strict';
var module = angular.module('grafana.services');
module.factory('CustomDatasource', function($q) {
// the datasource object passed to constructor
// is the same defined in config.js
function CustomDatasource(datasource) {
this.name = datasource.name;
this.supportMetrics = true;
this.url = datasource.url;
}
CustomDatasource.prototype.query = function(filterSrv, options) {
// get from & to in seconds
var from = kbn.parseDate(options.range.from).getTime() / 1000;
var to = kbn.parseDate(options.range.to).getTime() / 1000;
var series = [];
var stepInSeconds = (to - from) / options.maxDataPoints;
for (var i = 0; i < 3; i++) {
var walker = Math.random() * 100;
var time = from;
var timeSeries = {
target: "Series " + i,
datapoints: []
};
for (var j = 0; j < options.maxDataPoints; j++) {
timeSeries.datapoints[j] = [walker, time];
walker += Math.random() - 0.5;
time += stepInSeconds;
}
series.push(timeSeries);
}
return $q.when({data: series });
};
return CustomDatasource;
});
});
+71 -4
View File
@@ -7,7 +7,6 @@ define([
var model;
beforeEach(module('grafana.services'));
beforeEach(inject(function(dashboardSrv) {
model = dashboardSrv.create({});
}));
@@ -24,12 +23,71 @@ define([
});
describe('when getting next panel id', function() {
var model;
beforeEach(module('grafana.services'));
beforeEach(inject(function(dashboardSrv) {
model = dashboardSrv.create({
rows: [{ panels: [{ id: 5 }]}]
});
}));
it('should return max id + 1', function() {
expect(model.getNextPanelId()).to.be(6);
});
});
describe('row and panel manipulation', function() {
var dashboard;
beforeEach(module('grafana.services'));
beforeEach(inject(function(dashboardSrv) {
dashboard = dashboardSrv.create({});
}));
it('row span should sum spans', function() {
var spanLeft = dashboard.rowSpan({ panels: [{ span: 2 }, { span: 3 }] });
expect(spanLeft).to.be(5);
});
it('adding default should split span in half', function() {
dashboard.rows = [{ panels: [{ span: 12, id: 7 }] }];
dashboard.add_panel({span: 4}, dashboard.rows[0]);
expect(dashboard.rows[0].panels[0].span).to.be(6);
expect(dashboard.rows[0].panels[1].span).to.be(6);
expect(dashboard.rows[0].panels[1].id).to.be(8);
});
it('duplicate panel should try to add it to same row', function() {
var panel = { span: 4, attr: '123', id: 10 };
dashboard.rows = [{ panels: [panel] }];
dashboard.duplicatePanel(panel, dashboard.rows[0]);
expect(dashboard.rows[0].panels[0].span).to.be(4);
expect(dashboard.rows[0].panels[1].span).to.be(4);
expect(dashboard.rows[0].panels[1].attr).to.be('123');
expect(dashboard.rows[0].panels[1].id).to.be(11);
});
it('duplicate should add row if there is no space left', function() {
var panel = { span: 12, attr: '123' };
dashboard.rows = [{ panels: [panel] }];
dashboard.duplicatePanel(panel, dashboard.rows[0]);
expect(dashboard.rows[0].panels[0].span).to.be(12);
expect(dashboard.rows[0].panels.length).to.be(1);
expect(dashboard.rows[1].panels[0].attr).to.be('123');
});
});
describe('when creating dashboard with old schema', function() {
var model;
var graph;
beforeEach(module('grafana.services'));
beforeEach(inject(function(dashboardSrv) {
model = dashboardSrv.create({
services: { filter: { time: { from: 'now-1d', to: 'now'}, list: [1] }},
@@ -39,6 +97,7 @@ define([
{
type: 'graphite',
legend: true,
aliasYAxis: { test: 2 },
grid: { min: 1, max: 10 }
}
]
@@ -54,6 +113,10 @@ define([
expect(model.title).to.be('No Title');
});
it('should have panel id', function() {
expect(graph.id).to.be(1);
});
it('should move time and filtering list', function() {
expect(model.time.from).to.be('now-1d');
expect(model.templating.list[0]).to.be(1);
@@ -72,11 +135,15 @@ define([
expect(graph.grid.leftMax).to.be(10);
});
it('move aliasYAxis to series override', function() {
expect(graph.seriesOverrides[0].alias).to.be("test");
expect(graph.seriesOverrides[0].yaxis).to.be(2);
});
it('dashboard schema version should be set to latest', function() {
expect(model.version).to.be(2);
expect(model.version).to.be(4);
});
});
});
@@ -0,0 +1,37 @@
define([
'services/dashboard/dashboardViewStateSrv'
], function() {
'use strict';
describe('when updating view state', function() {
var viewState, location;
beforeEach(module('grafana.services'));
beforeEach(inject(function(dashboardViewStateSrv, $location, $rootScope) {
$rootScope.onAppEvent = function(){};
viewState = dashboardViewStateSrv.create($rootScope);
location = $location;
}));
describe('to fullscreen true and edit true', function() {
it('should update querystring and view state', function() {
var updateState = { fullscreen: true, edit: true, panelId: 1 };
viewState.update(updateState);
expect(location.search()).to.eql(updateState);
expect(viewState.fullscreen).to.be(true);
});
});
describe('to fullscreen false', function() {
it('should remove params from query string', function() {
viewState.update({fullscreen: true, panelId: 1, edit: true});
viewState.update({fullscreen: false});
expect(location.search()).to.eql({});
expect(viewState.fullscreen).to.be(false);
});
});
});
});
+147
View File
@@ -0,0 +1,147 @@
define([
'./helpers',
'angular',
'jquery',
'components/timeSeries',
'directives/grafanaGraph'
], function(helpers, angular, $, TimeSeries) {
'use strict';
describe('grafanaGraph', function() {
beforeEach(module('grafana.directives'));
function graphScenario(desc, func) {
describe(desc, function() {
var ctx = {};
ctx.setup = function (setupFunc) {
beforeEach(inject(function($rootScope, $compile) {
var scope = $rootScope.$new();
var element = angular.element("<div style='width:500px' grafana-graph><div>");
scope.height = '200px';
scope.panel = {
legend: {},
grid: {},
y_formats: [],
seriesOverrides: []
};
scope.dashboard = { timezone: 'browser' };
scope.range = {
from: new Date('2014-08-09 10:00:00'),
to: new Date('2014-09-09 13:00:00')
};
ctx.data = [];
ctx.data.push(new TimeSeries({
datapoints: [[1,1],[2,2]],
info: { alias: 'series1', enable: true }
}));
ctx.data.push(new TimeSeries({
datapoints: [[1,1],[2,2]],
info: { alias: 'series2', enable: true }
}));
setupFunc(scope, ctx.data);
$compile(element)(scope);
scope.$digest();
$.plot = ctx.plotSpy = sinon.spy();
scope.$emit('render', ctx.data);
ctx.plotData = ctx.plotSpy.getCall(0).args[1];
ctx.plotOptions = ctx.plotSpy.getCall(0).args[2];
}));
};
func(ctx);
});
}
graphScenario('simple lines options', function(ctx) {
ctx.setup(function(scope) {
scope.panel.lines = true;
scope.panel.fill = 5;
scope.panel.linewidth = 3;
scope.panel.steppedLine = true;
});
it('should configure plot with correct options', function() {
expect(ctx.plotOptions.series.lines.show).to.be(true);
expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
expect(ctx.plotOptions.series.lines.lineWidth).to.be(3);
expect(ctx.plotOptions.series.lines.steps).to.be(true);
});
});
graphScenario('grid thresholds 100, 200', function(ctx) {
ctx.setup(function(scope) {
scope.panel.grid = {
threshold1: 100,
threshold1Color: "#111",
threshold2: 200,
threshold2Color: "#222",
};
});
it('should add grid markings', function() {
var markings = ctx.plotOptions.grid.markings;
expect(markings[0].yaxis.from).to.be(100);
expect(markings[0].yaxis.to).to.be(200);
expect(markings[0].color).to.be('#111');
expect(markings[1].yaxis.from).to.be(200);
expect(markings[1].yaxis.to).to.be(Infinity);
});
});
graphScenario('inverted grid thresholds 200, 100', function(ctx) {
ctx.setup(function(scope) {
scope.panel.grid = {
threshold1: 200,
threshold1Color: "#111",
threshold2: 100,
threshold2Color: "#222",
};
});
it('should add grid markings', function() {
var markings = ctx.plotOptions.grid.markings;
expect(markings[0].yaxis.from).to.be(200);
expect(markings[0].yaxis.to).to.be(100);
expect(markings[0].color).to.be('#111');
expect(markings[1].yaxis.from).to.be(100);
expect(markings[1].yaxis.to).to.be(-Infinity);
});
});
graphScenario('series option overrides, fill & points', function(ctx) {
ctx.setup(function(scope, data) {
scope.panel.lines = true;
scope.panel.fill = 5;
scope.panel.seriesOverrides = [
{ alias: 'test', fill: 0, points: true }
];
data[1].info.alias = 'test';
});
it('should match second series and fill zero, and enable points', function() {
expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
expect(ctx.plotData[1].lines.fill).to.be(0.001);
expect(ctx.plotData[1].points.show).to.be(true);
});
});
graphScenario('should order series order according to zindex', function(ctx) {
ctx.setup(function(scope) {
scope.panel.seriesOverrides = [{ alias: 'series1', zindex: 2 }];
});
it('should move zindex 2 last', function() {
expect(ctx.plotData[0].info.alias).to.be('series2');
expect(ctx.plotData[1].info.alias).to.be('series1');
});
});
});
});
+9 -2
View File
@@ -26,6 +26,8 @@ define([
self.scope.panel = {};
self.scope.row = { panels:[] };
self.scope.filter = new FilterSrvStub();
self.scope.dashboard = {};
self.scope.dashboardViewState = new DashboardViewStateStub();
$rootScope.colors = [];
for (var i = 0; i < 50; i++) { $rootScope.colors.push('#' + i); }
@@ -54,15 +56,20 @@ define([
};
}
function DashboardViewStateStub() {
this.registerPanel = function() {
};
}
function FilterSrvStub() {
this.time = { from:'now-1h', to: 'now'};
this.timeRange = function(parse) {
if (!parse) {
if (parse === false) {
return this.time;
}
return {
from : kbn.parseDate(this.time.from),
to : kbn.parseDate(this.time.to)
to : kbn.parseDate(this.time.to)
};
};
+18
View File
@@ -22,6 +22,24 @@ define([
});
describe('high negative exponent, issue #696', function() {
it('should ignore decimal correction if exponent', function() {
var str = kbn.getFormatFunction('')(2.75e-10, { tickDecimals: 12 });
expect(str).to.be('2.75e-10');
});
it('should format 0 correctly', function() {
var str = kbn.getFormatFunction('')(0.0, { tickDecimals: 12 });
expect(str).to.be('0');
});
});
describe('none format tests', function() {
it('should translate 2 as 2.0000 if axis decimals is 4', function() {
var str = kbn.getFormatFunction('')(2, { tickDecimals: 4 });
expect(str).to.be('2.0000');
});
});
describe('nanosecond formatting', function () {
it('should translate 25 to 25 ns', function () {
+11 -4
View File
@@ -71,10 +71,17 @@ define([
it('should tokenize metric with template parameter', function() {
var lexer = new Lexer("metric.[[server]].test");
var tokens = lexer.tokenize();
expect(tokens[2].type).to.be('templateStart');
expect(tokens[3].type).to.be('identifier');
expect(tokens[3].value).to.be('server');
expect(tokens[4].type).to.be('templateEnd');
expect(tokens[2].type).to.be('identifier');
expect(tokens[2].value).to.be('[[server]]');
expect(tokens[4].type).to.be('identifier');
});
it('should tokenize metric with question mark', function() {
var lexer = new Lexer("metric.server_??.test");
var tokens = lexer.tokenize();
expect(tokens[2].type).to.be('identifier');
expect(tokens[2].value).to.be('server_??');
expect(tokens[4].type).to.be('identifier');
});
it('should handle error with unterminated string', function() {
+2 -2
View File
@@ -106,8 +106,8 @@ define([
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');
expect(rootNode.params[0].segments[1].type).to.be('segment');
expect(rootNode.params[0].segments[1].value).to.be('[[server]]');
});
it('invalid metric expression', function() {
-44
View File
@@ -12,50 +12,6 @@ define([
beforeEach(ctx.providePhase());
beforeEach(ctx.createControllerPhase('RowCtrl'));
describe('when getting rowSpan', function() {
it('should return sum of panels spans', function() {
var spanLeft = ctx.scope.rowSpan({ panels: [{ span: 2 }, { span: 3 }] });
expect(spanLeft).to.be(5);
});
});
describe('when adding panel to row with 12 span panel', function() {
it('should split span in half and add panel with defaults', function() {
ctx.scope.row = { panels: [{ span: 12 }] };
ctx.scope.add_panel_default('graph');
expect(ctx.scope.row.panels[0].span).to.be(6);
expect(ctx.scope.row.panels[1].span).to.be(6);
expect(ctx.scope.row.panels[1].type).to.be('graph');
});
});
describe('when duplicating panel', function() {
it('should try to add it to same row', function() {
var panel = { span: 4, attr: '123' };
ctx.scope.row = { panels: [panel] };
ctx.scope.duplicatePanel(panel, ctx.scope.row);
expect(ctx.scope.row.panels[0].span).to.be(4);
expect(ctx.scope.row.panels[1].span).to.be(4);
expect(ctx.scope.row.panels[1].attr).to.be('123');
});
});
describe('when duplicating panel', function() {
it('should add row if there is no space left', function() {
var panel = { span: 12, attr: '123' };
ctx.scope.row = { panels: [panel] };
ctx.scope.dashboard = { rows: [ctx.scope.row] };
ctx.scope.duplicatePanel(panel, ctx.scope.row);
expect(ctx.scope.row.panels[0].span).to.be(12);
expect(ctx.scope.row.panels.length).to.be(1);
expect(ctx.scope.dashboard.rows[1].panels[0].attr).to.be('123');
});
});
});
});
@@ -0,0 +1,51 @@
define([
'./helpers',
'panels/graph/seriesOverridesCtrl'
], function(helpers) {
'use strict';
describe('SeriesOverridesCtrl', function() {
var ctx = new helpers.ControllerTestContext();
beforeEach(module('grafana.services'));
beforeEach(module('grafana.panels.graph'));
beforeEach(ctx.providePhase());
beforeEach(ctx.createControllerPhase('SeriesOverridesCtrl'));
beforeEach(function() {
ctx.scope.render = function() {};
});
describe('Controller should init overrideMenu', function() {
it('click should include option and value index', function() {
expect(ctx.scope.overrideMenu[1].submenu[1].click).to.be('setOverride(1,1)');
});
});
describe('When setting an override', function() {
beforeEach(function() {
ctx.scope.setOverride(1, 0);
});
it('should set override property', function() {
expect(ctx.scope.override.lines).to.be(true);
});
it('should update view model', function() {
expect(ctx.scope.currentOverrides[0].name).to.be('Lines');
expect(ctx.scope.currentOverrides[0].value).to.be('true');
});
});
describe('When removing overide', function() {
it('click should include option and value index', function() {
ctx.scope.setOverride(1,0);
ctx.scope.removeOverride({ propertyName: 'lines' });
expect(ctx.scope.currentOverrides.length).to.be(0);
});
});
});
});
+115
View File
@@ -0,0 +1,115 @@
define([
'components/timeSeries'
], function(TimeSeries) {
'use strict';
describe("TimeSeries", function() {
var points, series;
var yAxisFormats = ['short', 'ms'];
var testData = {
info: { alias: 'test' },
datapoints: [
[1,2],[null,3],[10,4],[8,5]
]
};
describe('when getting flot pairs', function() {
it('with connected style, should ignore nulls', function() {
series = new TimeSeries(testData);
points = series.getFlotPairs('connected', yAxisFormats);
expect(points.length).to.be(3);
});
it('with null as zero style, should replace nulls with zero', function() {
series = new TimeSeries(testData);
points = series.getFlotPairs('null as zero', yAxisFormats);
expect(points.length).to.be(4);
expect(points[1][1]).to.be(0);
});
});
describe('series overrides', function() {
var series;
beforeEach(function() {
series = new TimeSeries(testData);
});
describe('fill & points', function() {
beforeEach(function() {
series.info.alias = 'test';
series.applySeriesOverrides([{ alias: 'test', fill: 0, points: true }]);
});
it('should set fill zero, and enable points', function() {
expect(series.lines.fill).to.be(0.001);
expect(series.points.show).to.be(true);
});
});
describe('series option overrides, bars, true & lines false', function() {
beforeEach(function() {
series.info.alias = 'test';
series.applySeriesOverrides([{ alias: 'test', bars: true, lines: false }]);
});
it('should disable lines, and enable bars', function() {
expect(series.lines.show).to.be(false);
expect(series.bars.show).to.be(true);
});
});
describe('series option overrides, linewidth, stack', function() {
beforeEach(function() {
series.info.alias = 'test';
series.applySeriesOverrides([{ alias: 'test', linewidth: 5, stack: false }]);
});
it('should disable stack, and set lineWidth', function() {
expect(series.stack).to.be(false);
expect(series.lines.lineWidth).to.be(5);
});
});
describe('series option overrides, pointradius, steppedLine', function() {
beforeEach(function() {
series.info.alias = 'test';
series.applySeriesOverrides([{ alias: 'test', pointradius: 5, steppedLine: true }]);
});
it('should set pointradius, and set steppedLine', function() {
expect(series.points.radius).to.be(5);
expect(series.lines.steps).to.be(true);
});
});
describe('override match on regex', function() {
beforeEach(function() {
series.info.alias = 'test_01';
series.applySeriesOverrides([{ alias: '/.*01/', lines: false }]);
});
it('should match second series', function() {
expect(series.lines.show).to.be(false);
});
});
describe('override series y-axis, and z-index', function() {
beforeEach(function() {
series.info.alias = 'test';
series.applySeriesOverrides([{ alias: 'test', yaxis: 2, zindex: 2 }]);
});
it('should set yaxis', function() {
expect(series.info.yaxis).to.be(2);
});
it('should set zindex', function() {
expect(series.zindex).to.be(2);
});
});
});
});
});
+6 -1
View File
@@ -6,6 +6,7 @@ require.config({
mocks: '../test/mocks',
config: '../config.sample',
kbn: 'components/kbn',
store: 'components/store',
settings: 'components/settings',
lodash: 'components/lodash.extended',
@@ -26,7 +27,7 @@ require.config({
crypto: '../vendor/crypto.min',
spectrum: '../vendor/spectrum',
jquery: '../vendor/jquery/jquery-1.8.0',
jquery: '../vendor/jquery/jquery-2.1.1.min',
bootstrap: '../vendor/bootstrap/bootstrap',
'bootstrap-tagsinput': '../vendor/tagsinput/bootstrap-tagsinput',
@@ -117,14 +118,18 @@ require([
'specs/lexer-specs',
'specs/parser-specs',
'specs/gfunc-specs',
'specs/timeSeries-specs',
'specs/row-ctrl-specs',
'specs/graphiteTargetCtrl-specs',
'specs/influxdb-datasource-specs',
'specs/graph-ctrl-specs',
'specs/grafanaGraph-specs',
'specs/seriesOverridesCtrl-specs',
'specs/filterSrv-specs',
'specs/kbn-format-specs',
'specs/dashboardSrv-specs',
'specs/influxSeries-specs',
'specs/dashboardViewStateSrv-specs',
'specs/overview-ctrl-specs',
], function () {
window.__karma__.start();
+8 -3
View File
@@ -257,9 +257,11 @@ var jqyoui = angular.module('ngDragDrop', []).service('ngDragDropService', ['$ti
require: '?jqyouiDroppable',
restrict: 'A',
link: function(scope, element, attrs) {
var dragSettings, zIndex;
// grafana change, remove watcher after first evaluation
var dragSettings, zIndex, removeWatcher;
var updateDraggable = function(newValue, oldValue) {
if (newValue) {
removeWatcher();
dragSettings = scope.$eval(element.attr('jqyoui-draggable')) || [];
element
.draggable({disabled: false})
@@ -283,7 +285,7 @@ var jqyoui = angular.module('ngDragDrop', []).service('ngDragDropService', ['$ti
element.draggable({disabled: true});
}
};
scope.$watch(function() { return scope.$eval(attrs.drag); }, updateDraggable);
removeWatcher = scope.$watch(function() { return scope.$eval(attrs.drag); }, updateDraggable);
updateDraggable();
}
};
@@ -292,8 +294,11 @@ var jqyoui = angular.module('ngDragDrop', []).service('ngDragDropService', ['$ti
restrict: 'A',
priority: 1,
link: function(scope, element, attrs) {
// grafana change, remove watcher after first evaluation
var removeWatcher;
var updateDroppable = function(newValue, oldValue) {
if (newValue) {
removeWatcher();
element
.droppable({disabled: false})
.droppable(scope.$eval(attrs.jqyouiOptions) || {})
@@ -319,7 +324,7 @@ var jqyoui = angular.module('ngDragDrop', []).service('ngDragDropService', ['$ti
}
};
scope.$watch(function() { return scope.$eval(attrs.drop); }, updateDroppable);
removeWatcher = scope.$watch(function() { return scope.$eval(attrs.drop); }, updateDroppable);
updateDroppable();
}
};
+2 -1
View File
@@ -783,7 +783,8 @@ angular.module('$strap.directives').directive('bsTooltip', [
value = newValue;
}
});
if (!!attrs.unique) {
// Grafana change, always hide other tooltips
if (true) {
element.on('show', function (ev) {
$('.tooltip.in').each(function () {
var $this = $(this), tooltip = $this.data('tooltip');
+3 -2
View File
@@ -753,7 +753,8 @@
if (!selector) {
selector = $this.attr('href')
selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
// grafana backport fix from bootstrap 3
selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
}
$parent = selector && $(selector)
@@ -2319,4 +2320,4 @@
})
}(window.jQuery);
}(window.jQuery);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+869 -461
View File
File diff suppressed because it is too large Load Diff