merged with oss

This commit is contained in:
Torkel Ödegaard committed 2014-08-16 08:56:02 +02:00
commit dac4954215
55 files changed
+892 -412

No files matched your search

+11
View File
@@ -3,12 +3,23 @@
**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.
**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)
**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)
+18 -8
View File
@@ -14,7 +14,7 @@ define([
'extend-jquery',
'bindonce',
],
function (angular, $, _, appLevelRequire) {
function (angular, $, _, appLevelRequire, config) {
"use strict";
@@ -56,6 +56,7 @@ function (angular, $, _, appLevelRequire) {
register_fns.factory = $provide.factory;
register_fns.service = $provide.service;
register_fns.filter = $filterProvider.register;
});
var apps_deps = [
@@ -76,14 +77,23 @@ function (angular, $, _, appLevelRequire) {
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/p_all',
], function () {
require(preBootRequires, function () {
// disable tool tip animation
$.fn.tooltip.defaults.animation = false;
// bootstrap the app
angular
+25 -2
View File
@@ -525,12 +525,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 : decimals);
};
}
};
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) {
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) {
+2 -1
View File
@@ -5,6 +5,7 @@ require.config({
baseUrl: '/public/app',
paths: {
app: 'p_app',
config: ['../config', '../config.sample'],
settings: 'components/settings',
kbn: 'components/kbn',
@@ -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',
+5 -1
View File
@@ -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;
};
});
-14
View File
@@ -15,20 +15,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;
+8 -6
View File
@@ -11,7 +11,9 @@ function (angular, $, config, _) {
var module = angular.module('grafana.controllers');
module.controller('DashCtrl', function(
$scope, $rootScope, dashboardKeybindings, filterSrv, dashboardSrv, panelMoveSrv, timer) {
$scope, $rootScope, dashboardKeybindings,
filterSrv, dashboardSrv, dashboardViewStateSrv,
panelMoveSrv, timer) {
$scope.editor = { index: 0 };
$scope.panelNames = config.panels;
@@ -24,9 +26,13 @@ function (angular, $, config, _) {
$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 +84,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 -3
View File
@@ -14,6 +14,7 @@ function (angular, _, moment, config) {
$scope.init = function() {
$scope.db = datasourceSrv.getGrafanaDB();
$scope.onAppEvent('save-dashboard', function() {
$scope.saveDashboard();
});
@@ -21,10 +22,7 @@ function (angular, _, moment, config) {
$scope.onAppEvent('zoom-out', function() {
$scope.zoom(2);
});
};
$scope.exitFullscreen = function() {
$scope.emitAppEvent('panel-fullscreen-exit');
};
$scope.set_default = function() {
@@ -78,6 +76,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);
@@ -135,6 +134,7 @@ function (angular, _, moment, config) {
$scope.openSaveDropdown = function() {
$scope.isFavorite = playlistSrv.isCurrentFavorite($scope.dashboard);
$scope.saveDropdownOpened = true;
};
$scope.markAsFavorite = function() {
+65 -3
View File
@@ -2,8 +2,9 @@ define([
'angular',
'config',
'lodash',
'jquery',
],
function (angular, config, _) {
function (angular, config, _, $) {
"use strict";
var module = angular.module('grafana.controllers');
@@ -11,15 +12,21 @@ function (angular, config, _) {
module.controller('GrafanaCtrl', function($scope, alertSrv, grafanaVersion, $rootScope) {
$scope.grafanaVersion = grafanaVersion[0] === '@' ? 'master' : grafanaVersion;
$scope.consoleEnabled = (window.localStorage && window.localStorage.grafanaConsole === 'true');
$rootScope.profilingEnabled = (window.localStorage && window.localStorage.profilingEnabled === 'true');
$rootScope.performance = { loadStart: new Date().getTime() };
$scope.init = function() {
$scope._ = _;
if ($rootScope.profilingEnabled) {
$scope.initProfiling();
}
$scope.dashAlerts = alertSrv;
$scope.grafana = {
style: 'dark'
};
$scope.consoleEnabled = (window.localStorage && window.localStorage.grafanaConsole === 'true');
};
$scope.toggleConsole = function() {
@@ -46,6 +53,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();
});
+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();
}
});
};
});
});
+2
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;
@@ -98,6 +99,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();
-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, _, $) {
}
};
});
});
});
+5 -4
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,6 +38,10 @@ 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();
});
@@ -300,9 +303,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) {
+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;
+127
View File
@@ -0,0 +1,127 @@
/**
* main app level module
*/
define([
'angular',
'jquery',
'lodash',
'require',
'config',
'bootstrap',
'angular-route',
'angular-strap',
'angular-dragdrop',
'extend-jquery',
'bindonce',
],
function (angular, $, _, appLevelRequire, config) {
"use strict";
var app = angular.module('grafana', []),
// we will keep a reference to each module defined before boot, so that we can
// go back and allow it to define new features later. Once we boot, this will be false
pre_boot_modules = [],
// these are the functions that we need to call to register different
// features if we define them after boot time
register_fns = {};
// This stores the grafana version number
app.constant('grafanaVersion',"@grafanaVersion@");
// Use this for cache busting partials
app.constant('cacheBust',"cache-bust="+Date.now());
/**
* Tells the application to watch the module, once bootstraping has completed
* the modules controller, service, etc. functions will be overwritten to register directly
* with this application.
* @param {[type]} module [description]
* @return {[type]} [description]
*/
app.useModule = function (module) {
if (pre_boot_modules) {
pre_boot_modules.push(module);
} else {
_.extend(module, register_fns);
}
return module;
};
app.config(function ($locationProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) {
$locationProvider.html5Mode(true);
// this is how the internet told me to dynamically add modules :/
register_fns.controller = $controllerProvider.register;
register_fns.directive = $compileProvider.directive;
register_fns.factory = $provide.factory;
register_fns.service = $provide.service;
register_fns.filter = $filterProvider.register;
});
var apps_deps = [
'ngRoute',
'$strap.directives',
'ngDragDrop',
'grafana',
'pasvaz.bindonce'
];
var module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];
_.each(module_types, function (type) {
var module_name = 'grafana.'+type;
// create the module
app.useModule(angular.module(module_name, []));
// push it into the apps dependencies
apps_deps.push(module_name);
});
var preBootRequires = [
'controllers/all',
'directives/all',
'filters/all',
'components/partials',
'routes/p_all',
];
_.each(config.plugins.dependencies, function(dep) {
preBootRequires.push('../plugins/' + dep);
});
app.boot = function() {
require(preBootRequires, function () {
// disable tool tip animation
$.fn.tooltip.defaults.animation = false;
// bootstrap the app
angular
.element(document)
.ready(function() {
angular.bootstrap(document, apps_deps)
.invoke(['$rootScope', function ($rootScope) {
_.each(pre_boot_modules, function (module) {
_.extend(module, register_fns);
});
pre_boot_modules = false;
$rootScope.requireContext = appLevelRequire;
$rootScope.require = function (deps, fn) {
var $scope = this;
$scope.requireContext(deps, function () {
var deps = _.toArray(arguments);
// Check that this is a valid scope.
if($scope.$id) {
$scope.$apply(function () {
fn.apply($scope, deps);
});
}
});
};
}]);
});
});
};
return app;
});
+1 -3
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">
+3 -17
View File
@@ -188,13 +188,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 +204,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 +243,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) {
@@ -297,10 +287,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);
};
@@ -371,7 +357,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
$scope.render();
};
$scope.init();
panelSrv.init($scope);
});
});
+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>
+22 -4
View File
@@ -172,10 +172,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 = moment(model.from.date).format('YYYY-MM-DD HH:mm:ss') + ' <br>to<br>';
model.tooltip += moment(model.to.date).format('YYYY-MM-DD HH:mm:ss');
}
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 = moment(model.from.date).format('MMM D, YYYY hh:mm:ss') + ' to ' +
moment(model.to.date).format('MMM D, YYYY hh:mm:ss');
}
}
return model;
};
var getTimeObj = function(date) {
+6 -4
View File
@@ -4,7 +4,7 @@
}
</style>
<li ng-show="fullscreen">
<li ng-show="dashboardViewState.fullscreen">
<a ng-click="exitFullscreen()">
Back to dashboard
</a>
@@ -16,15 +16,17 @@
</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 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">
<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" />
+13 -8
View File
@@ -1,4 +1,4 @@
<div ng-controller="DashCtrl" body-class>
<div ng-controller="DashCtrl" body-class ng-class="{'dashboard-fullscreen': dashboardViewState.fullscreen}">
<div class="navbar navbar-static-top">
<div class="navbar-inner">
@@ -27,7 +27,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 +98,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>
-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>
+1 -1
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">
+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',
+3
View File
@@ -12,14 +12,17 @@ function (angular) {
.when('/', {
templateUrl: '/app/partials/dashboard.html',
controller : 'DashFromDBProvider',
reloadOnSearch: false,
})
.when('/dashboard/db/:id', {
templateUrl: '/app/partials/dashboard.html',
controller : 'DashFromDBProvider',
reloadOnSearch: false,
})
.when('/dashboard/temp/:id', {
templateUrl: '/app/partials/dashboard.html',
controller : 'DashFromDBProvider',
reloadOnSearch: false,
})
.when('/login', {
templateUrl: '/app/partials/p_login.html',
+1
View File
@@ -11,5 +11,6 @@ define([
'./unsavedChangesSrv',
'./dashboard/dashboardKeyBindings',
'./dashboard/dashboardSrv',
'./dashboard/dashboardViewStateSrv',
],
function () {});
@@ -12,20 +12,6 @@ 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');
@@ -67,7 +53,7 @@ function(angular, $) {
modalData.$scope.dismiss();
}
scope.emitAppEvent('panel-fullscreen-exit');
scope.exitFullscreen();
}, { inputDisabled: true });
};
});
+85 -5
View File
@@ -10,7 +10,7 @@ function (angular, $, kbn, _) {
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 +29,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 +49,65 @@ 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.emit_refresh = function() {
$rootScope.$broadcast('refresh');
};
@@ -75,12 +136,32 @@ function (angular, $, kbn, _) {
p.updateSchema = function(old) {
var i, j, row, panel;
var isChanged = false;
var oldVersion = this.version;
this.version = 3;
if (this.version === 2) {
if (oldVersion === 3) {
return;
}
// Version 3 schema changes
// ensure panel ids
var maxId = this.getNextPanelId();
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) {
panel.id = maxId;
maxId += 1;
}
}
}
if (oldVersion === 2) {
return;
}
// Version 2 schema changes
if (old.services) {
if (old.services.filter) {
this.time = old.services.filter.time;
@@ -95,7 +176,6 @@ function (angular, $, kbn, _) {
panel = row.panels[j];
if (panel.type === 'graphite') {
panel.type = 'graph';
isChanged = true;
}
if (panel.type === 'graph') {
@@ -128,7 +208,7 @@ function (angular, $, kbn, _) {
}
}
this.version = 2;
this.version = 3;
};
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);
};
+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();
}
}
};
});
+1
View File
@@ -68,6 +68,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: [],
}
});
+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;
});
});
+65 -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] }},
@@ -54,6 +112,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);
@@ -73,10 +135,9 @@ define([
});
it('dashboard schema version should be set to latest', function() {
expect(model.version).to.be(2);
expect(model.version).to.be(3);
});
});
});
@@ -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);
});
});
});
});
+7
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,6 +56,11 @@ define([
};
}
function DashboardViewStateStub() {
this.registerPanel = function() {
};
}
function FilterSrvStub() {
this.time = { from:'now-1h', to: 'now'};
this.timeRange = function(parse) {
+14
View File
@@ -22,6 +22,20 @@ 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');
});
});
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 () {
-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');
});
});
});
});
+2 -3
View File
@@ -26,7 +26,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',
@@ -124,8 +124,7 @@ require([
'specs/filterSrv-specs',
'specs/kbn-format-specs',
'specs/dashboardSrv-specs',
'specs/influxSeries-specs',
'specs/overview-ctrl-specs',
'specs/influxSeries-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');
+1 -1
View File
@@ -2319,4 +2319,4 @@
})
}(window.jQuery);
}(window.jQuery);
-2
View File
@@ -1,2 +0,0 @@
/*! jQuery v@1.8.0 jquery.com | jquery.org/license */
(function(a,b){function G(a){var b=F[a]={};return p.each(a.split(s),function(a,c){b[c]=!0}),b}function J(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(I,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:+d+""===d?+d:H.test(d)?p.parseJSON(d):d}catch(f){}p.data(a,c,d)}else d=b}return d}function K(a){var b;for(b in a){if(b==="data"&&p.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function ba(){return!1}function bb(){return!0}function bh(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function bi(a,b){do a=a[b];while(a&&a.nodeType!==1);return a}function bj(a,b,c){b=b||0;if(p.isFunction(b))return p.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return p.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=p.grep(a,function(a){return a.nodeType===1});if(be.test(b))return p.filter(b,d,!c);b=p.filter(b,d)}return p.grep(a,function(a,d){return p.inArray(a,b)>=0===c})}function bk(a){var b=bl.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function bC(a,b){return a.getElementsByTagName(b)[0]||a.appendChild(a.ownerDocument.createElement(b))}function bD(a,b){if(b.nodeType!==1||!p.hasData(a))return;var c,d,e,f=p._data(a),g=p._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;d<e;d++)p.event.add(b,c,h[c][d])}g.data&&(g.data=p.extend({},g.data))}function bE(a,b){var c;if(b.nodeType!==1)return;b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase(),c==="object"?(b.parentNode&&(b.outerHTML=a.outerHTML),p.support.html5Clone&&a.innerHTML&&!p.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):c==="input"&&bv.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):c==="option"?b.selected=a.defaultSelected:c==="input"||c==="textarea"?b.defaultValue=a.defaultValue:c==="script"&&b.text!==a.text&&(b.text=a.text),b.removeAttribute(p.expando)}function bF(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bG(a){bv.test(a.type)&&(a.defaultChecked=a.checked)}function bX(a,b){if(b in a)return b;var c=b.charAt(0).toUpperCase()+b.slice(1),d=b,e=bV.length;while(e--){b=bV[e]+c;if(b in a)return b}return d}function bY(a,b){return a=b||a,p.css(a,"display")==="none"||!p.contains(a.ownerDocument,a)}function bZ(a,b){var c,d,e=[],f=0,g=a.length;for(;f<g;f++){c=a[f];if(!c.style)continue;e[f]=p._data(c,"olddisplay"),b?(!e[f]&&c.style.display==="none"&&(c.style.display=""),c.style.display===""&&bY(c)&&(e[f]=p._data(c,"olddisplay",cb(c.nodeName)))):(d=bH(c,"display"),!e[f]&&d!=="none"&&p._data(c,"olddisplay",d))}for(f=0;f<g;f++){c=a[f];if(!c.style)continue;if(!b||c.style.display==="none"||c.style.display==="")c.style.display=b?e[f]||"":"none"}return a}function b$(a,b,c){var d=bO.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function b_(a,b,c,d){var e=c===(d?"border":"content")?4:b==="width"?1:0,f=0;for(;e<4;e+=2)c==="margin"&&(f+=p.css(a,c+bU[e],!0)),d?(c==="content"&&(f-=parseFloat(bH(a,"padding"+bU[e]))||0),c!=="margin"&&(f-=parseFloat(bH(a,"border"+bU[e]+"Width"))||0)):(f+=parseFloat(bH(a,"padding"+bU[e]))||0,c!=="padding"&&(f+=parseFloat(bH(a,"border"+bU[e]+"Width"))||0));return f}function ca(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=!0,f=p.support.boxSizing&&p.css(a,"boxSizing")==="border-box";if(d<=0){d=bH(a,b);if(d<0||d==null)d=a.style[b];if(bP.test(d))return d;e=f&&(p.support.boxSizingReliable||d===a.style[b]),d=parseFloat(d)||0}return d+b_(a,b,c||(f?"border":"content"),e)+"px"}function cb(a){if(bR[a])return bR[a];var b=p("<"+a+">").appendTo(e.body),c=b.css("display");b.remove();if(c==="none"||c===""){bI=e.body.appendChild(bI||p.extend(e.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!bJ||!bI.createElement)bJ=(bI.contentWindow||bI.contentDocument).document,bJ.write("<!doctype html><html><body>"),bJ.close();b=bJ.body.appendChild(bJ.createElement(a)),c=bH(b,"display"),e.body.removeChild(bI)}return bR[a]=c,c}function ch(a,b,c,d){var e;if(p.isArray(b))p.each(b,function(b,e){c||cd.test(a)?d(a,e):ch(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&p.type(b)==="object")for(e in b)ch(a+"["+e+"]",b[e],c,d);else d(a,b)}function cy(a){return function(b,c){typeof b!="string"&&(c=b,b="*");var d,e,f,g=b.toLowerCase().split(s),h=0,i=g.length;if(p.isFunction(c))for(;h<i;h++)d=g[h],f=/^\+/.test(d),f&&(d=d.substr(1)||"*"),e=a[d]=a[d]||[],e[f?"unshift":"push"](c)}}function cz(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h,i=a[f],j=0,k=i?i.length:0,l=a===cu;for(;j<k&&(l||!h);j++)h=i[j](c,d,e),typeof h=="string"&&(!l||g[h]?h=b:(c.dataTypes.unshift(h),h=cz(a,c,d,e,h,g)));return(l||!h)&&!g["*"]&&(h=cz(a,c,d,e,"*",g)),h}function cA(a,c){var d,e,f=p.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((fLine truncated
+4
View File
@@ -0,0 +1,4 @@
/*! jQuery v2.1.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.1",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"Line truncated
},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?n.queue(this[0],a):void 0===b?this:this.each(function(){var c=n.queue(this,a,b);n._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&n.dequeue(this,a)})},dequeue:function(a){return this.each(function(){n.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=n.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=L.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var Q=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,R=["Top","Right","Bottom","Left"],S=function(a,b){return a=b||a,"none"===n.css(a,"display")||!n.contains(a.ownerDocument,a)},T=/^(?:checkbox|radio)$/i;!function(){var a=l.createDocumentFragment(),b=a.appendChild(l.createElement("div")),c=l.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExpLine truncated
},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)&&(a[d]=!1),a.removeAttribute(c)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),Zb={set:function(a,b,c){return b===!1?n.removeAttr(a,c):a.setAttribute(c,c),c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=$b[b]||n.find.attr;$b[b]=function(a,b,d){var e,f;return d||(f=$b[b],$b[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,$b[b]=f),e}});var _b=/^(?:input|select|textarea|button)$/i;n.fn.extend({prop:function(a,b){return J(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[n.propFix[a]||a]})}}),n.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!n.isXMLDoc(a),f&&(b=n.propFix[b]||b,e=n.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){return a.hasAttribute("tabindex")||_b.test(a.nodeName)||a.href?a.tabIndex:-1}}}}),k.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null}}),n.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){n.propFix[this.toLowerCase()]=this});var ac=/[\t\r\n\f]/g;n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h="string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=n.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0===arguments.length||"string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?n.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(n.isFunction(a)?function(c){n(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=n(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===U||"boolean"===c)&&(this.className&&L.set(this,"__className__",this.className),this.className=this.className||a===!1?"":L.get(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(ac," ").indexOf(b)>=0)return!0;return!1}});var bc=/\r/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e="":"number"==typeof e?e+="":n.isArray(e)&&(e=n.map(e,function(a){return null==a?"":a+""})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(bc,""):null==c?"":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,"value");return null!=b?b:n.trim(n.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&n.nodeName(c.parentNode,"optgroup"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=n.inArray(d.value,f)>=0)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),n.each(["radio","checkbox"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>=0:void 0}},k.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})}),n.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||Line truncated