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

Conflicts:
	src/app/components/require.config.js
This commit is contained in:
Torkel Ödegaard 2014-11-14 17:40:16 +01:00
commit e5219af481
108 changed files with 3158 additions and 6936 deletions

View File

@ -3,8 +3,36 @@
**UI Improvements*
- [Issue #770](https://github.com/grafana/grafana/issues/770). UI: Panel dropdown menu replaced with a new panel menu
**Graph**
- [Issue #877](https://github.com/grafana/grafana/issues/877). Graph: Smart auto decimal precision when using scaled unit formats
- [Issue #850](https://github.com/grafana/grafana/issues/850). Graph: Shared tooltip that shows multiple series & crosshair line, thx @toni-moreno
- [Issue #940](https://github.com/grafana/grafana/issues/940). Graph: New series style override option "Fill below to", useful to visualize max & min as a shadow for the mean
- [Issue #1030](https://github.com/grafana/grafana/issues/1030). Graph: Legend table display/look changed, now includes column headers for min/max/avg, and full width (unless on right side)
- [Issue #861](https://github.com/grafana/grafana/issues/861). Graph: Export graph time series data as csv file
**New Panels**
- [Issue #951](https://github.com/grafana/grafana/issues/951). SingleStat: New singlestat panel
**Misc**
- [Issue #938](https://github.com/grafana/grafana/issues/938). Panel: Plugin panels now reside outside of app/panels directory
- [Issue #952](https://github.com/grafana/grafana/issues/952). Help: Shortcut "?" to open help modal with list of all shortcuts
- [Issue #991](https://github.com/grafana/grafana/issues/991). ScriptedDashboard: datasource services are now available in scripted dashboards, you can query datasource for metric keys, generate dashboards, and even save them in a scripted dashboard (see scripted_gen_and_save.js for example)
- [Issue #1041](https://github.com/grafana/grafana/issues/1041). Panel: All panels can now have links to other dashboards or absolute links, these links are available in the panel menu.
**Changes**
- [Issue #1007](https://github.com/grafana/grafana/issues/1007). Graph: Series hide/show toggle changed to be default exclusive, so clicking on a series name will show only that series. (SHIFT or meta)+click will toggle hide/show.
**OpenTSDB**
- [Issue #930](https://github.com/grafana/grafana/issues/930). OpenTSDB: Adding counter max and counter reset value to open tsdb query editor, thx @rsimiciuc
- [Issue #917](https://github.com/grafana/grafana/issues/917). OpenTSDB: Templating support for OpenTSDB series name and tags, thx @mchataigner
**InfluxDB**
- [Issue #714](https://github.com/grafana/grafana/issues/714). InfluxDB: Support for sub second resolution graphs
**Fixes**
- [Issue #925](https://github.com/grafana/grafana/issues/925). Graph: bar width calculation fix for some edge cases (bars would render on top of each other)
- [Issue #505](https://github.com/grafana/grafana/issues/505). Graph: fix for second y axis tick unit labels wrapping on the next line
- [Issue #987](https://github.com/grafana/grafana/issues/987). Dashboard: Collapsed rows became invisible when hide controls was enabled
=======
# 1.8.1 (2014-09-30)

View File

@ -56,7 +56,6 @@ function (angular, $, _, appLevelRequire, config) {
register_fns.factory = $provide.factory;
register_fns.service = $provide.service;
register_fns.filter = $filterProvider.register;
});
var apps_deps = [
@ -78,6 +77,8 @@ function (angular, $, _, appLevelRequire, config) {
});
var preBootRequires = [
'services/all',
'features/all',
'controllers/all',
'directives/all',
'filters/all',

View File

@ -316,6 +316,10 @@ function($, _, moment) {
kbn.formatFuncCreator = function(factor, extArray) {
return function(size, decimals, scaledDecimals) {
if (size === null) {
return "";
}
var steps = 0;
while (Math.abs(size) >= factor) {
@ -331,6 +335,10 @@ function($, _, moment) {
};
kbn.toFixed = function(value, decimals) {
if (value === null) {
return "";
}
var factor = decimals ? Math.pow(10, decimals) : 1;
var formatted = String(Math.round(value * factor) / factor);
@ -359,6 +367,8 @@ function($, _, moment) {
kbn.valueFormats.none = kbn.toFixed;
kbn.valueFormats.ms = function(size, decimals, scaledDecimals) {
if (size === null) { return ""; }
if (Math.abs(size) < 1000) {
return kbn.toFixed(size, decimals) + " ms";
}
@ -383,6 +393,8 @@ function($, _, moment) {
};
kbn.valueFormats.s = function(size, decimals, scaledDecimals) {
if (size === null) { return ""; }
if (Math.abs(size) < 600) {
return kbn.toFixed(size, decimals) + " s";
}
@ -407,6 +419,8 @@ function($, _, moment) {
};
kbn.valueFormats['µs'] = function(size, decimals, scaledDecimals) {
if (size === null) { return ""; }
if (Math.abs(size) < 1000) {
return kbn.toFixed(size, decimals) + " µs";
}
@ -419,6 +433,8 @@ function($, _, moment) {
};
kbn.valueFormats.ns = function(size, decimals, scaledDecimals) {
if (size === null) { return ""; }
if (Math.abs(size) < 1000) {
return kbn.toFixed(size, decimals) + " ns";
}
@ -443,6 +459,17 @@ function($, _, moment) {
.replace(/ +/g,'-');
};
kbn.exportSeriesListToCsv = function(seriesList) {
var text = 'Series;Time;Value\n';
_.each(seriesList, function(series) {
_.each(series.datapoints, function(dp) {
text += series.alias + ';' + new Date(dp[1]).toISOString() + ';' + dp[0] + '\n';
});
});
var blob = new Blob([text], { type: "text/csv;charset=utf-8" });
window.saveAs(blob, 'grafana_data_export.csv');
};
kbn.stringToJsRegex = function(str) {
if (str[0] !== '/') {
return new RegExp(str);

View File

@ -0,0 +1,45 @@
define([
],
function () {
"use strict";
function PanelMeta(options) {
this.description = options.description;
this.titlePos = options.titlePos;
this.fullscreen = options.fullscreen;
this.menu = [];
this.editorTabs = [];
this.extendedMenu = [];
if (options.fullscreen) {
this.addMenuItem('view', 'icon-eye-open', 'toggleFullscreen(false)');
}
this.addMenuItem('edit', 'icon-cog', 'editPanel()');
this.addMenuItem('duplicate', 'icon-copy', 'duplicatePanel()');
this.addMenuItem('share', 'icon-share', 'sharePanel()');
this.addEditorTab('General', 'app/partials/panelgeneral.html');
if (options.metricsEditor) {
this.addEditorTab('Metrics', 'app/partials/metrics.html');
}
this.addExtendedMenuItem('Panel JSON', '', 'editPanelJson()');
}
PanelMeta.prototype.addMenuItem = function(text, icon, click) {
this.menu.push({text: text, icon: icon, click: click});
};
PanelMeta.prototype.addExtendedMenuItem = function(text, icon, click) {
this.extendedMenu.push({text: text, icon: icon, click: click});
};
PanelMeta.prototype.addEditorTab = function(title, src) {
this.editorTabs.push({title: title, src: src});
};
return PanelMeta;
});

View File

@ -30,7 +30,6 @@ require.config({
bootstrap: '../vendor/bootstrap/bootstrap',
jquery: '../vendor/jquery/jquery-2.1.1.min',
'jquery-ui': '../vendor/jquery/jquery-ui-1.10.3',
'extend-jquery': 'components/extend-jquery',
@ -42,6 +41,7 @@ require.config({
'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
'jquery.flot.time': '../vendor/jquery/jquery.flot.time',
'jquery.flot.crosshair': '../vendor/jquery/jquery.flot.crosshair',
'jquery.flot.fillbelow': '../vendor/jquery/jquery.flot.fillbelow',
modernizr: '../vendor/modernizr-2.6.1',
@ -77,7 +77,6 @@ require.config({
// simple dependency declaration
//
'jquery-ui': ['jquery'],
'jquery.flot': ['jquery'],
'jquery.flot.pie': ['jquery', 'jquery.flot'],
'jquery.flot.events': ['jquery', 'jquery.flot'],
@ -86,8 +85,9 @@ require.config({
'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
'jquery.flot.time': ['jquery', 'jquery.flot'],
'jquery.flot.crosshair':['jquery', 'jquery.flot'],
'jquery.flot.fillbelow':['jquery', 'jquery.flot'],
'angular-cookies': ['angular'],
'angular-dragdrop': ['jquery','jquery-ui','angular'],
'angular-dragdrop': ['jquery', 'angular'],
'angular-loader': ['angular'],
'angular-mocks': ['angular'],
'angular-resource': ['angular'],

View File

@ -15,12 +15,16 @@ function (_, crypto) {
var defaults = {
datasources : {},
window_title_prefix : 'Grafana - ',
panels : ['graph', 'text'],
panels : {
'graph': { path: 'panels/graph' },
'singlestat': { path: 'panels/singlestat' },
'text': { path: 'panels/text' }
},
plugins : {},
default_route : '/dashboard/file/default.json',
playlist_timespan : "1m",
unsaved_changes_warning : true,
search : { max_results: 16 },
search : { max_results: 100 },
admin : {}
};
@ -76,7 +80,7 @@ function (_, crypto) {
});
if (settings.plugins.panels) {
settings.panels = _.union(settings.panels, settings.plugins.panels);
_.extend(settings.panels, settings.plugins.panels);
}
if (!settings.plugins.dependencies) {

View File

@ -7,8 +7,12 @@ function (_, kbn) {
function TimeSeries(opts) {
this.datapoints = opts.datapoints;
this.info = opts.info;
this.label = opts.info.alias;
this.label = opts.alias;
this.id = opts.alias;
this.alias = opts.alias;
this.color = opts.color;
this.valueFormater = kbn.valueFormats.none;
this.stats = {};
}
function matchSeriesOverride(aliasOrRegex, seriesAlias) {
@ -30,13 +34,13 @@ function (_, kbn) {
this.lines = {};
this.points = {};
this.bars = {};
this.info.yaxis = 1;
this.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)) {
if (!matchSeriesOverride(override.alias, this.alias)) {
continue;
}
if (override.lines !== void 0) { this.lines.show = override.lines; }
@ -48,8 +52,10 @@ function (_, kbn) {
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.fillBelowTo !== void 0) { this.fillBelowTo = override.fillBelowTo; }
if (override.yaxis !== void 0) {
this.info.yaxis = override.yaxis;
this.yaxis = override.yaxis;
}
}
};
@ -57,12 +63,12 @@ function (_, kbn) {
TimeSeries.prototype.getFlotPairs = function (fillStyle) {
var result = [];
this.color = this.info.color;
this.yaxis = this.info.yaxis;
this.info.total = 0;
this.info.max = -212312321312;
this.info.min = 212312321312;
this.stats.total = 0;
this.stats.max = Number.MIN_VALUE;
this.stats.min = Number.MAX_VALUE;
this.stats.avg = null;
this.stats.current = null;
this.allIsNull = true;
var ignoreNulls = fillStyle === 'connected';
var nullAsZero = fillStyle === 'null as zero';
@ -81,38 +87,47 @@ function (_, kbn) {
}
if (_.isNumber(currentValue)) {
this.info.total += currentValue;
this.stats.total += currentValue;
this.allIsNull = false;
}
if (currentValue > this.info.max) {
this.info.max = currentValue;
if (currentValue > this.stats.max) {
this.stats.max = currentValue;
}
if (currentValue < this.info.min) {
this.info.min = currentValue;
if (currentValue < this.stats.min) {
this.stats.min = currentValue;
}
result.push([currentTime * 1000, currentValue]);
result.push([currentTime, currentValue]);
}
if (result.length > 2) {
this.info.timeStep = result[1][0] - result[0][0];
if (this.datapoints.length >= 2) {
this.stats.timeStep = this.datapoints[1][1] - this.datapoints[0][1];
}
if (this.stats.max === Number.MIN_VALUE) { this.stats.max = null; }
if (this.stats.min === Number.MAX_VALUE) { this.stats.min = null; }
if (result.length) {
this.info.avg = (this.info.total / result.length);
this.info.current = result[result.length-1][1];
this.stats.avg = (this.stats.total / result.length);
this.stats.current = result[result.length-1][1];
if (this.stats.current === null && result.length > 1) {
this.stats.current = result[result.length-2][1];
}
}
return result;
};
TimeSeries.prototype.updateLegendValues = function(formater, decimals, scaledDecimals) {
this.info.avg = this.info.avg != null ? formater(this.info.avg, decimals, scaledDecimals) : null;
this.info.current = this.info.current != null ? formater(this.info.current, decimals, scaledDecimals) : null;
this.info.min = this.info.min != null ? formater(this.info.min, decimals, scaledDecimals) : null;
this.info.max = this.info.max != null ? formater(this.info.max, decimals, scaledDecimals) : null;
this.info.total = this.info.total != null ? formater(this.info.total, decimals, scaledDecimals) : null;
this.valueFormater = formater;
this.decimals = decimals;
this.scaledDecimals = scaledDecimals;
};
TimeSeries.prototype.formatValue = function(value) {
return this.valueFormater(value, this.decimals, this.scaledDecimals);
};
return TimeSeries;

View File

@ -3,7 +3,6 @@ define([
'jquery',
'config',
'lodash',
'services/all',
],
function (angular, $, config, _) {
"use strict";
@ -18,11 +17,10 @@ function (angular, $, config, _) {
templateValuesSrv,
dashboardSrv,
dashboardViewStateSrv,
panelMoveSrv,
$timeout) {
$scope.editor = { index: 0 };
$scope.panelNames = config.panels;
$scope.panelNames = _.map(config.panels, function(value, key) { return key; });
var resizeEventTimeout;
this.init = function(dashboardData) {
@ -51,7 +49,6 @@ function (angular, $, config, _) {
// init services
timeSrv.init($scope.dashboard);
templateValuesSrv.init($scope.dashboard, $scope.dashboardViewState);
panelMoveSrv.init($scope.dashboard, $scope);
$scope.checkFeatureToggles();
dashboardKeybindings.shortcuts($scope);
@ -92,21 +89,12 @@ function (angular, $, config, _) {
};
};
$scope.edit_path = function(type) {
var p = $scope.panel_path(type);
if(p) {
return p+'/editor.html';
} else {
return false;
}
$scope.panelEditorPath = function(type) {
return 'app/' + config.panels[type].path + '/editor.html';
};
$scope.panel_path =function(type) {
if(type) {
return 'app/panels/'+type.replace(".","/");
} else {
return false;
}
$scope.pulldownEditorPath = function(type) {
return 'app/panels/'+type+'/editor.html';
};
$scope.showJsonEditor = function(evt, options) {
@ -120,12 +108,23 @@ function (angular, $, config, _) {
$scope.submenuEnabled = $scope.dashboard.templating.enable || $scope.dashboard.annotations.enable;
};
$scope.setEditorTabs = function(panelMeta) {
$scope.editorTabs = ['General','Panel'];
if(!_.isUndefined(panelMeta.editorTabs)) {
$scope.editorTabs = _.union($scope.editorTabs,_.pluck(panelMeta.editorTabs,'title'));
$scope.onDrop = function(panelId, row, dropTarget) {
var info = $scope.dashboard.getPanelInfoById(panelId);
if (dropTarget) {
var dropInfo = $scope.dashboard.getPanelInfoById(dropTarget.id);
dropInfo.row.panels[dropInfo.index] = info.panel;
info.row.panels[info.index] = dropTarget;
var dragSpan = info.panel.span;
info.panel.span = dropTarget.span;
dropTarget.span = dragSpan;
}
return $scope.editorTabs;
else {
info.row.panels.splice(info.index, 1);
info.panel.span = 12 - $scope.dashboard.rowSpan(row);
row.panels.push(info.panel);
}
$rootScope.$broadcast('render');
};
});

View File

@ -93,12 +93,18 @@ function (angular, _, moment, config, store) {
};
$scope.deleteDashboard = function(evt, options) {
if (!confirm('Do you want to delete dashboard ' + options.title + ' ?')) {
return;
}
if (!$scope.isAdmin()) { return false; }
$scope.appEvent('confirm-modal', {
title: 'Delete dashboard',
text: 'Do you want to delete dashboard ' + options.title + '?',
onConfirm: function() {
$scope.deleteDashboardConfirmed(options);
}
});
};
$scope.deleteDashboardConfirmed = function(options) {
var id = options.id;
$scope.db.deleteDashboard(id).then(function(id) {
$scope.appEvent('alert-success', ['Dashboard Deleted', id + ' has been deleted']);

View File

@ -201,7 +201,7 @@ function (angular, _, config, gfunc, Parser) {
$scope.targetTextChanged = function() {
parseTarget();
$scope.$parent.get_data();
$scope.get_data();
};
$scope.targetChanged = function() {
@ -275,6 +275,10 @@ function (angular, _, config, gfunc, Parser) {
}
};
$scope.moveMetricQuery = function(fromIndex, toIndex) {
_.move($scope.panel.targets, fromIndex, toIndex);
};
$scope.duplicate = function() {
var clone = angular.copy($scope.target);
$scope.panel.targets.push(clone);

View File

@ -1,7 +1,8 @@
define([
'angular'
'angular',
'lodash'
],
function (angular) {
function (angular, _) {
'use strict';
var module = angular.module('grafana.controllers');
@ -83,10 +84,11 @@ function (angular) {
};
$scope.listSeries = function(query, callback) {
if (!seriesList || query === '') {
if (query !== '') {
seriesList = [];
$scope.datasource.listSeries().then(function(series) {
$scope.datasource.listSeries(query).then(function(series) {
seriesList = series;
console.log(series);
callback(seriesList);
});
}
@ -95,6 +97,10 @@ function (angular) {
}
};
$scope.moveMetricQuery = function(fromIndex, toIndex) {
_.move($scope.panel.targets, fromIndex, toIndex);
};
$scope.duplicate = function() {
var clone = angular.copy($scope.target);
$scope.panel.targets.push(clone);

View File

@ -47,9 +47,13 @@ function (angular, app, _) {
};
$scope.delete_row = function() {
if (confirm("Are you sure you want to delete this row?")) {
$scope.dashboard.rows = _.without($scope.dashboard.rows, $scope.row);
}
$scope.appEvent('confirm-modal', {
title: 'Delete row',
text: 'Are you sure you want to delete this row?',
onConfirm: function() {
$scope.dashboard.rows = _.without($scope.dashboard.rows, $scope.row);
}
});
};
$scope.move_row = function(direction) {
@ -76,9 +80,13 @@ function (angular, app, _) {
};
$scope.remove_panel_from_row = function(row, panel) {
if (confirm('Are you sure you want to remove this ' + panel.type + ' panel?')) {
row.panels = _.without(row.panels,panel);
}
$scope.appEvent('confirm-modal', {
title: 'Remove panel',
text: 'Are you sure you want to remove this panel?',
onConfirm: function() {
row.panels = _.without(row.panels, panel);
}
});
};
$scope.replacePanel = function(newPanel, oldPanel) {
@ -94,15 +102,12 @@ function (angular, app, _) {
});
};
$scope.duplicatePanel = function(panel, row) {
$scope.dashboard.duplicatePanel(panel, row || $scope.row);
};
$scope.reset_panel = function(type) {
var defaultSpan = 12;
var _as = 12 - $scope.dashboard.rowSpan($scope.row);
$scope.panel = {
title: 'no title [click here]',
error : false,
span : _as < defaultSpan && _as > 0 ? _as : defaultSpan,
editable: true,
@ -144,13 +149,18 @@ function (angular, app, _) {
module.directive('panelDropZone', function() {
return function(scope, element) {
scope.$watch('dashboard.$$panelDragging', function(newVal) {
if (newVal && scope.dashboard.rowSpan(scope.row) < 10) {
scope.$on("ANGULAR_DRAG_START", function() {
var dropZoneSpan = 12 - scope.dashboard.rowSpan(scope.row);
if (dropZoneSpan > 0) {
element.find('.panel-container').css('height', scope.row.height);
element[0].style.width = ((dropZoneSpan / 1.2) * 10) + '%';
element.show();
}
else {
element.hide();
}
});
scope.$on("ANGULAR_DRAG_END", function() {
element.hide();
});
};
});

View File

@ -27,19 +27,12 @@ function (angular, _) {
}
var panelId = $scope.panel.id;
var range = timeSrv.timeRange(false);
var params = angular.copy($location.search());
if (_.isString(range.to) && range.to.indexOf('now')) {
range = timeSrv.timeRange();
}
var range = timeSrv.timeRangeForUrl();
params.from = range.from;
params.to = range.to;
if (_.isDate(params.from)) { params.from = params.from.getTime(); }
if (_.isDate(params.to)) { params.to = params.to.getTime(); }
if ($scope.includeTemplateVars) {
_.each(templateSrv.variables, function(variable) {
params['var-' + variable.name] = variable.current.text;
@ -66,11 +59,13 @@ function (angular, _) {
var paramsArray = [];
_.each(params, function(value, key) {
var str = key;
if (value !== true) {
str += '=' + encodeURIComponent(value);
if (value === null) { return; }
if (value === true) {
paramsArray.push(key);
} else {
key += '=' + encodeURIComponent(value);
paramsArray.push(key);
}
paramsArray.push(str);
});
$scope.shareUrl = baseUrl + "?" + paramsArray.join('&') ;

View File

@ -101,7 +101,6 @@
"legend_counts": true,
"timezone": "browser",
"percentage": false,
"zerofill": true,
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {

View File

@ -68,6 +68,17 @@ for (var i = 0; i < rows; i++) {
'target': "randomWalk('random walk2')"
}
],
seriesOverrides: [
{
alias: '/random/',
yaxis: 2,
fill: 0,
linewidth: 5
}
],
tooltip: {
shared: true
}
}
]
});

View File

@ -0,0 +1,95 @@
/* global _ */
/*
* Complex scripted dashboard
* This script generates a dashboard object that Grafana can load. It also takes a number of user
* supplied URL parameters (int ARGS variable)
*
* Return a dashboard object, or a function
*
* For async scripts, return a function, this function must take a single callback function as argument,
* call this callback function with the dashboard object (look at scripted_async.js for an example)
*/
'use strict';
// accessable variables in this scope
var window, document, ARGS, $, jQuery, moment, kbn, services, _;
// default datasource
var datasource = services.datasourceSrv.default;
// get datasource used for saving dashboards
var dashboardDB = services.datasourceSrv.getGrafanaDB();
var targets = [];
function getTargets(path) {
return datasource.metricFindQuery(path + '.*').then(function(result) {
if (!result) {
return null;
}
if (targets.length === 10) {
return null;
}
var promises = _.map(result, function(metric) {
if (metric.expandable) {
return getTargets(path + "." + metric.text);
}
else {
targets.push(path + '.' + metric.text);
}
return null;
});
return services.$q.all(promises);
});
}
function createDashboard(target, index) {
// Intialize a skeleton with nothing but a rows array and service object
var dashboard = { rows : [] };
dashboard.title = 'Scripted dash ' + index;
dashboard.time = {
from: "now-6h",
to: "now"
};
dashboard.rows.push({
title: 'Chart',
height: '300px',
panels: [
{
title: 'Events',
type: 'graph',
span: 12,
targets: [ {target: target} ]
}
]
});
return dashboard;
}
function saveDashboard(dashboard) {
var model = services.dashboardSrv.create(dashboard);
dashboardDB.saveDashboard(model);
}
return function(callback) {
getTargets('apps').then(function() {
console.log('targets: ', targets);
_.each(targets, function(target, index) {
var dashboard = createDashboard(target, index);
saveDashboard(dashboard);
if (index === targets.length - 1) {
callback(dashboard);
}
});
});
};

View File

@ -65,7 +65,6 @@
"avg": false
},
"percentage": false,
"zerofill": true,
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {

View File

@ -68,13 +68,12 @@ function (angular, app, _, $, gfunc) {
});
$input.blur(function() {
$input.hide();
$input.val('');
$button.show();
$button.focus();
// clicking the function dropdown menu wont
// work if you remove class at once
setTimeout(function() {
$input.val('');
$input.hide();
$button.show();
elem.removeClass('open');
}, 200);
});

View File

@ -10,7 +10,6 @@ define([
'./confirmClick',
'./configModal',
'./spectrumPicker',
'./grafanaGraph',
'./bootstrap-tagsinput',
'./bodyClass',
'./addGraphiteFunc',
@ -18,5 +17,6 @@ define([
'./templateParamSelector',
'./graphiteSegment',
'./grafanaVersionCheck',
'./dropdown.typeahead',
'./influxdbFuncEditor'
], function () {});

View File

@ -0,0 +1,105 @@
define([
'angular',
'app',
'lodash',
'jquery',
],
function (angular, app, _, $) {
'use strict';
angular
.module('grafana.directives')
.directive('dropdownTypeahead', function($compile) {
var inputTemplate = '<input type="text"'+
' class="grafana-target-segment-input input-medium grafana-target-segment-input"' +
' spellcheck="false" style="display:none"></input>';
var buttonTemplate = '<a class="grafana-target-segment grafana-target-function dropdown-toggle"' +
' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
' data-placement="top"><i class="icon-plus"></i></a>';
return {
scope: {
"menuItems": "=dropdownTypeahead",
"dropdownTypeaheadOnSelect": "&dropdownTypeaheadOnSelect"
},
link: function($scope, elem) {
var $input = $(inputTemplate);
var $button = $(buttonTemplate);
$input.appendTo(elem);
$button.appendTo(elem);
var typeaheadValues = _.reduce($scope.menuItems, function(memo, value) {
_.each(value.submenu, function(item) {
memo.push(value.text + ' ' + item.text);
});
return memo;
}, []);
$scope.menuItemSelected = function(optionIndex, valueIndex) {
var option = $scope.menuItems[optionIndex];
var result = {
$item: option.submenu[valueIndex],
$optionIndex: optionIndex,
$valueIndex: valueIndex
};
$scope.dropdownTypeaheadOnSelect(result);
};
$input.attr('data-provide', 'typeahead');
$input.typeahead({
source: typeaheadValues,
minLength: 1,
items: 10,
updater: function (value) {
var result = {};
_.each($scope.menuItems, function(menuItem, optionIndex) {
_.each(menuItem.submenu, function(submenuItem, valueIndex) {
if (value === (menuItem.text + ' ' + submenuItem.text)) {
result.$item = submenuItem;
result.$optionIndex = optionIndex;
result.$valueIndex = valueIndex;
}
});
});
if (result.$item) {
$scope.$apply(function() {
$scope.dropdownTypeaheadOnSelect(result);
});
}
$input.trigger('blur');
return '';
}
});
$button.click(function() {
$button.hide();
$input.show();
$input.focus();
});
$input.keyup(function() {
elem.toggleClass('open', $input.val() === '');
});
$input.blur(function() {
$input.hide();
$input.val('');
$button.show();
$button.focus();
// clicking the function dropdown menu wont
// work if you remove class at once
setTimeout(function() {
elem.removeClass('open');
}, 200);
});
$compile(elem.contents())($scope);
}
};
});
});

View File

@ -1,132 +0,0 @@
define([
'jquery',
'kbn',
],
function ($, kbn) {
'use strict';
function registerTooltipFeatures(elem, dashboard, scope) {
var $tooltip = $('<div id="tooltip">');
elem.mouseleave(function () {
if (scope.panel.tooltip.shared || dashboard.sharedCrosshair) {
var plot = elem.data().plot;
if (plot) {
$tooltip.detach();
plot.unhighlight();
scope.appEvent('clearCrosshair');
}
}
});
function findHoverIndex(posX, series) {
for (var j = 0; j < series.data.length; j++) {
if (series.data[j][0] > posX) {
return Math.max(j - 1, 0);
}
}
return j - 1;
}
function showTooltip(title, innerHtml, pos) {
var body = '<div class="graph-tooltip small"><div class="graph-tooltip-time">'+ title + '</div> ' ;
body += innerHtml + '</div>';
$tooltip.html(body).place_tt(pos.pageX + 20, pos.pageY);
}
elem.bind("plothover", function (event, pos, item) {
var plot = elem.data().plot;
var data = plot.getData();
var group, value, timestamp, seriesInfo, format, i, series, hoverIndex, seriesHtml;
if(dashboard.sharedCrosshair){
scope.appEvent('setCrosshair', { pos: pos, scope: scope });
}
if (scope.panel.tooltip.shared) {
plot.unhighlight();
//check if all series has same length if so, only one x index will
//be checked and only for exact timestamp values
var pointCount = data[0].data.length;
for (i = 1; i < data.length; i++) {
if (data[i].data.length !== pointCount) {
showTooltip('Shared tooltip error', '<ul>' +
'<li>Series point counts are not the same</li>' +
'<li>Set null point mode to null or null as zero</li>' +
'<li>For influxdb users set fill(0) in your query</li></ul>', pos);
return;
}
}
seriesHtml = '';
series = data[0];
hoverIndex = findHoverIndex(pos.x, series);
//now we know the current X (j) position for X and Y values
timestamp = dashboard.formatDate(series.data[hoverIndex][0]);
var last_value = 0; //needed for stacked values
for (i = data.length-1; i >= 0; --i) {
//stacked values should be added in reverse order
series = data[i];
seriesInfo = series.info;
format = scope.panel.y_formats[seriesInfo.yaxis - 1];
if (scope.panel.stack) {
if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
value = series.data[hoverIndex][1];
} else {
last_value += series.data[hoverIndex][1];
value = last_value;
}
} else {
value = series.data[hoverIndex][1];
}
value = kbn.valueFormats[format](value, series.yaxis.tickDecimals);
if (seriesInfo.alias) {
group = '<i class="icon-minus" style="color:' + series.color +';"></i> ' + seriesInfo.alias;
} else {
group = kbn.query_color_dot(series.color, 15) + ' ';
}
//pre-pending new values
seriesHtml = group + ': <span class="graph-tooltip-value">' + value + '</span><br>' + seriesHtml;
plot.highlight(i, hoverIndex);
}
showTooltip(timestamp, seriesHtml, pos);
return;
}
if (item) {
seriesInfo = item.series.info;
format = scope.panel.y_formats[seriesInfo.yaxis - 1];
group = '<i class="icon-minus" style="color:' + item.series.color +';"></i> ' + seriesInfo.alias;
if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
value = item.datapoint[1] - item.datapoint[2];
}
else {
value = item.datapoint[1];
}
value = kbn.valueFormats[format](value, item.series.yaxis.tickDecimals);
timestamp = dashboard.formatDate(item.datapoint[0]);
group += ': <span class="graph-tooltip-value">' + value + '</span>';
showTooltip(timestamp, group, pos);
} else {
$tooltip.detach();
}
});
}
return {
register: registerTooltipFeatures
};
});

View File

@ -1,9 +1,10 @@
define([
'angular',
'jquery',
'config',
'./panelMenu',
],
function (angular, $) {
function (angular, $, config) {
'use strict';
angular
@ -26,7 +27,7 @@ function (angular, $) {
'<i class="icon-spinner icon-spin icon-large"></i>' +
'</span>' +
'<div class="panel-title-container" panel-menu></div>' +
'<div class="panel-title-container drag-handle" panel-menu></div>' +
'</div>'+
'</div>';
@ -68,10 +69,12 @@ function (angular, $) {
elem.addClass('ng-cloak');
var panelPath = config.panels[panelType].path;
$scope.require([
'jquery',
'text!panels/'+panelType+'/module.html',
'panels/' + panelType + "/module",
'text!'+panelPath+'/module.html',
panelPath + "/module",
], function ($, moduleTemplate) {
var $module = $(moduleTemplate);
$module.prepend(panelHeader);

View File

@ -8,16 +8,12 @@ function (angular, $, _) {
angular
.module('grafana.directives')
.directive('panelMenu', function($compile) {
var linkTemplate = '<a class="panel-title">{{panel.title | interpolateTemplateVars}}</a>';
var moveAttributes = ' data-drag=true data-jqyoui-options="kbnJqUiDraggableOptions"'+
' jqyoui-draggable="{'+
'animate:false,'+
'mutate:false,'+
'index:{{$index}},'+
'onStart:\'panelMoveStart\','+
'onStop:\'panelMoveStop\''+
'}" ng-model="panel" ';
.directive('panelMenu', function($compile, linkSrv) {
var linkTemplate =
'<span class="panel-title drag-handle pointer">' +
'<span class="panel-title-text drag-handle">{{panel.title | interpolateTemplateVars}}</span>' +
'<span class="panel-links-icon"></span>' +
'</span>';
function createMenuTemplate($scope) {
var template = '<div class="panel-menu small">';
@ -26,11 +22,11 @@ function (angular, $, _) {
template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(-1)"><i class="icon-minus"></i></a>';
template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(1)"><i class="icon-plus"></i></a>';
template += '<a class="panel-menu-icon pull-right" ng-click="remove_panel_from_row(row, panel)"><i class="icon-remove"></i></a>';
template += '<a class="panel-menu-icon pull-right" ' + moveAttributes + '><i class="icon-move"></i></a>';
template += '<div class="clearfix"></div>';
template += '</div>';
template += '<div class="panel-menu-row">';
template += '<a class="panel-menu-link" gf-dropdown="extendedMenu"><i class="icon-th-list"></i></a>';
_.each($scope.panelMeta.menu, function(item) {
template += '<a class="panel-menu-link" ';
@ -46,18 +42,36 @@ function (angular, $, _) {
return template;
}
function getExtendedMenu($scope) {
var menu = angular.copy($scope.panelMeta.extendedMenu);
if ($scope.panel.links) {
_.each($scope.panel.links, function(link) {
var info = linkSrv.getPanelLinkAnchorInfo(link);
menu.push({text: info.title, href: info.href, target: info.target });
});
}
return menu;
}
return {
restrict: 'A',
link: function($scope, elem) {
var $link = $(linkTemplate);
var $panelContainer = elem.parents(".panel-container");
var menuWidth = $scope.panelMeta.menu.length === 5 ? 246 : 201;
var menuWidth = $scope.panelMeta.menu.length === 4 ? 236 : 191;
var menuScope = null;
var timeout = null;
var $menu = null;
elem.append($link);
$scope.$watchCollection('panel.links', function(newValue) {
var showIcon = (newValue ? newValue.length > 0 : false) && $scope.panel.title !== '';
$link.toggleClass('has-panel-links', showIcon);
});
function dismiss(time) {
clearTimeout(timeout);
timeout = null;
@ -109,6 +123,7 @@ function (angular, $, _) {
});
menuScope = $scope.$new();
menuScope.extendedMenu = getExtendedMenu($scope);
$('.panel-menu').remove();
elem.append($menu);
@ -122,6 +137,11 @@ function (angular, $, _) {
dismiss(2500);
};
if ($scope.panelMeta.titlePos && $scope.panel.title) {
elem.css('text-align', 'left');
$link.css('padding-left', '10px');
}
elem.click(showMenu);
$compile(elem.contents())($scope);
}

View File

@ -32,7 +32,11 @@ function (angular) {
};
input.spectrum(options);
scope.$on('$destroy', function() {
input.spectrum('destroy');
});
}
};
});
});
});

3
src/app/features/all.js Normal file
View File

@ -0,0 +1,3 @@
define([
'./panellinkeditor/module',
], function () {});

View File

@ -0,0 +1,39 @@
define([
'angular',
'kbn',
],
function (angular, kbn) {
'use strict';
angular
.module('grafana.services')
.service('linkSrv', function(templateSrv, timeSrv) {
this.getPanelLinkAnchorInfo = function(link) {
var info = {};
if (link.type === 'absolute') {
info.target = '_blank';
info.href = templateSrv.replace(link.url || '');
info.title = templateSrv.replace(link.title || '');
info.href += '?';
}
else {
info.title = templateSrv.replace(link.title || '');
var slug = kbn.slugifyForUrl(link.dashboard || '');
info.href = '#dashboard/db/' + slug + '?';
}
var range = timeSrv.timeRangeForUrl();
info.href += 'from=' + range.from;
info.href += '&to=' + range.to;
if (link.params) {
info.href += "&" + link.params;
}
return info;
};
});
});

View File

@ -0,0 +1,49 @@
<div class="editor-row">
<div class="section">
<h5>Drilldown / detail link<tip>These links appear in the dropdown menu in the panel menu</tip></h5>
<div class="grafana-target" ng-repeat="link in panel.links"j>
<div class="grafana-target-inner">
<ul class="grafana-segment-list">
<li class="grafana-target-segment">
<i class="icon-remove pointer" ng-click="deleteLink(link)"></i>
</li>
<li class="grafana-target-segment">title</li>
<li>
<input type="text" ng-model="link.title" class="input-medium grafana-target-segment-input">
</li>
<li class="grafana-target-segment">type</li>
<li>
<select class="input-medium grafana-target-segment-input" style="width: 101px;" ng-model="link.type" ng-options="f for f in ['dashboard','absolute']"></select>
</li>
<li class="grafana-target-segment" ng-show="link.type === 'dashboard'">dashboard</li>
<li ng-show="link.type === 'dashboard'">
<input type="text"
ng-model="link.dashboard"
bs-typeahead="searchDashboards"
class="input-large grafana-target-segment-input">
</li>
<li class="grafana-target-segment" ng-show="link.type === 'absolute'">url</li>
<li ng-show="link.type === 'absolute'">
<input type="text" ng-model="link.url" class="input-large grafana-target-segment-input">
</li>
<li class="grafana-target-segment">params</li>
<li>
<input type="text" ng-model="link.params" class="input-medium grafana-target-segment-input">
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
</div>
<div class="editor-row">
<br>
<button class="btn btn-success" ng-click="addLink()">Add link</button>
</div>

View File

@ -0,0 +1,51 @@
define([
'angular',
'lodash',
'./linkSrv',
],
function (angular, _) {
'use strict';
angular
.module('grafana.directives')
.directive('panelLinkEditor', function() {
return {
scope: {
panel: "="
},
restrict: 'E',
controller: 'PanelLinkEditorCtrl',
templateUrl: 'app/features/panellinkeditor/module.html',
link: function() {
}
};
}).controller('PanelLinkEditorCtrl', function($scope, datasourceSrv) {
$scope.panel.links = $scope.panel.links || [];
$scope.addLink = function() {
$scope.panel.links.push({
type: 'dashboard',
name: 'Drilldown dashboard'
});
};
$scope.searchDashboards = function(query, callback) {
var ds = datasourceSrv.getGrafanaDB();
if (ds === null) { return; }
ds.searchDashboards(query).then(function(result) {
var dashboards = _.map(result.dashboards, function(dash) {
return dash.title;
});
callback(dashboards);
});
};
$scope.deleteLink = function(link) {
$scope.panel.links = _.without($scope.panel.links, link);
};
});
});

View File

@ -56,9 +56,13 @@ define(['angular', 'jquery', 'lodash', 'moment'], function (angular, $, _, momen
});
module.filter('interpolateTemplateVars', function(templateSrv) {
return function(text) {
function interpolateTemplateVars(text) {
return templateSrv.replaceWithText(text);
};
}
interpolateTemplateVars.$stateful = true;
return interpolateTemplateVars;
});
});

View File

@ -40,11 +40,11 @@
<div class="editor-row">
<div class="section">
<h5>Legend styles</h5>
<editor-opt-bool text="Show legend" model="panel.legend.show" change="render()"></editor-opt-bool>
<editor-opt-bool text="Include values" model="panel.legend.values" change="render()"></editor-opt-bool>
<editor-opt-bool text="Align as table" model="panel.legend.alignAsTable" change="render()"></editor-opt-bool>
<editor-opt-bool text="Show" model="panel.legend.show" change="get_data();"></editor-opt-bool>
<editor-opt-bool text="Values" model="panel.legend.values" change="render()"></editor-opt-bool>
<editor-opt-bool text="Table" model="panel.legend.alignAsTable" change="render()"></editor-opt-bool>
<editor-opt-bool text="Right side" model="panel.legend.rightSide" change="render()"></editor-opt-bool>
</div>
</div>
<div class="section" ng-if="panel.legend.values">
<h5>Legend values</h5>

View File

@ -4,9 +4,17 @@ define([
'kbn',
'moment',
'lodash',
'./grafanaGraph.tooltip'
'./graph.tooltip',
'jquery.flot',
'jquery.flot.events',
'jquery.flot.selection',
'jquery.flot.time',
'jquery.flot.stack',
'jquery.flot.stackpercent',
'jquery.flot.fillbelow',
'jquery.flot.crosshair'
],
function (angular, $, kbn, moment, _, graphTooltip) {
function (angular, $, kbn, moment, _, GraphTooltip) {
'use strict';
var module = angular.module('grafana.directives');
@ -46,10 +54,6 @@ function (angular, $, kbn, moment, _, graphTooltip) {
scope.get_data();
});
scope.$on('toggleLegend', function() {
render_panel();
});
// Receive render events
scope.$on('render',function(event, renderData) {
data = renderData || data;
@ -68,7 +72,7 @@ function (angular, $, kbn, moment, _, graphTooltip) {
height = parseInt(height.replace('px', ''), 10);
}
height = height - 32; // subtract panel title bar
height -= scope.panel.title ? 24 : 9; // subtract panel title bar
if (scope.panel.legend.show && !scope.panel.legend.rightSide) {
height = height - 21; // subtract one line legend
@ -110,9 +114,9 @@ function (angular, $, kbn, moment, _, graphTooltip) {
var series = data[i];
var axis = yaxis[series.yaxis - 1];
var formater = kbn.valueFormats[scope.panel.y_formats[series.yaxis - 1]];
series.updateLegendValues(formater, axis.tickDecimals, axis.scaledDecimals);
series.updateLegendValues(formater, axis.tickDecimals, axis.scaledDecimals + 2);
if(!scope.$$phase) { scope.$digest(); }
}
}
// Function for rendering panel
@ -177,15 +181,16 @@ function (angular, $, kbn, moment, _, graphTooltip) {
var series = data[i];
series.applySeriesOverrides(panel.seriesOverrides);
series.data = series.getFlotPairs(panel.nullPointMode, panel.y_formats);
// if hidden remove points and disable stack
if (scope.hiddenSeries[series.info.alias]) {
if (scope.hiddenSeries[series.alias]) {
series.data = [];
series.stack = false;
}
}
if (data.length && data[0].info.timeStep) {
options.series.bars.barWidth = data[0].info.timeStep / 1.5;
if (data.length && data[0].stats.timeStep) {
options.series.bars.barWidth = data[0].stats.timeStep / 1.5;
}
addTimeAxis(options);
@ -206,6 +211,8 @@ function (angular, $, kbn, moment, _, graphTooltip) {
}
if (shouldDelayDraw(panel)) {
// temp fix for legends on the side, need to render twice to get dimensions right
callPlot();
setTimeout(callPlot, 50);
legendSideLastValue = panel.legend.rightSide;
}
@ -416,7 +423,9 @@ function (angular, $, kbn, moment, _, graphTooltip) {
elem.html('<img src="' + url + '"></img>');
}
graphTooltip.register(elem, dashboard, scope, $rootScope);
new GraphTooltip(elem, dashboard, scope, function() {
return data;
});
elem.bind("plotselected", function (event, ranges) {
scope.$apply(function() {

View File

@ -0,0 +1,182 @@
define([
'jquery',
],
function ($) {
'use strict';
function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
var self = this;
var $tooltip = $('<div id="tooltip">');
this.findHoverIndexFromDataPoints = function(posX, series,last) {
var ps = series.datapoints.pointsize;
var initial = last*ps;
var len = series.datapoints.points.length;
for (var j = initial; j < len; j += ps) {
if (series.datapoints.points[j] > posX) {
return Math.max(j - ps, 0)/ps;
}
}
return j/ps - 1;
};
this.findHoverIndexFromData = function(posX, series) {
var len = series.data.length;
for (var j = 0; j < len; j++) {
if (series.data[j][0] > posX) {
return Math.max(j - 1, 0);
}
}
return j - 1;
};
this.showTooltip = function(title, innerHtml, pos) {
var body = '<div class="graph-tooltip small"><div class="graph-tooltip-time">'+ title + '</div> ' ;
body += innerHtml + '</div>';
$tooltip.html(body).place_tt(pos.pageX + 20, pos.pageY);
};
this.getMultiSeriesPlotHoverInfo = function(seriesList, pos) {
var value, i, series, hoverIndex;
var results = [];
var pointCount = seriesList[0].data.length;
for (i = 1; i < seriesList.length; i++) {
if (seriesList[i].data.length !== pointCount) {
results.pointCountMismatch = true;
return results;
}
}
series = seriesList[0];
hoverIndex = this.findHoverIndexFromData(pos.x, series);
var lasthoverIndex = 0;
if(!scope.panel.steppedLine) {
lasthoverIndex = hoverIndex;
}
//now we know the current X (j) position for X and Y values
results.time = series.data[hoverIndex][0];
var last_value = 0; //needed for stacked values
for (i = 0; i < seriesList.length; i++) {
series = seriesList[i];
if (scope.panel.stack) {
if (scope.panel.tooltip.value_type === 'individual') {
value = series.data[hoverIndex][1];
} else {
last_value += series.data[hoverIndex][1];
value = last_value;
}
} else {
value = series.data[hoverIndex][1];
}
// Highlighting multiple Points depending on the plot type
if (scope.panel.steppedLine || (scope.panel.stack && scope.panel.nullPointMode == "null")) {
// stacked and steppedLine plots can have series with different length.
// Stacked series can increase its length on each new stacked serie if null points found,
// to speed the index search we begin always on the las found hoverIndex.
var newhoverIndex = this.findHoverIndexFromDataPoints(pos.x, series,lasthoverIndex);
// update lasthoverIndex depends also on the plot type.
if(!scope.panel.steppedLine) {
// on stacked graphs new will be always greater than last
lasthoverIndex = newhoverIndex;
} else {
// if steppeLine, not always series increases its length, so we should begin
// to search correct index from the original hoverIndex on each serie.
lasthoverIndex = hoverIndex;
}
results.push({ value: value, hoverIndex: newhoverIndex });
} else {
results.push({ value: value, hoverIndex: hoverIndex });
}
}
return results;
};
elem.mouseleave(function () {
if (scope.panel.tooltip.shared || dashboard.sharedCrosshair) {
var plot = elem.data().plot;
if (plot) {
$tooltip.detach();
plot.unhighlight();
scope.appEvent('clearCrosshair');
}
}
});
elem.bind("plothover", function (event, pos, item) {
var plot = elem.data().plot;
var plotData = plot.getData();
var seriesList = getSeriesFn();
var group, value, timestamp, hoverInfo, i, series, seriesHtml;
if(dashboard.sharedCrosshair){
scope.appEvent('setCrosshair', { pos: pos, scope: scope });
}
if (seriesList.length === 0) {
return;
}
if (scope.panel.tooltip.shared) {
plot.unhighlight();
var seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);
if (seriesHoverInfo.pointCountMismatch) {
self.showTooltip('Shared tooltip error', '<ul>' +
'<li>Series point counts are not the same</li>' +
'<li>Set null point mode to null or null as zero</li>' +
'<li>For influxdb users set fill(0) in your query</li></ul>', pos);
return;
}
seriesHtml = '';
timestamp = dashboard.formatDate(seriesHoverInfo.time);
for (i = 0; i < seriesHoverInfo.length; i++) {
series = seriesList[i];
hoverInfo = seriesHoverInfo[i];
value = series.formatValue(hoverInfo.value);
seriesHtml += '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
seriesHtml += '<i class="icon-minus" style="color:' + series.color +';"></i> ' + series.label + ':</div>';
seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
plot.highlight(i, hoverInfo.hoverIndex);
}
self.showTooltip(timestamp, seriesHtml, pos);
}
// single series tooltip
else if (item) {
series = seriesList[item.seriesIndex];
group = '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
group += '<i class="icon-minus" style="color:' + item.series.color +';"></i> ' + series.label + ':</div>';
if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
value = item.datapoint[1] - item.datapoint[2];
}
else {
value = item.datapoint[1];
}
value = series.formatValue(value);
timestamp = dashboard.formatDate(item.datapoint[0]);
group += '<div class="graph-tooltip-value">' + value + '</div>';
self.showTooltip(timestamp, group, pos);
}
// no hit
else {
$tooltip.detach();
}
});
}
return GraphTooltip;
});

View File

@ -1,58 +0,0 @@
<section class="graph-legend" ng-class="{'graph-legend-table': panel.legend.alignAsTable}">
<div class="graph-legend-series"
ng-repeat='series in legend'
ng-class="{'pull-right': series.yaxis === 2, 'graph-legend-series-hidden': hiddenSeries[series.alias]}"
>
<div class="graph-legend-icon">
<i class='icon-minus pointer' ng-style="{color: series.color}" bs-popover="'colorPopup.html'" data-placement="bottom">
</i>
</div>
<div class="graph-legend-alias small">
<a ng-click="toggleSeries(series, $event)" data-unique="1" data-placement="{{series.yaxis === 2 ? 'bottomRight' : 'bottomLeft'}}">
{{series.alias}}
</a>
</div>
<div class="graph-legend-value current small" ng-show="panel.legend.values && panel.legend.current" ng-bind="series.current">
</div>
<div class="graph-legend-value min small" ng-show="panel.legend.values && panel.legend.min" ng-bind="series.min">
</div>
<div class="graph-legend-value max small" ng-show="panel.legend.values && panel.legend.max" ng-bind="series.max">
</div>
<div class="graph-legend-value total small" ng-show="panel.legend.values && panel.legend.total" ng-bind="series.total">
</div>
<div class="graph-legend-value avg small" ng-show="panel.legend.values && panel.legend.avg" ng-bind="series.avg">
</div>
</div>
</section>
<script type="text/ng-template" id="colorPopup.html">
<div class="graph-legend-popover">
<a class="close" ng-click="dismiss();" href="">×</a>
<div class="editor-row small" style="padding-bottom: 0;">
<label>Axis:</label>
<button ng-click="toggleYAxis(series);dismiss();"
class="btn btn-mini"
ng-class="{'btn-success': series.yaxis === 1 }">
Left
</button>
<button ng-click="toggleYAxis(series);dismiss();"
class="btn btn-mini"
ng-class="{'btn-success': series.yaxis === 2 }">
Right
</button>
</div>
<div class="editor-row">
<i ng-repeat="color in colors"
class="pointer"
ng-class="{'icon-circle-blank': color === series.color,'icon-circle': color !== series.color}"
ng-style="{color:color}"
ng-click="changeSeriesColor(series, color);dismiss();">
</i>
</div>
</div>
</script>

View File

@ -0,0 +1,162 @@
define([
'angular',
'app',
'lodash',
'kbn',
'jquery',
'jquery.flot',
'jquery.flot.time',
],
function (angular, app, _, kbn, $) {
'use strict';
var module = angular.module('grafana.panels.graph');
module.directive('graphLegend', function(popoverSrv) {
return {
link: function(scope, elem) {
var $container = $('<section class="graph-legend"></section>');
var firstRender = true;
var panel = scope.panel;
var data;
var seriesList;
var i;
scope.$on('render', function() {
data = scope.seriesList;
if (data) {
render();
}
});
function getSeriesIndexForElement(el) {
return el.parents('[data-series-index]').data('series-index');
}
function openColorSelector(e) {
var el = $(e.currentTarget);
var index = getSeriesIndexForElement(el);
var seriesInfo = seriesList[index];
var popoverScope = scope.$new();
popoverScope.series = seriesInfo;
popoverSrv.show({
element: $(':first-child', el),
templateUrl: 'app/panels/graph/legend.popover.html',
scope: popoverScope
});
}
function toggleSeries(e) {
var el = $(e.currentTarget);
var index = getSeriesIndexForElement(el);
var seriesInfo = seriesList[index];
scope.toggleSeries(seriesInfo, e);
}
function sortLegend(e) {
var el = $(e.currentTarget);
var stat = el.data('stat');
if (stat !== panel.legend.sort) { panel.legend.sortDesc = null; }
// if already sort ascending, disable sorting
if (panel.legend.sortDesc === false) {
panel.legend.sort = null;
panel.legend.sortDesc = null;
render();
return;
}
panel.legend.sortDesc = !panel.legend.sortDesc;
panel.legend.sort = stat;
render();
}
function getTableHeaderHtml(statName) {
if (!panel.legend[statName]) { return ""; }
var html = '<th class="pointer" data-stat="' + statName + '">' + statName;
if (panel.legend.sort === statName) {
var cssClass = panel.legend.sortDesc ? 'icon-caret-down' : 'icon-caret-up' ;
html += ' <span class="' + cssClass + '"></span>';
}
return html + '</th>';
}
function render() {
if (firstRender) {
elem.append($container);
$container.on('click', '.graph-legend-icon', openColorSelector);
$container.on('click', '.graph-legend-alias', toggleSeries);
$container.on('click', 'th', sortLegend);
firstRender = false;
}
seriesList = data;
$container.empty();
$container.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
if (panel.legend.alignAsTable) {
var header = '<tr>';
header += '<th colspan="2" style="text-align:left"></th>';
if (panel.legend.values) {
header += getTableHeaderHtml('min');
header += getTableHeaderHtml('max');
header += getTableHeaderHtml('avg');
header += getTableHeaderHtml('current');
header += getTableHeaderHtml('total');
}
header += '</tr>';
$container.append($(header));
}
if (panel.legend.sort) {
seriesList = _.sortBy(seriesList, function(series) {
return series.stats[panel.legend.sort];
});
if (panel.legend.sortDesc) {
seriesList = seriesList.reverse();
}
}
for (i = 0; i < seriesList.length; i++) {
var series = seriesList[i];
var html = '<div class="graph-legend-series';
if (series.yaxis === 2) { html += ' pull-right'; }
if (scope.hiddenSeries[series.alias]) { html += ' graph-legend-series-hidden'; }
html += '" data-series-index="' + i + '">';
html += '<div class="graph-legend-icon">';
html += '<i class="icon-minus pointer" style="color:' + series.color + '"></i>';
html += '</div>';
html += '<div class="graph-legend-alias">';
html += '<a>' + series.label + '</a>';
html += '</div>';
var avg = series.formatValue(series.stats.avg);
var current = series.formatValue(series.stats.current);
var min = series.formatValue(series.stats.min);
var max = series.formatValue(series.stats.max);
var total = series.formatValue(series.stats.total);
if (panel.legend.values) {
if (panel.legend.min) { html += '<div class="graph-legend-value min">' + min + '</div>'; }
if (panel.legend.max) { html += '<div class="graph-legend-value max">' + max + '</div>'; }
if (panel.legend.avg) { html += '<div class="graph-legend-value avg">' + avg + '</div>'; }
if (panel.legend.current) { html += '<div class="graph-legend-value current">' + current + '</div>'; }
if (panel.legend.total) { html += '<div class="graph-legend-value total">' + total + '</div>'; }
}
html += '</div>';
$container.append($(html));
}
}
}
};
});
});

View File

@ -0,0 +1,26 @@
<div class="graph-legend-popover">
<a class="close" ng-click="dismiss();" href="">×</a>
<div class="editor-row small" style="padding-bottom: 0;">
<label>Axis:</label>
<button ng-click="toggleYAxis(series);dismiss();"
class="btn btn-mini"
ng-class="{'btn-success': series.yaxis === 1 }">
Left
</button>
<button ng-click="toggleYAxis(series);dismiss();"
class="btn btn-mini"
ng-class="{'btn-success': series.yaxis === 2 }">
Right
</button>
</div>
<div class="editor-row">
<i ng-repeat="color in colors"
class="pointer"
ng-class="{'icon-circle-blank': color === series.color,'icon-circle': color !== series.color}"
ng-style="{color:color}"
ng-click="changeSeriesColor(series, color);dismiss();">&nbsp;</i>
</div>
</div>

View File

@ -1,25 +1,22 @@
<div ng-controller='GraphCtrl'>
<div ng-controller='GraphCtrl'>
<div class="graph-wrapper" ng-class="{'graph-legend-rightside': panel.legend.rightSide}">
<div class="graph-canvas-wrapper">
<div class="graph-wrapper" ng-class="{'graph-legend-rightside': panel.legend.rightSide}">
<div class="graph-canvas-wrapper">
<div ng-if="datapointsWarning" class="datapoints-warning">
<span class="small" ng-show="!datapointsCount">No datapoints <tip>Can be caused by timezone mismatch between browser and graphite server</tip></span>
<span class="small" ng-show="datapointsOutside">Datapoints outside time range <tip>Can be caused by timezone mismatch between browser and graphite server</tip></span>
</div>
<div ng-if="datapointsWarning" class="datapoints-warning">
<span class="small" ng-show="!datapointsCount">No datapoints <tip>Can be caused by timezone mismatch between browser and graphite server</tip></span>
<span class="small" ng-show="datapointsOutside">Datapoints outside time range <tip>Can be caused by timezone mismatch between browser and graphite server</tip></span>
</div>
<div grafana-graph class="histogram-chart">
</div>
<div grafana-graph class="histogram-chart">
</div>
</div>
</div>
<div class="graph-legend-wrapper"
ng-if="panel.legend.show"
ng-include="'app/panels/graph/legend.html'">
</div>
</div>
<div class="graph-legend-wrapper" ng-if="panel.legend.show" graph-legend></div>
</div>
<div class="clearfix"></div>
<div class="clearfix"></div>
<div style="margin-top: 30px" ng-if="editMode">
<div class="dashboard-editor-header">
@ -29,13 +26,13 @@
</div>
<div ng-model="editor.index" bs-tabs>
<div ng-repeat="tab in editorTabs" data-title="{{tab}}">
<div ng-repeat="tab in panelMeta.editorTabs" data-title="{{tab.title}}">
</div>
</div>
</div>
<div class="dashboard-editor-body">
<div ng-repeat="tab in panelMeta.fullEditorTabs" ng-if="editorTabs[editor.index] == tab.title">
<div ng-repeat="tab in panelMeta.editorTabs" ng-if="editor.index === $index">
<div ng-include src="tab.src"></div>
</div>
</div>

View File

@ -6,82 +6,46 @@ define([
'kbn',
'moment',
'components/timeSeries',
'./seriesOverridesCtrl',
'components/panelmeta',
'services/panelSrv',
'services/annotationsSrv',
'services/datasourceSrv',
'jquery.flot',
'jquery.flot.events',
'jquery.flot.selection',
'jquery.flot.time',
'jquery.flot.stack',
'jquery.flot.stackpercent',
'jquery.flot.crosshair'
'./seriesOverridesCtrl',
'./graph',
'./legend',
],
function (angular, app, $, _, kbn, moment, TimeSeries) {
function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
'use strict';
var module = angular.module('grafana.panels.graph');
app.useModule(module);
module.controller('GraphCtrl', function($scope, $rootScope, panelSrv, annotationsSrv, timeSrv) {
$scope.panelMeta = {
modals : [],
editorTabs: [],
fullEditorTabs : [
{
title: 'General',
src:'app/partials/panelgeneral.html'
},
{
title: 'Metrics',
src:'app/partials/metrics.html'
},
{
title:'Axes & Grid',
src:'app/panels/graph/axisEditor.html'
},
{
title:'Display Styles',
src:'app/panels/graph/styleEditor.html'
}
],
fullscreenEdit: true,
fullscreenView: true,
description : "Graphing"
};
$scope.panelMeta = new PanelMeta({
description: 'Graph panel',
fullscreen: true,
metricsEditor: true
});
$scope.panelMeta.addEditorTab('Axes & Grid', 'app/panels/graph/axisEditor.html');
$scope.panelMeta.addEditorTab('Display Styles', 'app/panels/graph/styleEditor.html');
$scope.panelMeta.addExtendedMenuItem('Export CSV', '', 'exportCsv()');
$scope.panelMeta.addExtendedMenuItem('Toggle legend', '', 'toggleLegend()');
// Set and populate defaults
var _d = {
// datasource name, null = default datasource
datasource: null,
/** @scratch /panels/histogram/3
* renderer:: sets client side (flot) or native graphite png renderer (png)
*/
// sets client side (flot) or native graphite png renderer (png)
renderer: 'flot',
/** @scratch /panels/histogram/3
* x-axis:: Show the x-axis
*/
// Show/hide the x-axis
'x-axis' : true,
/** @scratch /panels/histogram/3
* y-axis:: Show the y-axis
*/
// Show/hide y-axis
'y-axis' : true,
/** @scratch /panels/histogram/3
* scale:: Scale the y-axis by this factor
*/
scale : 1,
/** @scratch /panels/histogram/3
* y_formats :: 'none','bytes','bits','bps','short', 's', 'ms'
*/
// y axis formats, [left axis,right axis]
y_formats : ['short', 'short'],
/** @scratch /panels/histogram/5
* grid object:: Min and max y-axis values
* grid.min::: Minimum y-axis value
* grid.ma1::: Maximum y-axis value
*/
// grid options
grid : {
leftMax: null,
rightMax: null,
@ -92,48 +56,23 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
threshold1Color: 'rgba(216, 200, 27, 0.27)',
threshold2Color: 'rgba(234, 112, 112, 0.22)'
},
annotate : {
enable : false,
},
/** @scratch /panels/histogram/3
* resolution:: If auto_int is true, shoot for this many bars.
*/
resolution : 100,
/** @scratch /panels/histogram/3
* ==== Drawing options
* lines:: Show line chart
*/
// show/hide lines
lines : true,
/** @scratch /panels/histogram/3
* fill:: Area fill factor for line charts, 1-10
*/
// fill factor
fill : 0,
/** @scratch /panels/histogram/3
* linewidth:: Weight of lines in pixels
*/
// line width in pixels
linewidth : 1,
/** @scratch /panels/histogram/3
* points:: Show points on chart
*/
// show hide points
points : false,
/** @scratch /panels/histogram/3
* pointradius:: Size of points in pixels
*/
// point radius in pixels
pointradius : 5,
/** @scratch /panels/histogram/3
* bars:: Show bars on chart
*/
// show hide bars
bars : false,
/** @scratch /panels/histogram/3
* stack:: Stack multiple series
*/
// enable/disable stacking
stack : false,
/** @scratch /panels/histogram/3
* legend:: Display the legend
*/
// stack percentage mode
percentage : false,
// legend options
legend: {
show: true, // disable/enable legend
values: false, // disable/enable legend values
@ -143,31 +82,20 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
total: false,
avg: false
},
/** @scratch /panels/histogram/3
* ==== Transformations
/** @scratch /panels/histogram/3
* percentage:: Show the y-axis as a percentage of the axis total. Only makes sense for multiple
* queries
*/
percentage : false,
/** @scratch /panels/histogram/3
* zerofill:: Improves the accuracy of line charts at a small performance cost.
*/
zerofill : true,
// how null points should be handled
nullPointMode : 'connected',
// staircase line mode
steppedLine: false,
// tooltip options
tooltip : {
value_type: 'cumulative',
shared: false,
},
// metric queries
targets: [{}],
// series color overrides
aliasColors: {},
// other style overrides
seriesOverrides: [],
};
@ -178,11 +106,17 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
_.defaults($scope.panel.legend, _d.legend);
$scope.hiddenSeries = {};
$scope.seriesList = [];
$scope.updateTimeRange = function () {
$scope.range = timeSrv.timeRange();
$scope.rangeUnparsed = timeSrv.timeRange(false);
$scope.resolution = Math.ceil($(window).width() * ($scope.panel.span / 12));
if ($scope.panel.maxDataPoints) {
$scope.resolution = $scope.panel.maxDataPoints;
}
else {
$scope.resolution = Math.ceil($(window).width() * ($scope.panel.span / 12));
}
$scope.interval = kbn.calculateInterval($scope.range, $scope.resolution, $scope.panel.interval);
};
@ -206,13 +140,13 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
$scope.panelMeta.loading = false;
$scope.panelMeta.error = err.message || "Timeseries data request error";
$scope.inspector.error = err;
$scope.seriesList = [];
$scope.render([]);
});
};
$scope.dataHandler = function(results) {
$scope.panelMeta.loading = false;
$scope.legend = [];
// png renderer returns just a url
if (_.isString(results)) {
@ -224,16 +158,16 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
$scope.datapointsCount = 0;
$scope.datapointsOutside = false;
var data = _.map(results.data, $scope.seriesHandler);
$scope.seriesList = _.map(results.data, $scope.seriesHandler);
$scope.datapointsWarning = $scope.datapointsCount === 0 || $scope.datapointsOutside;
$scope.annotationsPromise
.then(function(annotations) {
data.annotations = annotations;
$scope.render(data);
$scope.seriesList.annotations = annotations;
$scope.render($scope.seriesList);
}, function() {
$scope.render(data);
$scope.render($scope.seriesList);
});
};
@ -242,20 +176,14 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
var alias = seriesData.target;
var color = $scope.panel.aliasColors[alias] || $rootScope.colors[index];
var seriesInfo = {
alias: alias,
color: color,
};
$scope.legend.push(seriesInfo);
var series = new TimeSeries({
datapoints: datapoints,
info: seriesInfo,
alias: alias,
color: color,
});
if (datapoints && datapoints.length > 0) {
var last = moment.utc(datapoints[datapoints.length - 1][1] * 1000);
var last = moment.utc(datapoints[datapoints.length - 1][1]);
var from = moment.utc($scope.range.from);
if (last - from < -10000) {
$scope.datapointsOutside = true;
@ -268,7 +196,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
};
$scope.render = function(data) {
$scope.$emit('render', data);
$scope.$broadcast('render', data);
};
$scope.changeSeriesColor = function(series, color) {
@ -278,18 +206,18 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
};
$scope.toggleSeries = function(serie, event) {
if ($scope.hiddenSeries[serie.alias]) {
delete $scope.hiddenSeries[serie.alias];
}
else {
$scope.hiddenSeries[serie.alias] = true;
}
if (event.ctrlKey || event.metaKey || event.shiftKey) {
if ($scope.hiddenSeries[serie.alias]) {
delete $scope.hiddenSeries[serie.alias];
}
else {
$scope.hiddenSeries[serie.alias] = true;
}
} else {
$scope.toggleSeriesExclusiveMode(serie);
}
$scope.$emit('toggleLegend', $scope.legend);
$scope.render();
};
$scope.toggleSeriesExclusiveMode = function(serie) {
@ -300,7 +228,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
}
// check if every other series is hidden
var alreadyExclusive = _.every($scope.legend, function(value) {
var alreadyExclusive = _.every($scope.seriesList, function(value) {
if (value.alias === serie.alias) {
return true;
}
@ -310,13 +238,13 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
if (alreadyExclusive) {
// remove all hidden series
_.each($scope.legend, function(value) {
_.each($scope.seriesList, function(value) {
delete $scope.hiddenSeries[value.alias];
});
}
else {
// hide all but this serie
_.each($scope.legend, function(value) {
_.each($scope.seriesList, function(value) {
if (value.alias === serie.alias) {
return;
}
@ -341,8 +269,8 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
$scope.render();
};
$scope.addSeriesOverride = function() {
$scope.panel.seriesOverrides.push({});
$scope.addSeriesOverride = function(override) {
$scope.panel.seriesOverrides.push(override || {});
};
$scope.removeSeriesOverride = function(override) {
@ -350,12 +278,14 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
$scope.render();
};
$scope.toggleEditorHelp = function(index) {
if ($scope.editorHelpIndex === index) {
$scope.editorHelpIndex = null;
return;
}
$scope.editorHelpIndex = index;
// Called from panel menu
$scope.toggleLegend = function() {
$scope.panel.legend.show = !$scope.panel.legend.show;
$scope.get_data();
};
$scope.exportCsv = function() {
kbn.exportSeriesListToCsv($scope.seriesList);
};
panelSrv.init($scope);

View File

@ -23,7 +23,7 @@ define([
option.submenu = _.map(values, function(value, index) {
return {
text: String(value),
click: 'setOverride(' + option.index + ',' + index + ')'
click: 'menuItemSelected(' + option.index + ',' + index + ')'
};
});
@ -34,6 +34,14 @@ define([
var option = $scope.overrideMenu[optionIndex];
var value = option.values[valueIndex];
$scope.override[option.propertyName] = value;
// automatically disable lines for this series and the fill bellow to series
// can be removed by the user if they still want lines
if (option.propertyName === 'fillBelowTo') {
$scope.override['lines'] = false;
$scope.addSeriesOverride({ alias: value, lines: false });
}
$scope.updateCurrentOverrides();
$scope.render();
};
@ -45,8 +53,8 @@ define([
};
$scope.getSeriesNames = function() {
return _.map($scope.legend, function(info) {
return info.alias;
return _.map($scope.seriesList, function(series) {
return series.alias;
});
};
@ -67,6 +75,7 @@ define([
$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('Fill below to', 'fillBelowTo', $scope.getSeriesNames());
$scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]);
$scope.addOverrideOption('Points', 'points', [true, false]);
$scope.addOverrideOption('Points Radius', 'pointradius', [1,2,3,4,5]);

View File

@ -88,11 +88,10 @@
<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'" data-placement="top">
<i class="icon-plus"></i>
</a>
<li class="dropdown" dropdown-typeahead="overrideMenu" dropdown-typeahead-on-select="setOverride($optionIndex, $valueIndex)">
</li>
</ul>
<div class="clearfix"></div>
</div>

View File

@ -0,0 +1,76 @@
<div class="editor-row">
<div class="section">
<h5>Big value</h5>
<div class="editor-option">
<label class="small">Prefix</label>
<input type="text" class="input-small" ng-model="panel.prefix" ng-blur="render()"></input>
</div>
<div class="editor-option">
<label class="small">Value</label>
<select class="input-small" ng-model="panel.valueName" ng-options="f for f in ['min','max','avg', 'current', 'total']" ng-change="render()"></select>
</div>
<div class="editor-option">
<label class="small">Postfix</label>
<input type="text" class="input-small" ng-model="panel.postfix" ng-blur="render()" ng-trim="false"></input>
</div>
</div>
<div class="section">
<h5>Big value font size</h5>
<div class="editor-option">
<label class="small">Prefix</label>
<select class="input-mini" style="width: 75px;" ng-model="panel.prefixFontSize" ng-options="f for f in ['30%','50%','70%','80%','100%']" ng-change="render()"></select>
</div>
<div class="editor-option">
<label class="small">Value</label>
<select class="input-mini" style="width: 75px;" ng-model="panel.valueFontSize" ng-options="f for f in ['30%','50%','70%','80%','100%', '110%', '120%']" ng-change="render()"></select>
</div>
<div class="editor-option">
<label class="small">Postfix</label>
<select class="input-mini" style="width: 75px;" ng-model="panel.postfixFontSize" ng-options="f for f in ['30%','50%','70%','80%','100%']" ng-change="render()"></select>
</div>
</div>
<div class="section">
<h5>Formats</h5>
<div class="editor-option">
<label class="small">Unit format</label>
<select class="input-small" ng-model="panel.format" ng-options="f for f in ['none','short','bytes', 'bits', 'bps', 's', 'ms', 'µs', 'ns', 'percent']" ng-change="render()"></select>
</div>
</div>
<div class="section">
<h5>Coloring</h5>
<editor-opt-bool text="Background" model="panel.colorBackground" change="setColoring({background: true})"></editor-opt-bool>
<editor-opt-bool text="Value" model="panel.colorValue" change="setColoring({value: true})"></editor-opt-bool>
<div class="editor-option">
<label class="small">Thresholds<tip>Comma seperated values</tip></label>
<input type="text" class="input-large" ng-model="panel.thresholds" ng-blur="render()" placeholder="0,50,80"></input>
</div>
<div class="editor-option">
<label class="small">Color</label>
<spectrum-picker ng-model="panel.colors[0]" ng-change="render()" ></spectrum-picker>
<spectrum-picker ng-model="panel.colors[1]" ng-change="render()" ></spectrum-picker>
<spectrum-picker ng-model="panel.colors[2]" ng-change="render()" ></spectrum-picker>
<a class="pointer" ng-click="invertColorOrder()">invert order</a>
</div>
</div>
</div>
<div class="editor-row">
<div class="section">
<h5>Spark lines</h5>
<editor-opt-bool text="Spark line" model="panel.sparkline.show" change="render()"></editor-opt-bool>
<editor-opt-bool text="Background mode" model="panel.sparkline.full" change="render()"></editor-opt-bool>
<div class="editor-option">
<label class="small">Line color</label>
<spectrum-picker ng-model="panel.sparkline.lineColor" ng-change="render()" ></spectrum-picker>
</div>
<div class="editor-option">
<label class="small">Fill color</label>
<spectrum-picker ng-model="panel.sparkline.fillColor" ng-change="render()" ></spectrum-picker>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,26 @@
<div ng-controller='SingleStatCtrl'>
<div class="singlestat-panel" singlestat-panel></div>
<div class="clearfix"></div>
<div style="margin-top: 30px" ng-if="editMode">
<div class="dashboard-editor-header">
<div class="dashboard-editor-title">
<i class="icon icon-dashboard"></i>
Singlestat
</div>
<div ng-model="editor.index" bs-tabs>
<div ng-repeat="tab in panelMeta.editorTabs" data-title="{{tab.title}}">
</div>
</div>
</div>
<div class="dashboard-editor-body">
<div ng-repeat="tab in panelMeta.editorTabs" ng-if="editor.index === $index">
<div ng-include src="tab.src"></div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,197 @@
define([
'angular',
'app',
'lodash',
'components/timeSeries',
'kbn',
'components/panelmeta',
'services/panelSrv',
'./singleStatPanel',
],
function (angular, app, _, TimeSeries, kbn, PanelMeta) {
'use strict';
var module = angular.module('grafana.panels.singlestat');
app.useModule(module);
module.controller('SingleStatCtrl', function($scope, panelSrv, timeSrv) {
$scope.panelMeta = new PanelMeta({
description: 'Singlestat panel',
titlePos: 'left',
fullscreen: true,
metricsEditor: true
});
$scope.panelMeta.addEditorTab('Options', 'app/panels/singlestat/editor.html');
// Set and populate defaults
var _d = {
links: [],
maxDataPoints: 100,
interval: null,
targets: [{}],
cacheTimeout: null,
format: 'none',
prefix: '',
postfix: '',
valueName: 'avg',
prefixFontSize: '50%',
valueFontSize: '100%',
postfixFontSize: '50%',
thresholds: '',
colorBackground: false,
colorValue: false,
colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
sparkline: {
show: false,
full: false,
lineColor: 'rgb(31, 120, 193)',
fillColor: 'rgba(31, 118, 189, 0.18)',
}
};
_.defaults($scope.panel, _d);
$scope.init = function() {
panelSrv.init($scope);
$scope.$on('refresh', $scope.get_data);
};
$scope.updateTimeRange = function () {
$scope.range = timeSrv.timeRange();
$scope.rangeUnparsed = timeSrv.timeRange(false);
$scope.resolution = $scope.panel.maxDataPoints;
$scope.interval = kbn.calculateInterval($scope.range, $scope.resolution, $scope.panel.interval);
};
$scope.get_data = function() {
$scope.updateTimeRange();
var metricsQuery = {
range: $scope.rangeUnparsed,
interval: $scope.interval,
targets: $scope.panel.targets,
maxDataPoints: $scope.resolution,
cacheTimeout: $scope.panel.cacheTimeout
};
return $scope.datasource.query(metricsQuery)
.then($scope.dataHandler)
.then(null, function(err) {
console.log("err");
$scope.panelMeta.loading = false;
$scope.panelMeta.error = err.message || "Timeseries data request error";
$scope.inspector.error = err;
$scope.render();
});
};
$scope.dataHandler = function(results) {
$scope.panelMeta.loading = false;
$scope.series = _.map(results.data, $scope.seriesHandler);
$scope.render();
};
$scope.seriesHandler = function(seriesData) {
var series = new TimeSeries({
datapoints: seriesData.datapoints,
alias: seriesData.target,
});
series.flotpairs = series.getFlotPairs('connected');
return series;
};
$scope.setColoring = function(options) {
if (options.background) {
$scope.panel.colorValue = false;
$scope.panel.colors = ['rgba(71, 212, 59, 0.4)', 'rgba(245, 150, 40, 0.73)', 'rgba(225, 40, 40, 0.59)'];
}
else {
$scope.panel.colorBackground = false;
$scope.panel.colors = ['rgba(50, 172, 45, 0.97)', 'rgba(237, 129, 40, 0.89)', 'rgba(245, 54, 54, 0.9)'];
}
$scope.render();
};
$scope.invertColorOrder = function() {
var tmp = $scope.panel.colors[0];
$scope.panel.colors[0] = $scope.panel.colors[2];
$scope.panel.colors[2] = tmp;
$scope.render();
};
$scope.getDecimalsForValue = function(value) {
var opts = {};
if (value === 0) {
return { decimals: 0, scaledDecimals: 0 };
}
var delta = value / 2;
var dec = -Math.floor(Math.log(delta) / Math.LN10);
var magn = Math.pow(10, -dec),
norm = delta / magn, // norm is between 1.0 and 10.0
size;
if (norm < 1.5) {
size = 1;
} else if (norm < 3) {
size = 2;
// special case for 2.5, requires an extra decimal
if (norm > 2.25) {
size = 2.5;
++dec;
}
} else if (norm < 7.5) {
size = 5;
} else {
size = 10;
}
size *= magn;
if (opts.minTickSize != null && size < opts.minTickSize) {
size = opts.minTickSize;
}
var result = {};
result.decimals = Math.max(0, dec);
result.scaledDecimals = result.decimals - Math.floor(Math.log(size) / Math.LN11) + 2;
return result;
};
$scope.render = function() {
var data = {};
if (!$scope.series || $scope.series.length === 0) {
data.flotpairs = [];
data.mainValue = Number.NaN;
data.mainValueFormated = 'NaN';
}
else {
var series = $scope.series[0];
data.mainValue = series.stats[$scope.panel.valueName];
var decimalInfo = $scope.getDecimalsForValue(data.mainValue);
var formatFunc = kbn.valueFormats[$scope.panel.format];
data.mainValueFormated = formatFunc(data.mainValue, decimalInfo.decimals, decimalInfo.scaledDecimals);
data.flotpairs = series.flotpairs;
}
data.thresholds = $scope.panel.thresholds.split(',').map(function(strVale) {
return Number(strVale.trim());
});
data.colorMap = $scope.panel.colors;
$scope.data = data;
$scope.$emit('render');
};
$scope.init();
});
});

View File

@ -0,0 +1,204 @@
define([
'angular',
'app',
'lodash',
'jquery',
'jquery.flot',
],
function (angular, app, _, $) {
'use strict';
var module = angular.module('grafana.panels.singlestat', []);
app.useModule(module);
module.directive('singlestatPanel', function($location, linkSrv, $timeout) {
return {
link: function(scope, elem) {
var data, panel;
var $panelContainer = elem.parents('.panel-container');
scope.$on('render', function() {
render();
});
function setElementHeight() {
try {
var height = scope.height || panel.height || scope.row.height;
if (_.isString(height)) {
height = parseInt(height.replace('px', ''), 10);
}
height -= panel.title ? 24 : 9; // subtract panel title bar
elem.css('height', height + 'px');
return true;
} catch(e) { // IE throws errors sometimes
return false;
}
}
function applyColoringThresholds(value, valueString) {
if (!panel.colorValue) {
return valueString;
}
var color = getColorForValue(value);
if (color) {
return '<span style="color:' + color + '">'+ valueString + '</span>';
}
return valueString;
}
function getColorForValue(value) {
for (var i = data.thresholds.length - 1; i >= 0 ; i--) {
if (value > data.thresholds[i]) {
return data.colorMap[i];
}
}
return null;
}
function getSpan(className, fontSize, value) {
return '<span class="' + className + '" style="font-size:' + fontSize + '">' +
value + '</span>';
}
function getBigValueHtml() {
var body = '<div class="singlestat-panel-value-container">';
if (panel.prefix) { body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, scope.panel.prefix); }
var value = applyColoringThresholds(data.mainValue, data.mainValueFormated);
body += getSpan('singlestat-panel-value', panel.valueFontSize, value);
if (panel.postfix) { body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, panel.postfix); }
body += '</div>';
return body;
}
function addSparkline() {
var panel = scope.panel;
var width = elem.width() + 20;
var height = elem.height() || 100;
var plotCanvas = $('<div></div>');
var plotCss = {};
plotCss.position = 'absolute';
if (panel.sparkline.full) {
plotCss.bottom = '5px';
plotCss.left = '-5px';
plotCss.width = (width - 10) + 'px';
plotCss.height = (height - 45) + 'px';
}
else {
plotCss.bottom = "0px";
plotCss.left = "-5px";
plotCss.width = (width - 10) + 'px';
plotCss.height = Math.floor(height * 0.25) + "px";
}
plotCanvas.css(plotCss);
var options = {
legend: { show: false },
series: {
lines: {
show: true,
fill: 1,
lineWidth: 1,
fillColor: panel.sparkline.fillColor,
},
},
yaxes: { show: false },
xaxis: {
show: false,
mode: "time",
min: scope.range.from.getTime(),
max: scope.range.to.getTime(),
},
grid: { hoverable: false, show: false },
};
elem.append(plotCanvas);
var plotSeries = {
data: data.flotpairs,
color: panel.sparkline.lineColor
};
setTimeout(function() {
$.plot(plotCanvas, [plotSeries], options);
}, 10);
}
function render() {
if (!scope.data) { return; }
data = scope.data;
panel = scope.panel;
setElementHeight();
var body = getBigValueHtml();
if (panel.colorBackground && data.mainValue) {
var color = getColorForValue(data.mainValue);
if (color) {
$panelContainer.css('background-color', color);
if (scope.fullscreen) {
elem.css('background-color', color);
} else {
elem.css('background-color', '');
}
}
} else {
$panelContainer.css('background-color', '');
elem.css('background-color', '');
}
elem.html(body);
if (panel.sparkline.show) {
addSparkline();
}
elem.toggleClass('pointer', panel.links.length > 0);
}
// drilldown link tooltip
var drilldownTooltip = $('<div id="tooltip" class="">gello</div>"');
elem.mouseleave(function() {
if (panel.links.length === 0) { return;}
drilldownTooltip.detach();
});
elem.click(function() {
if (panel.links.length === 0) { return; }
var linkInfo = linkSrv.getPanelLinkAnchorInfo(panel.links[0]);
if (linkInfo.href[0] === '#') { linkInfo.href = linkInfo.href.substring(1); }
$timeout(function() { $location.url(linkInfo.href); });
drilldownTooltip.detach();
});
elem.mousemove(function(e) {
if (panel.links.length === 0) { return;}
drilldownTooltip.text('click to go to: ' + panel.links[0].title);
drilldownTooltip.place_tt(e.clientX+20, e.clientY-15);
});
}
};
});
});

View File

@ -3,8 +3,9 @@ define([
'app',
'lodash',
'require',
'components/panelmeta',
],
function (angular, app, _, require) {
function (angular, app, _, require, PanelMeta) {
'use strict';
var module = angular.module('grafana.panels.text', []);
@ -14,13 +15,15 @@ function (angular, app, _, require) {
module.controller('text', function($scope, templateSrv, $sce, panelSrv) {
$scope.panelMeta = {
$scope.panelMeta = new PanelMeta({
description : "A static text panel that can use plain text, markdown, or (sanitized) HTML"
};
});
$scope.panelMeta.addEditorTab('Edit text', 'app/panels/text/editor.html');
// Set and populate defaults
var _d = {
title: 'default title',
title : 'default title',
mode : "markdown", // 'html', 'markdown', 'text'
content : "",
style: {},
@ -29,7 +32,7 @@ function (angular, app, _, require) {
_.defaults($scope.panel, _d);
$scope.init = function() {
panelSrv.init(this);
panelSrv.init($scope);
$scope.ready = false;
$scope.$on('refresh', $scope.render);
$scope.render();

View File

@ -10,19 +10,26 @@
}
</style>
<form name="input" style="margin:0">
<ul class="nav nav-pills timepicker-dropdown">
<li class="dropdown">
<ul class="nav timepicker-dropdown">
<a class="dropdown-toggle timepicker-dropdown" data-toggle="dropdown" href="" bs-tooltip="time.tooltip" data-placement="bottom" ng-click="dismiss();">
<span ng-bind="time.rangeString"></span>
<span ng-show="dashboard.refresh" class="text-warning">refreshed every {{dashboard.refresh}} </span>
<i class="icon-caret-down"></i>
</a>
<li class="grafana-menu-zoom-out">
<a class='small' ng-click='zoom(2)'>
Zoom Out
</a>
</li>
<ul class="dropdown-menu">
<!-- Relative time options -->
<li bindonce ng-repeat='timespan in panel.time_options track by $index'>
<a ng-click="setRelativeFilter(timespan)" bo-text="'Last ' + timespan"></a>
<li class="dropdown">
<a class="dropdown-toggle timepicker-dropdown" data-toggle="dropdown" href="" bs-tooltip="time.tooltip" data-placement="bottom" ng-click="dismiss();">
<span ng-bind="time.rangeString"></span>
<span ng-show="dashboard.refresh" class="text-warning">refreshed every {{dashboard.refresh}} </span>
<i class="icon-caret-down"></i>
</a>
<ul class="dropdown-menu">
<!-- Relative time options -->
<li bindonce ng-repeat='timespan in panel.time_options track by $index'>
<a ng-click="setRelativeFilter(timespan)" bo-text="'Last ' + timespan"></a>
</li>
<!-- Auto refresh submenu -->

View File

@ -75,8 +75,8 @@ function (angular, app, _, moment, kbn) {
// Date picker needs the date to be at the start of the day
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();
$scope.temptime.from.date = moment($scope.temptime.from.date).add(1, 'days').toDate();
$scope.temptime.to.date = moment($scope.temptime.to.date).add(1, 'days').toDate();
}
$scope.appEvent('show-dash-editor', {src: 'app/panels/timepicker/custom.html', scope: $scope });

View File

@ -0,0 +1,23 @@
<div class="modal-body">
<div class="dashboard-editor-header">
<div class="dashboard-editor-title">
<i class="icon icon-ok"></i>
{{title}}
</div>
</div>
<div class="dashboard-editor-body">
<p class="row-fluid text-center large">
{{text}}
<br>
<br>
</p>
<div class="row-fluid">
<span class="span4"></span>
<button type="button" class="btn btn-success span2" ng-click="dismiss()">No</button>
<button type="button" class="btn btn-danger span2" ng-click="onConfirm();dismiss();">Yes</button>
<span class="span4"></span>
</div>
</div>

View File

@ -14,7 +14,7 @@
<div class="main-view-container">
<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-control-inner">
<div class="row-close" ng-show="row.collapse" data-placement="bottom" >
<div class="row-close-buttons">
<span class="row-button bgPrimary" ng-click="toggle_row(row)">
@ -77,29 +77,25 @@
<div class="panels-wrapper" ng-if="!row.collapse">
<div class="row-text pointer" ng-click="toggle_row(row)" ng-if="row.showTitle" ng-bind="row.title">
</div>
<div class="panel-menu-container" data-menu-container>
<!-- <a class="pointer"><i class="icon&#45;eye&#45;open"></i> <span>view</span></a> -->
<!-- <a class="pointer"><i class="icon&#45;cog"></i> <span>edit</span></a> -->
<!-- <a class="pointer"><i class="icon&#45;resize&#45;horizontal"></i> <span>span</span></a> -->
<!-- <a class="pointer"><i class="icon&#45;copy"></i> <span>duplicate</span></a> -->
<!-- <a class="pointer"><i class="icon&#45;share"></i> <span>share</span></a> -->
<!-- <a class="pointer"><i class="icon&#45;remove"></i> <span>remove</span></a> -->
</div>
<!-- Panels -->
<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}">
class="panel"
ui-draggable="true" drag="panel.id"
ui-on-Drop="onDrop($data, row, panel)"
drag-handle-class="drag-handle" panel-width ng-model="panel">
<grafana-panel type="panel.type" ng-cloak></grafana-panel>
</div>
<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 panel-drop-zone class="panel panel-drop-zone"
ui-on-drop="onDrop($data, row)"
data-drop="true">
<div class="panel-container" style="background: transparent">
<div style="text-align: center">
<em>Drop here</em>
</div>
</div>
</div>
<div class="clearfix"></div>

View File

@ -13,12 +13,6 @@
</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>

View File

@ -84,7 +84,7 @@
</div>
<div ng-repeat="pulldown in dashboard.nav" ng-controller="SubmenuCtrl" ng-show="editor.index == 4+$index">
<ng-include ng-show="pulldown.enable" src="edit_path(pulldown.type)"></ng-include>
<ng-include ng-show="pulldown.enable" src="pulldownEditorPath(pulldown.type)"></ng-include>
<button ng-hide="pulldown.enable" class="btn" ng-click="pulldown.enable = true">Enable the {{pulldown.type}}</button>
</div>

View File

@ -30,6 +30,19 @@
ng-click="duplicate()">
Duplicate
</a>
</li>
<li role="menuitem">
<a tabindex="1"
ng-click="moveMetricQuery($index, $index-1)">
Move up
</a>
</li>
<li role="menuitem">
<a tabindex="1"
ng-click="moveMetricQuery($index, $index+1)">
Move down
</a>
</li>
</ul>
</li>
<li>
@ -83,16 +96,29 @@
<i class="icon-wrench"></i>
</li>
<li class="grafana-target-segment">
cacheTimeout
Cache timeout
</li>
<li>
<input type="text"
class="input-mini grafana-target-segment-input"
ng-model="panel.cacheTimeout"
bs-tooltip="'Graphite parameter to overwride memcache default timeout (unit is seconds)'"
data-placement="right"
spellcheck='false'
placeholder="60">
class="input-mini grafana-target-segment-input"
ng-model="panel.cacheTimeout"
bs-tooltip="'Graphite parameter to overwride memcache default timeout (unit is seconds)'"
data-placement="right"
spellcheck='false'
placeholder="60">
</li>
<li class="grafana-target-segment">
Max data points
</li>
<li>
<input type="text"
class="input-mini grafana-target-segment-input"
ng-model="panel.maxDataPoints"
bs-tooltip="'Override max data points, automatically set to graph width in pixels.'"
data-placement="right"
ng-model-onblur ng-change="get_data()"
spellcheck='false'
placeholder="auto">
</li>
</ul>
<div class="clearfix"></div>
@ -122,6 +148,11 @@
templating
</a>
</li>
<li class="grafana-target-segment">
<a ng-click="toggleEditorHelp(5)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
max data points
</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
@ -177,7 +208,18 @@
</ul>
</div>
<div class="grafana-info-box span6" ng-if="editorHelpIndex === 5">
<h5>Max data points</h5>
<ul>
<li>Every graphite request is issued with a maxDataPoints parameter</li>
<li>Graphite uses this parameter to consolidate the real number of values down to this number</li>
<li>If there are more real values, then by default they will be consolidated using averages</li>
<li>This could hide real peaks and max values in your series</li>
<li>You can change how point consolidation is made using the consolidateBy graphite function</li>
<li>Point consolidation will effect series legend values (min,max,total,current)</li>
<li>If you override maxDataPoint and set a high value performance can be severely effected</li>
</ul>
</div>
</div>
</div>

View File

@ -0,0 +1,50 @@
<div class="modal-body">
<div class="dashboard-editor-header">
<div class="dashboard-editor-title">
<i class="icon icon-keyboard"></i>
Keyboard shutcuts
</div>
</div>
<div class="dashboard-editor-body">
<table class="shortcut-table">
<tr>
<th></th>
<th style="text-align: left;">Dashboard wide shortcuts</th>
</tr>
<tr>
<td style="text-align: right;"><span class="label label-info">ESC</span></td>
<td>Exit fullscreen edit/view mode, close search or any editor view</td>
</tr>
<tr>
<td><span class="label label-info">CTRL+F</span></td>
<td>Open dashboard search view (also contains import/playlist controls)</td>
</tr>
<tr>
<td><span class="label label-info">CTRL+S</span></td>
<td>Save dashboard</td>
</tr>
<tr>
<td><span class="label label-info">CTRL+H</span></td>
<td>Hide row controls</td>
</tr>
<tr>
<td><span class="label label-info">CTRL+Z</span></td>
<td>Zoom out</td>
</tr>
<tr>
<td><span class="label label-info">CTRL+R</span></td>
<td>Refresh (Fetches new data and rerenders panels)</td>
</tr>
<tr>
<td><span class="label label-info">CTRL+O</span></td>
<td>Enable/Disable shared graph crosshair</td>
</tr>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-info" ng-click="dismiss()">Close</button>
</div>

View File

@ -15,26 +15,26 @@
tabindex="1">
<i class="icon icon-cog"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem">
<a tabindex="1" ng-click="duplicate()">Duplicate</a>
<a tabindex="2" ng-click="showQuery()" ng-hide="target.rawQuery">Raw query mode</a>
<a tabindex="2" ng-click="hideQuery()" ng-show="target.rawQuery">Query editor mode</a>
</li>
</ul>
</li>
<li>
<a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
<i class="icon icon-remove"></i>
</a>
</li>
</ul>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem"><a tabindex="1" ng-click="duplicate()">Duplicate</a></li>
<li role="menuitem"><a tabindex="1" ng-click="showQuery()" ng-hide="target.rawQuery">Raw query mode</a></li>
<li role="menuitem"><a tabindex="1" ng-click="hideQuery()" ng-show="target.rawQuery">Query editor mode</a></li>
<li role="menuitem"><a tabindex="1" ng-click="moveMetricQuery($index, $index-1)">Move up </a></li>
<li role="menuitem"><a tabindex="1" ng-click="moveMetricQuery($index, $index+1)">Move down</a></li>
</ul>
</li>
<li>
<a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
<i class="icon icon-remove"></i>
</a>
</li>
</ul>
<ul class="grafana-segment-list">
<li>
<a class="grafana-target-segment" ng-click="target.hide = !target.hide; get_data();" role="menuitem">
<i class="icon-eye-open"></i>
</a>
<ul class="grafana-segment-list">
<li>
<a class="grafana-target-segment" ng-click="target.hide = !target.hide; get_data();" role="menuitem">
<i class="icon-eye-open"></i>
</a>
</li>
</ul>
@ -64,6 +64,8 @@
ng-model="target.series"
spellcheck='false'
bs-typeahead="listSeries"
match-all="true"
min-length="3"
placeholder="series name"
data-min-length=0 data-items=100
ng-blur="seriesBlur()">

View File

@ -14,4 +14,5 @@
</ul>
</div>
<div class="clearfix"></div>
</div>

View File

@ -89,6 +89,32 @@
ng-model="target.isCounter"
ng-change="targetBlur()">
</li>
<li class="grafana-target-segment" ng-hide="!target.isCounter">
Counter Max:
</li>
<li ng-hide="!target.isCounter">
<input type="text"
class="grafana-target-segment-input input-medium"
ng-disabled="!target.shouldComputeRate"
ng-model="target.counterMax"
spellcheck='false'
placeholder="Counter max value"
ng-blur="targetBlur()"
/>
</li>
<li class="grafana-target-segment" ng-hide="!target.isCounter">
Counter Reset Value:
</li>
<li ng-hide="!target.isCounter">
<input type="text"
class="grafana-target-segment-input input-medium"
ng-disabled="!target.shouldComputeRate"
ng-model="target.counterResetValue"
spellcheck='false'
placeholder="Counter reset value"
ng-blur="targetBlur()"
/>
</li>
<li class="grafana-target-segment">
Alias:
</li>

View File

@ -5,22 +5,14 @@
</div>
<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
<div ng-repeat="tab in setEditorTabs(panelMeta)" data-title="{{tab}}">
<div ng-repeat="tab in panelMeta.editorTabs" data-title="{{tab.title}}">
</div>
</div>
</div>
<div class="dashboard-editor-body">
<div ng-show="editorTabs[editor.index] == 'General'">
<div ng-include src="'app/partials/panelgeneral.html'"></div>
</div>
<div ng-show="editorTabs[editor.index] == 'Panel'">
<div ng-include src="edit_path(panel.type)"></div>
</div>
<div ng-repeat="tab in panelMeta.editorTabs" ng-show="editorTabs[editor.index] == tab.title">
<div ng-repeat="tab in panelMeta.editorTabs" ng-show="editor.index == $index">
<div ng-include src="tab.src"></div>
</div>
</div>

View File

@ -12,3 +12,7 @@
</div>
</div>
</div>
<panel-link-editor panel="panel"></panel-link-editor>

View File

@ -22,7 +22,7 @@
</button>
<span style="position: relative;">
<input type="text" placeholder="search dashboards, metrics, or graphs" xng-focus="giveSearchFocus"
ng-keydown="keyDown($event)" ng-model="query.query" spellcheck='false' ng-change="search()" />
ng-keydown="keyDown($event)" ng-model="query.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="search()" />
<a class="search-tagview-switch" href="javascript:void(0);" ng-class="{'active': tagsOnly}" ng-click="showTags($event)">tags</a>
</span>
</div>

View File

@ -16,21 +16,28 @@ function (angular, $, config, _, kbn, moment) {
.when('/dashboard/script/:jsFile', {
templateUrl: 'app/partials/dashboard.html',
controller : 'DashFromScriptProvider',
reloadOnSearch: false,
});
});
module.controller('DashFromScriptProvider', function($scope, $rootScope, $http, $routeParams, alertSrv, $q) {
module.controller('DashFromScriptProvider', function($scope, $rootScope, $http, $routeParams, $q, dashboardSrv, datasourceSrv, $timeout) {
var execute_script = function(result) {
var services = {
dashboardSrv: dashboardSrv,
datasourceSrv: datasourceSrv,
$q: $q,
};
/*jshint -W054 */
var script_func = new Function('ARGS','kbn','_','moment','window','document','$','jQuery', result.data);
var script_result = script_func($routeParams, kbn, _ , moment, window, document, $, $);
var script_func = new Function('ARGS','kbn','_','moment','window','document','$','jQuery', 'services', result.data);
var script_result = script_func($routeParams, kbn, _ , moment, window, document, $, $, services);
// Handle async dashboard scripts
if (_.isFunction(script_result)) {
var deferred = $q.defer();
script_result(function(dashboard) {
$rootScope.$apply(function() {
$timeout(function() {
deferred.resolve({ data: dashboard });
});
});
@ -47,7 +54,7 @@ function (angular, $, config, _, kbn, moment) {
.then(execute_script)
.then(null,function(err) {
console.log('Script dashboard error '+ err);
alertSrv.set('Error', "Could not load <i>scripts/"+file+"</i>. Please make sure it exists and returns a valid dashboard", 'error');
$scope.appEvent('alert-error', ["Script Error", "Please make sure it exists and returns a valid dashboard"]);
return false;
});
};

View File

@ -7,7 +7,7 @@ function (angular, _) {
var module = angular.module('grafana.services');
module.service('alertSrv', function($timeout, $sce, $rootScope) {
module.service('alertSrv', function($timeout, $sce, $rootScope, $modal, $q) {
var self = this;
this.init = function() {
@ -20,6 +20,7 @@ function (angular, _) {
$rootScope.onAppEvent('alert-success', function(e, alert) {
self.set(alert[0], alert[1], 'success', 3000);
});
$rootScope.onAppEvent('confirm-modal', this.showConfirmModal);
};
// List of all alert objects
@ -57,5 +58,28 @@ function (angular, _) {
this.clearAll = function() {
self.list = [];
};
this.showConfirmModal = function(e, payload) {
var scope = $rootScope.$new();
scope.title = payload.title;
scope.text = payload.text;
scope.onConfirm = payload.onConfirm;
var confirmModal = $modal({
template: './app/partials/confirm_modal.html',
persist: true,
modalClass: 'confirm-modal',
show: false,
scope: scope,
keyboard: false
});
$q.when(confirmModal).then(function(modalEl) {
modalEl.modal('show');
});
};
});
});

View File

@ -7,9 +7,9 @@ define([
'./templateValuesSrv',
'./panelSrv',
'./timer',
'./panelMove',
'./keyboardManager',
'./annotationsSrv',
'./popoverSrv',
'./playlistSrv',
'./unsavedChangesSrv',
'./dashboard/dashboardKeyBindings',

View File

@ -58,7 +58,7 @@ define([
function errorHandler(err) {
console.log('Annotation error: ', err);
var message = err.message || "Aannotation query failed";
var message = err.message || "Annotation query failed";
alertSrv.set('Annotations error', message,'error');
}

View File

@ -1,14 +1,13 @@
define([
'angular',
'jquery',
'services/all'
],
function(angular, $) {
"use strict";
var module = angular.module('grafana.services');
module.service('dashboardKeybindings', function($rootScope, keyboardManager) {
module.service('dashboardKeybindings', function($rootScope, keyboardManager, $modal, $q) {
this.shortcuts = function(scope) {
@ -22,6 +21,24 @@ function(angular, $) {
keyboardManager.unbind('esc');
});
var helpModalScope = null;
keyboardManager.bind('shift+?', function() {
if (helpModalScope) { return; }
helpModalScope = $rootScope.$new();
var helpModal = $modal({
template: './app/partials/help_modal.html',
persist: false,
show: false,
scope: helpModalScope,
keyboard: false
});
helpModalScope.$on('$destroy', function() { helpModalScope = null; });
$q.when(helpModal).then(function(modalEl) { modalEl.modal('show'); });
}, { inputDisabled: true });
keyboardManager.bind('ctrl+f', function() {
scope.appEvent('show-dash-editor', { src: 'app/partials/search.html' });
}, { inputDisabled: true });
@ -32,6 +49,10 @@ function(angular, $) {
scope.dashboard.emit_refresh('refresh');
}, { inputDisabled: true });
keyboardManager.bind('ctrl+l', function() {
scope.$broadcast('toggle-all-legends');
}, { inputDisabled: true });
keyboardManager.bind('ctrl+h', function() {
var current = scope.dashboard.hideControls;
scope.dashboard.hideControls = !current;

View File

@ -35,6 +35,7 @@ function (angular, $, kbn, _, moment) {
this.annotations = this._ensureListExist(data.annotations);
this.refresh = data.refresh;
this.version = data.version || 0;
this.hideAllLegends = data.hideAllLegends || false;
if (this.nav.length === 0) {
this.nav.push({ type: 'timepicker' });
@ -91,6 +92,26 @@ function (angular, $, kbn, _, moment) {
row.panels.push(panel);
};
p.getPanelInfoById = function(panelId) {
var result = {};
_.each(this.rows, function(row) {
_.each(row.panels, function(panel, index) {
if (panel.id === panelId) {
result.panel = panel;
result.row = row;
result.index = index;
return;
}
});
});
if (!result.panel) {
return null;
}
return result;
};
p.duplicatePanel = function(panel, row) {
var rowIndex = _.indexOf(this.rows, row);
var newPanel = angular.copy(panel);

View File

@ -32,33 +32,34 @@ function (angular, _, $) {
});
this.update(this.getQueryStringState(), true);
this.expandRowForPanel();
}
DashboardViewState.prototype.expandRowForPanel = function() {
if (!this.state.panelId) { return; }
var panelInfo = this.$scope.dashboard.getPanelInfoById(this.state.panelId);
if (panelInfo) {
panelInfo.row.collapse = false;
}
};
DashboardViewState.prototype.needsSync = function(urlState) {
return _.isEqual(this.state, urlState) === false;
};
DashboardViewState.prototype.getQueryStringState = function() {
var queryParams = $location.search();
var urlState = {
panelId: parseInt(queryParams.panelId) || null,
fullscreen: queryParams.fullscreen ? true : false,
edit: queryParams.edit ? true : false,
};
_.each(queryParams, function(value, key) {
if (key.indexOf('var-') !== 0) { return; }
urlState[key] = value;
});
return urlState;
var state = $location.search();
state.panelId = parseInt(state.panelId) || null;
state.fullscreen = state.fullscreen ? true : null;
state.edit = (state.edit === "true" || state.edit === true) || null;
return state;
};
DashboardViewState.prototype.serializeToUrl = function() {
var urlState = _.clone(this.state);
urlState.fullscreen = this.state.fullscreen ? true : null,
urlState.edit = this.state.edit ? true : null;
return urlState;
};
@ -68,7 +69,8 @@ function (angular, _, $) {
if (!this.state.fullscreen) {
this.state.panelId = null;
this.state.edit = false;
this.state.fullscreen = null;
this.state.edit = null;
}
if (!skipUrlSync) {

View File

@ -224,7 +224,7 @@ function (angular, _, config, kbn, moment) {
var endsInOpen = function(string, opener, closer) {
var character;
var count = 0;
for (var i=0; i<string.length; i++) {
for (var i = 0, len = string.length; i < len; i++) {
character = string[i];
if (character === opener) {
@ -279,18 +279,20 @@ function (angular, _, config, kbn, moment) {
return { dashboards: [], tags: [] };
}
var hits = { dashboards: [], tags: results.facets.tags.terms || [] };
var resultsHits = results.hits.hits;
var displayHits = { dashboards: [], tags: results.facets.tags.terms || [] };
for (var i = 0; i < results.hits.hits.length; i++) {
hits.dashboards.push({
id: results.hits.hits[i]._id,
title: results.hits.hits[i]._source.title,
tags: results.hits.hits[i]._source.tags
for (var i = 0, len = resultsHits.length; i < len; i++) {
var hit = resultsHits[i];
displayHits.dashboards.push({
id: hit._id,
title: hit._source.title,
tags: hit._source.tags
});
}
hits.tagsOnly = tagsOnly;
return hits;
displayHits.tagsOnly = tagsOnly;
return displayHits;
});
};

View File

@ -39,6 +39,13 @@ function (_) {
defaultParams: [1],
});
addFuncDef({
name: 'perSecond',
category: categories.Transform,
params: [],
defaultParams: [],
});
addFuncDef({
name: "holtWintersForecast",
category: categories.Calculate,
@ -93,6 +100,27 @@ function (_) {
category: categories.Combine,
});
addFuncDef({
name: 'mapSeries',
shortName: 'map',
params: [{ name: "node", type: 'int' }],
defaultParams: [3],
category: categories.Combine,
});
addFuncDef({
name: 'reduceSeries',
shortName: 'reduce',
params: [
{ name: "function", type: 'string', options: ['asPercent', 'diffSeries', 'divideSeries'] },
{ name: "reduceNode", type: 'int', options: [0,1,2,3,4,5,6,7,8,9,10,11,12,13] },
{ name: "reduceMatchers", type: 'string' },
{ name: "reduceMatchers", type: 'string' },
],
defaultParams: ['asPercent', 2, 'used_bytes', 'total_bytes'],
category: categories.Combine,
});
addFuncDef({
name: 'sumSeries',
shortName: 'sum',
@ -148,7 +176,10 @@ function (_) {
addFuncDef({
name: 'averageSeriesWithWildcards',
category: categories.Combine,
params: [{ name: "node", type: "int" }],
params: [
{ name: "node", type: "int" },
{ name: "node", type: "int", optional: true },
],
defaultParams: [3]
});
@ -193,7 +224,7 @@ function (_) {
{
name: "node",
type: "int",
options: [1,2,3,4,5,6,7,8,9,10,12]
options: [0,1,2,3,4,5,6,7,8,9,10,12]
},
{
name: "function",
@ -329,8 +360,12 @@ function (_) {
addFuncDef({
name: 'summarize',
category: categories.Transform,
params: [{ name: "interval", type: "string" }, { name: "func", type: "select", options: ['sum', 'avg', 'min', 'max', 'last'] }],
defaultParams: ['1h', 'sum']
params: [
{ name: "interval", type: "string" },
{ name: "func", type: "select", options: ['sum', 'avg', 'min', 'max', 'last'] },
{ name: "alignToFrom", type: "boolean", optional: true, options: ['false', 'true'] },
],
defaultParams: ['1h', 'sum', 'false']
});
addFuncDef({
@ -533,7 +568,7 @@ function (_) {
var parameters = _.map(this.params, function(value, index) {
var paramType = this.def.params[index].type;
if (paramType === 'int' || paramType === 'value_or_series') {
if (paramType === 'int' || paramType === 'value_or_series' || paramType === 'boolean') {
return value;
}

View File

@ -53,13 +53,24 @@ function (angular, _, $, config, kbn, moment) {
httpOptions.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
}
return this.doGraphiteRequest(httpOptions);
return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs);
}
catch(err) {
return $q.reject(err);
}
};
GraphiteDatasource.prototype.convertDataPointsToMs = function(result) {
if (!result || !result.data) { return []; }
for (var i = 0; i < result.data.length; i++) {
var series = result.data[i];
for (var y = 0; y < series.datapoints.length; y++) {
series.datapoints[y][1] *= 1000;
}
}
return result;
};
GraphiteDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) {
// Graphite metric as annotation
if (annotation.target) {
@ -84,7 +95,7 @@ function (angular, _, $, config, kbn, moment) {
list.push({
annotation: annotation,
time: datapoint[1] * 1000,
time: datapoint[1],
title: target.target
});
}

View File

@ -18,7 +18,7 @@ function () {
var query = 'select ';
var seriesName = target.series;
if(!seriesName.match('^/.*/')) {
if(!seriesName.match('^/.*/') && !seriesName.match(/^merge\(.*\)/)) {
seriesName = '"' + seriesName+ '"';
}

View File

@ -44,7 +44,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
// replace grafana variables
query = query.replace('$timeFilter', timeFilter);
query = query.replace('$interval', (target.interval || options.interval));
query = query.replace(/\$interval/g, (target.interval || options.interval));
// replace templated variables
query = templateSrv.replace(query);
@ -85,8 +85,13 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
});
};
InfluxDatasource.prototype.listSeries = function() {
return this._seriesQuery('list series').then(function(data) {
InfluxDatasource.prototype.listSeries = function(query) {
// wrap in regex
if (query && query.length > 0 && query[0] !== '/') {
query = '/' + query + '/';
}
return this._seriesQuery('list series ' + query).then(function(data) {
if (!data || data.length === 0) {
return [];
}
@ -141,7 +146,6 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
InfluxDatasource.prototype._seriesQuery = function(query) {
return this._influxRequest('GET', '/series', {
q: query,
time_precision: 's',
});
};

View File

@ -61,6 +61,7 @@ function (angular) {
else if (e.which) {
code = e.which;
}
var character = String.fromCharCode(code).toLowerCase();
if (code === 188) {
@ -93,6 +94,9 @@ function (angular) {
",": "<",
".": ">",
"/": "?",
"»": "?",
"«": "?",
"¿": "?",
"\\": "|"
};
// Special Keys - and their codes
@ -277,4 +281,4 @@ function (angular) {
return keyboardManagerService;
}]);
});
});

View File

@ -1,14 +1,15 @@
define([
'angular',
'lodash',
'kbn'
'kbn',
'moment'
],
function (angular, _, kbn) {
'use strict';
var module = angular.module('grafana.services');
module.factory('OpenTSDBDatasource', function($q, $http) {
module.factory('OpenTSDBDatasource', function($q, $http, templateSrv) {
function OpenTSDBDatasource(datasource) {
this.type = 'opentsdb';
@ -99,7 +100,7 @@ function (angular, _, kbn) {
// TSDB returns datapoints has a hash of ts => value.
// Can't use _.pairs(invert()) because it stringifies keys/values
_.each(md.dps, function (v, k) {
dps.push([v, k]);
dps.push([v, k * 1000]);
});
return { target: metricLabel, datapoints: dps };
@ -123,12 +124,12 @@ function (angular, _, kbn) {
}
var query = {
metric: target.metric,
metric: templateSrv.replace(target.metric),
aggregator: "avg"
};
if (target.aggregator) {
query.aggregator = target.aggregator;
query.aggregator = templateSrv.replace(target.aggregator);
}
if (target.shouldComputeRate) {
@ -136,6 +137,14 @@ function (angular, _, kbn) {
query.rateOptions = {
counter: !!target.isCounter
};
if (target.counterMax && target.counterMax.length) {
query.rateOptions.counterMax = parseInt(target.counterMax);
}
if (target.counterResetValue && target.counterResetValue.length) {
query.rateOptions.resetValue = parseInt(target.counterResetValue);
}
}
if (target.shouldDownsample) {
@ -143,6 +152,11 @@ function (angular, _, kbn) {
}
query.tags = angular.copy(target.tags);
if(query.tags){
for(var key in query.tags){
query.tags[key] = templateSrv.replace(query.tags[key]);
}
}
return query;
}

View File

@ -1,85 +0,0 @@
define([
'angular',
'lodash'
],
function (angular, _) {
'use strict';
var module = angular.module('grafana.services');
module.service('panelMoveSrv', function($rootScope) {
function PanelMoveSrv(dashboard) {
this.dashboard = dashboard;
_.bindAll(this, 'onStart', 'onOver', 'onOut', 'onDrop', 'onStop', 'cleanup');
}
var p = PanelMoveSrv.prototype;
/* each of these can take event,ui,data parameters */
p.onStart = function() {
this.dashboard.$$panelDragging = true;
$rootScope.$apply();
};
p.onOver = function() {
$rootScope.$apply();
};
p.onOut = function() {
$rootScope.$apply();
};
/*
Use our own drop logic. the $parent.$parent this is ugly.
*/
p.onDrop = function(event,ui,data) {
var
dragRow = data.draggableScope.$parent.$parent.row.panels,
dropRow = data.droppableScope.$parent.$parent.row.panels,
dragIndex = data.dragSettings.index,
dropIndex = data.dropSettings.index;
// Remove panel from source row
dragRow.splice(dragIndex,1);
// Add to destination row
if (!_.isUndefined(dropRow)) {
dropRow.splice(dropIndex,0,data.dragItem);
}
this.dashboard.$$panelDragging = false;
// Cleanup nulls/undefined left behind
this.cleanup();
$rootScope.$apply();
$rootScope.$broadcast('render');
};
p.onStop = function() {
this.dashboard.$$panelDragging = false;
this.cleanup();
$rootScope.$apply();
};
p.cleanup = function () {
_.each(this.dashboard.rows, function(row) {
row.panels = _.without(row.panels,{});
row.panels = _.compact(row.panels);
});
};
return {
init: function(dashboard, scope) {
var panelMove = new PanelMoveSrv(dashboard);
scope.panelMoveDrop = panelMove.onDrop;
scope.panelMoveStart = panelMove.onStart;
scope.panelMoveStop = panelMove.onStop;
scope.panelMoveOver = panelMove.onOver;
scope.panelMoveOut = panelMove.onOut;
}
};
});
});

View File

@ -11,44 +11,10 @@ function (angular, _) {
this.init = function($scope) {
if (!$scope.panel.span) { $scope.panel.span = 12; }
var menu = [
{
text: "view",
icon: "icon-eye-open",
click: 'toggleFullscreen(false)',
condition: $scope.panelMeta.fullscreenView
},
{
text: 'edit',
icon: 'icon-cogs',
click: 'editPanel()',
condition: true,
},
{
text: 'duplicate',
icon: 'icon-copy',
click: 'duplicatePanel(panel)',
condition: true
},
{
text: 'json',
icon: 'icon-code',
click: 'editPanelJson()',
condition: true
},
{
text: 'share',
icon: 'icon-share',
click: 'sharePanel()',
condition: true
},
];
$scope.inspector = {};
$scope.panelMeta.menu = _.where(menu, { condition: true });
$scope.editPanel = function() {
if ($scope.panelMeta.fullscreenEdit) {
if ($scope.panelMeta.fullscreen) {
$scope.toggleFullscreen(true);
}
else {
@ -67,6 +33,10 @@ function (angular, _) {
$scope.appEvent('show-json-editor', { object: $scope.panel, updateHandler: $scope.replacePanel });
};
$scope.duplicatePanel = function() {
$scope.dashboard.duplicatePanel($scope.panel, $scope.row);
};
$scope.updateColumnSpan = function(span) {
$scope.panel.span = Math.min(Math.max($scope.panel.span + span, 1), 12);
@ -99,6 +69,14 @@ function (angular, _) {
$scope.get_data();
};
$scope.toggleEditorHelp = function(index) {
if ($scope.editorHelpIndex === index) {
$scope.editorHelpIndex = null;
return;
}
$scope.editorHelpIndex = index;
};
$scope.toggleFullscreen = function(edit) {
$scope.dashboardViewState.update({ fullscreen: true, edit: edit, panelId: $scope.panel.id });
};
@ -110,9 +88,6 @@ function (angular, _) {
// Post init phase
$scope.fullscreen = false;
$scope.editor = { index: 1 };
if ($scope.panelMeta.fullEditorTabs) {
$scope.editorTabs = _.pluck($scope.panelMeta.fullEditorTabs, 'title');
}
$scope.datasources = datasourceSrv.getMetricSources();
$scope.setDatasource($scope.panel.datasource);

View File

@ -0,0 +1,46 @@
define([
'angular',
'lodash',
],
function (angular, _) {
'use strict';
var module = angular.module('grafana.services');
module.service('popoverSrv', function($templateCache, $timeout, $q, $http, $compile) {
this.getTemplate = function(url) {
return $q.when($templateCache.get(url) || $http.get(url, {cache: true}));
};
this.show = function(options) {
var popover = options.element.data('popover');
if (popover) {
popover.scope.$destroy();
popover.destroy();
return;
}
this.getTemplate(options.templateUrl).then(function(result) {
var template = _.isString(result) ? result : result.data;
options.element.popover({
content: template,
placement: 'bottom',
html: true
});
popover = options.element.data('popover');
popover.hasContent = function () {
return template;
};
popover.toggle();
popover.scope = options.scope;
$compile(popover.$tip)(popover.scope);
});
};
});
});

View File

@ -30,6 +30,7 @@ function (angular, _, kbn) {
var option = _.findWhere(variable.options, { text: urlValue });
option = option || { text: urlValue, value: urlValue };
this.setVariableValue(variable, option, true);
this.updateAutoInterval(variable);
}
else if (variable.refresh) {
this.updateOptions(variable);

View File

@ -95,6 +95,18 @@ define([
$timeout(this.refreshDashboard, 0);
};
this.timeRangeForUrl = function() {
var range = this.timeRange(false);
if (_.isString(range.to) && range.to.indexOf('now')) {
range = this.timeRange();
}
if (_.isDate(range.from)) { range.from = range.from.getTime(); }
if (_.isDate(range.to)) { range.to = range.to.getTime(); }
return range;
};
this.timeRange = function(parse) {
var _t = this.time;
if(_.isUndefined(_t) || _.isUndefined(_t.from)) {

View File

@ -73,7 +73,7 @@ define(['settings'], function(Settings) {
// specify the limit for dashboard search results
search: {
max_results: 20
max_results: 100
},
// default home dashboard

View File

@ -7,10 +7,17 @@
@import "search.less";
@import "panel.less";
@import "forms.less";
@import "singlestat.less";
.row-control-inner {
padding:0px;
margin:0px;
position:relative;
}
.hide-controls {
padding: 0;
.row-control-inner {
.row-tab {
display: none;
}
.submenu-controls {
@ -110,7 +117,7 @@
position: fixed;
left: 0px;
right: 0px;
top: 54px;
top: 51px;
height: 100%;
padding: 0 10px;
background: @grafanaPanelBackground;
@ -128,8 +135,11 @@
.dashboard-fullscreen {
.main-view-container {
height: 0;
overflow: hidden;
height: 0;
.row-control-inner {
display: none;
}
}
}
@ -546,3 +556,13 @@ select.grafana-target-segment-input {
.grafana-tip {
padding-left: 5px;
}
.shortcut-table {
td { padding: 3px; }
th:last-child { text-align: left; }
td:first-child { text-align: right; }
}
.confirm-modal {
max-width: 500px;
}

View File

@ -18,12 +18,12 @@
top: 1px;
}
.graph-legend-series,
.graph-legend-icon,
.graph-legend-alias,
.graph-legend-value {
float: left;
white-space: nowrap;
font-size: 85%;
text-align: left;
&.current:before {
content: "Current: "
@ -43,6 +43,8 @@
}
.graph-legend-series {
float: left;
white-space: nowrap;
padding-left: 10px;
padding-top: 6px;
}
@ -53,6 +55,8 @@
.graph-legend-table {
display: table;
width: 100%;
margin: 0;
.graph-legend-series {
display: table-row;
@ -60,35 +64,60 @@
padding-left: 0;
&.pull-right {
float: none;
.graph-legend-alias::after {
content: 'y\00B2';
}
}
td, .graph-legend-alias, .graph-legend-icon, .graph-legend-value {
float: none;
display: table-cell;
white-space: nowrap;
padding: 2px 10px;
text-align: right;
border-bottom: 1px solid @grafanaListBorderBottom;
}
.graph-legend-icon {
width: 5px;
padding: 0;
top: 0;
.icon-minus {
position: relative;
top: 2px;
}
}
.graph-legend-value {
padding-left: 15px;
}
.graph-legend-alias {
padding-left: 7px;
text-align: left;
width: 95%;
}
.graph-legend-series:nth-child(odd) {
background-color: @grafanaListAccent;
}
.graph-legend-value {
&.current, &.max, &.min, &.total, &.avg {
&:before {
content: '';
}
}
}
.graph-legend-alias {
float: none;
display: table-cell;
th {
text-align: right;
padding: 5px 10px;
font-weight: bold;
color: @blue;
font-size: 85%;
white-space: nowrap;
}
.graph-legend-icon {
display: table-cell;
float: none;
white-space: nowrap;
padding: 0 4px;
top: 2px;
}
.graph-legend-value {
float: none;
display: table-cell;
white-space: nowrap;
padding-left: 15px;
}
}
.graph-legend-rightside {
&.graph-wrapper {
@ -106,7 +135,8 @@
display: table-cell;
vertical-align: top;
position: relative;
left: -4px;
left: 4px;
top: -20px;
}
.graph-legend {
@ -169,6 +199,8 @@
}
.graph-tooltip {
white-space: nowrap;
.graph-tooltip-time {
text-align: center;
font-weight: bold;
@ -176,9 +208,18 @@
top: -3px;
}
.graph-tooltip-list-item {
display: table-row;
}
.graph-tooltip-series-name {
display: table-cell;
}
.graph-tooltip-value {
display: table-cell;
font-weight: bold;
float: right;
padding-left: 10px;
text-align: right;
}
}

View File

@ -231,10 +231,6 @@ form input.ng-invalid {
z-index: 9999;
}
.dragInProgress .panel-container {
border: 3px solid rgba(100,100,100,0.50);
}
.link {
color: @linkColor;
cursor: pointer;

View File

@ -2,6 +2,7 @@
display: inline-block;
float: left;
vertical-align: top;
position: relative;
}
.panel-container {
@ -17,6 +18,7 @@
.panel-title-container {
min-height: 5px;
padding-top: 4px;
cursor: context-menu;
}
@ -24,8 +26,16 @@
border: 0px;
font-weight: bold;
position: relative;
font-size: 0.9em;
cursor: context-menu;
&.has-panel-links {
.panel-title-text:after {
content: "\f0c1";
font-family:'FontAwesome';
font-size: 80%;
padding-left: 10px;
}
}
}
.panel-loading {
@ -39,7 +49,6 @@
text-align: center;
}
.panel-error {
color: @white;
position: absolute;
@ -93,8 +102,25 @@
border: none;
}
}
.dropdown-menu {
text-align: left;
}
}
.panel-highlight {
.box-shadow(~"inset 0 1px 1px rgba(0,0,0,.075), 0 0 5px rgba(82,168,236, 0.8)");
}
.on-drag-hover {
.panel-container {
.box-shadow(~"inset 0 1px 1px rgba(0,0,0,.075), 0 0 5px rgba(82,168,236, 0.8)");
}
}
.panel-drop-zone {
display: none;
.panel-container {
border: 1px solid @grayDark;
}
}

View File

@ -0,0 +1,51 @@
.singlestat-panel {
position: relative;
display: table;
width: 100%;
}
.singlestat-panel-value-container {
padding: 20px;
display: table-cell;
vertical-align: middle;
text-align: center;
position: relative;
z-index: 1;
font-size: 3em;
font-weight: bold;
}
.singlestat-panel-prefix {
padding-right: 20px;
}
.singlestat-panel-table {
width: 100%;
td {
padding: 5px 10px;
white-space: nowrap;
text-align: right;
border-bottom: 1px solid @grafanaListBorderBottom;
}
th {
text-align: right;
padding: 5px 10px;
font-weight: bold;
color: @blue
}
td:first-child {
text-align: left;
}
tr:nth-child(odd) td {
background-color: @grafanaListAccent;
}
tr:last-child td {
border: none;
}
}

View File

@ -1,6 +1,6 @@
.submenu-controls-visible:not(.hide-controls) {
.panel-fullscreen {
top: 91px;
top: 88px;
}
}

View File

@ -0,0 +1,10 @@
<div>
<div class="row-fluid">
<div class="span4">
<label class="small">Mode</label> <select class="input-medium" ng-model="panel.mode" ng-options="f for f in ['html','markdown','text']"></select>
</div>
<div class="span2" ng-show="panel.mode == 'text'">
<label class="small">Font Size</label> <select class="input-mini" ng-model="panel.style['font-size']" ng-options="f for f in ['6pt','7pt','8pt','10pt','12pt','14pt','16pt','18pt','20pt','24pt','28pt','32pt','36pt','42pt','48pt','52pt','60pt','72pt']"></select>
</div>
</div>
</div>

View File

@ -0,0 +1,3 @@
<div ng-controller='CustomPanelCtrl'>
<h2>Custom panel</h2>
</div>

View File

@ -0,0 +1,31 @@
define([
'angular',
'app',
'lodash',
'require',
],
function (angular, app, _) {
'use strict';
var module = angular.module('grafana.panels.custom', []);
app.useModule(module);
module.controller('CustomPanelCtrl', function($scope, panelSrv) {
$scope.panelMeta = {
description : "Example plugin panel",
};
// set and populate defaults
var _d = {
};
_.defaults($scope.panel, _d);
$scope.init = function() {
panelSrv.init($scope);
};
$scope.init();
});
});

View File

@ -19,7 +19,7 @@ function (angular, _, kbn) {
this.url = datasource.url;
}
CustomDatasource.prototype.query = function(filterSrv, options) {
CustomDatasource.prototype.query = function(options) {
// get from & to in seconds
var from = kbn.parseDate(options.range.from).getTime() / 1000;
var to = kbn.parseDate(options.range.to).getTime() / 1000;

View File

@ -30,7 +30,7 @@ define([
viewState.update({fullscreen: false});
expect(location.search()).to.eql({});
expect(viewState.fullscreen).to.be(false);
expect(viewState.state.fullscreen).to.be(false);
expect(viewState.state.fullscreen).to.be(null);
});
});

View File

@ -79,7 +79,7 @@ define([
var func = gfunc.createFuncInstance('summarize', { withDefaultParams: true });
func.updateParam('1h', 0);
expect(func.params[0]).to.be('1h');
expect(func.text).to.be('summarize(1h, sum)');
expect(func.text).to.be('summarize(1h, sum, false)');
});
it('should parse numbers as float', function() {

View File

@ -27,15 +27,20 @@ define([
ctx.scope.$digest();
});
it('should build legend model', function() {
expect(ctx.scope.legend[0].alias).to.be('test.cpu1');
expect(ctx.scope.legend[1].alias).to.be('test.cpu2');
});
it('should send time series to render', function() {
var data = ctx.scope.render.getCall(0).args[0];
expect(data.length).to.be(2);
});
describe('get_data failure following success', function() {
beforeEach(function() {
ctx.datasource.query = sinon.stub().returns(ctx.$q.reject('Datasource Error'));
ctx.scope.get_data();
ctx.scope.$digest();
});
});
});
});

View File

@ -3,7 +3,7 @@ define([
'angular',
'jquery',
'components/timeSeries',
'directives/grafanaGraph'
'panels/graph/graph'
], function(helpers, angular, $, TimeSeries) {
'use strict';
@ -47,11 +47,11 @@ define([
ctx.data = [];
ctx.data.push(new TimeSeries({
datapoints: [[1,1],[2,2]],
info: { alias: 'series1', enable: true }
alias: 'series1'
}));
ctx.data.push(new TimeSeries({
datapoints: [[1,1],[2,2]],
info: { alias: 'series2', enable: true }
alias: 'series2'
}));
setupFunc(scope, ctx.data);
@ -126,6 +126,20 @@ define([
});
});
graphScenario('should use timeStep for barWidth', function(ctx) {
ctx.setup(function(scope, data) {
scope.panel.bars = true;
data[0] = new TimeSeries({
datapoints: [[1,10],[2,20]],
alias: 'series1',
});
});
it('should set barWidth', function() {
expect(ctx.plotOptions.series.bars.barWidth).to.be(10/1.5);
});
});
graphScenario('series option overrides, fill & points', function(ctx) {
ctx.setup(function(scope, data) {
scope.panel.lines = true;
@ -134,7 +148,7 @@ define([
{ alias: 'test', fill: 0, points: true }
];
data[1].info.alias = 'test';
data[1].alias = 'test';
});
it('should match second series and fill zero, and enable points', function() {
@ -150,8 +164,8 @@ define([
});
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');
expect(ctx.plotData[0].alias).to.be('series2');
expect(ctx.plotData[1].alias).to.be('series1');
});
});
@ -161,7 +175,7 @@ define([
});
it('should remove datapoints and disable stack', function() {
expect(ctx.plotData[0].info.alias).to.be('series1');
expect(ctx.plotData[0].alias).to.be('series1');
expect(ctx.plotData[1].data.length).to.be(0);
expect(ctx.plotData[1].stack).to.be(false);
});

View File

@ -1,55 +1,93 @@
define([
'jquery',
'directives/grafanaGraph.tooltip'
], function($, tooltip) {
'panels/graph/graph.tooltip'
], function($, GraphTooltip) {
'use strict';
describe('graph tooltip', function() {
var elem = $('<div></div>');
var dashboard = {
formatDate: sinon.stub().returns('date'),
};
var scope = {
appEvent: sinon.spy(),
onAppEvent: sinon.spy(),
panel: {
tooltip: {
shared: true
},
y_formats: ['ms', 'none'],
stack: true
}
};
var scope = {
appEvent: sinon.spy(),
onAppEvent: sinon.spy(),
};
var data = [
{
data: [[10,10], [12,20]],
info: { yaxis: 1 },
yaxis: { tickDecimals: 2 },
var elem = $('<div></div>');
var dashboard = { };
function describeSharedTooltip(desc, fn) {
var ctx = {};
ctx.scope = scope;
ctx.scope.panel = {
tooltip: {
shared: true
},
{
data: [[10,10], [12,20]],
info: { yaxis: 1 },
yaxis: { tickDecimals: 2 },
}
];
var plot = {
getData: sinon.stub().returns(data),
highlight: sinon.stub(),
unhighlight: sinon.stub()
stack: false
};
elem.data('plot', plot);
ctx.setup = function(setupFn) {
ctx.setupFn = setupFn;
};
beforeEach(function() {
tooltip.register(elem, dashboard, scope);
elem.trigger('plothover', [{}, {x: 13}, {}]);
describe(desc, function() {
beforeEach(function() {
ctx.setupFn();
var tooltip = new GraphTooltip(elem, dashboard, scope);
ctx.results = tooltip.getMultiSeriesPlotHoverInfo(ctx.data, ctx.pos);
});
fn(ctx);
});
}
describeSharedTooltip("steppedLine false, stack false", function(ctx) {
ctx.setup(function() {
ctx.data = [
{ data: [[10, 15], [12, 20]], },
{ data: [[10, 2], [12, 3]], }
];
ctx.pos = { x: 11 };
});
it('should add tooltip', function() {
var tooltipHtml = $(".graph-tooltip").text();
expect(tooltipHtml).to.be('date : 40.00 ms : 20.00 ms');
it('should return 2 series', function() {
expect(ctx.results.length).to.be(2);
});
it('should add time to results array', function() {
expect(ctx.results.time).to.be(10);
});
it('should set value and hoverIndex', function() {
expect(ctx.results[0].value).to.be(15);
expect(ctx.results[1].value).to.be(2);
expect(ctx.results[0].hoverIndex).to.be(0);
});
});
describeSharedTooltip("steppedLine false, stack true, individual false", function(ctx) {
ctx.setup(function() {
ctx.data = [
{ data: [[10, 15], [12, 20]], },
{ data: [[10, 2], [12, 3]], }
];
ctx.scope.panel.stack = true;
ctx.pos = { x: 11 };
});
it('should show stacked value', function() {
expect(ctx.results[1].value).to.be(17);
});
});
describeSharedTooltip("steppedLine false, stack true, individual true", function(ctx) {
ctx.setup(function() {
ctx.data = [
{ data: [[10, 15], [12, 20]], },
{ data: [[10, 2], [12, 3]], }
];
ctx.scope.panel.stack = true;
ctx.scope.panel.tooltip.value_type = 'individual';
ctx.pos = { x: 11 };
});
it('should not show stacked value', function() {
expect(ctx.results[1].value).to.be(2);
});
});

View File

@ -21,7 +21,7 @@ define([
maxDataPoints: 500,
};
var response = [{ target: 'prod1.count', points: [[10, 1], [12,1]], }];
var response = [{ target: 'prod1.count', datapoints: [[10, 1], [12,1]], }];
var results;
var request;

View File

@ -44,6 +44,35 @@ define([
});
describe('merge function detection', function() {
it('should not quote wrap regex merged series', function() {
var builder = new InfluxQueryBuilder({
series: 'merge(/^google.test/)',
column: 'value',
function: 'mean'
});
var query = builder.build();
expect(query).to.be('select mean(value) from merge(/^google.test/) where $timeFilter ' +
'group by time($interval) order asc');
});
it('should quote wrap series names that start with "merge"', function() {
var builder = new InfluxQueryBuilder({
series: 'merge.google.test',
column: 'value',
function: 'mean'
});
var query = builder.build();
expect(query).to.be('select mean(value) from "merge.google.test" where $timeFilter ' +
'group by time($interval) order asc');
});
});
});
});

View File

@ -17,7 +17,7 @@ define([
describe('When querying influxdb with one target using query editor target spec', function() {
var results;
var urlExpected = "/series?p=mupp&q=select+mean(value)+from+%22test%22"+
"+where+time+%3E+now()+-+1h+group+by+time(1s)+order+asc&time_precision=s";
"+where+time+%3E+now()+-+1h+group+by+time(1s)+order+asc";
var query = {
range: { from: 'now-1h', to: 'now' },
targets: [{ series: 'test', column: 'value', function: 'mean' }],
@ -50,7 +50,7 @@ define([
describe('When querying influxdb with one raw query', function() {
var results;
var urlExpected = "/series?p=mupp&q=select+value+from+series"+
"+where+time+%3E+now()+-+1h&time_precision=s";
"+where+time+%3E+now()+-+1h";
var query = {
range: { from: 'now-1h', to: 'now' },
targets: [{ query: "select value from series where $timeFilter", rawQuery: true }]
@ -73,7 +73,7 @@ define([
describe('When issuing annotation query', function() {
var results;
var urlExpected = "/series?p=mupp&q=select+title+from+events.backend_01"+
"+where+time+%3E+now()+-+1h&time_precision=s";
"+where+time+%3E+now()+-+1h";
var range = { from: 'now-1h', to: 'now' };
var annotation = { query: 'select title from events.$server where $timeFilter' };

View File

@ -18,7 +18,7 @@ define([
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)');
expect(ctx.scope.overrideMenu[1].submenu[1].click).to.be('menuItemSelected(1,1)');
});
});

View File

@ -7,6 +7,12 @@ define([
describe('SharePanelCtrl', function() {
var ctx = new helpers.ControllerTestContext();
function setTime(range) {
ctx.timeSrv.timeRangeForUrl = sinon.stub().returns(range);
}
setTime({ from: 'now-1h', to: 'now' });
beforeEach(module('grafana.controllers'));
beforeEach(ctx.providePhase());
@ -14,10 +20,12 @@ define([
describe('shareUrl with current time range and panel', function() {
it('should generate share url relative time', function() {
ctx.$location.path('/test');
ctx.scope.panel = { id: 22 };
ctx.timeSrv.time = { from: 'now-1h', to: 'now' };
setTime({ from: 'now-1h', to: 'now' });
ctx.scope.buildUrl();
expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=now-1h&to=now&panelId=22&fullscreen');
@ -26,26 +34,17 @@ define([
it('should generate share url absolute time', function() {
ctx.$location.path('/test');
ctx.scope.panel = { id: 22 };
ctx.timeSrv.time = { from: new Date(1362178800000), to: new Date(1396648800000) };
setTime({ from: 1362178800000, to: 1396648800000 });
ctx.scope.buildUrl();
expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=1362178800000&to=1396648800000&panelId=22&fullscreen');
});
it('should generate share url with time as JSON strings', function() {
ctx.$location.path('/test');
ctx.scope.panel = { id: 22 };
ctx.timeSrv.time = { from: "2012-01-31T23:00:00.000Z", to: "2014-04-04T22:00:00.000Z" };
ctx.scope.buildUrl();
expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=1328050800000&to=1396648800000&panelId=22&fullscreen');
});
it('should remove panel id when toPanel is false', function() {
ctx.$location.path('/test');
ctx.scope.panel = { id: 22 };
ctx.scope.toPanel = false;
ctx.timeSrv.time = { from: 'now-1h', to: 'now' };
setTime({ from: 'now-1h', to: 'now' });
ctx.scope.buildUrl();
expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=now-1h&to=now');
@ -57,7 +56,7 @@ define([
ctx.scope.includeTemplateVars = true;
ctx.scope.toPanel = false;
ctx.templateSrv.variables = [{ name: 'app', current: {text: 'mupp' }}, {name: 'server', current: {text: 'srv-01'}}];
ctx.timeSrv.time = { from: 'now-1h', to: 'now' };
setTime({ from: 'now-1h', to: 'now' });
ctx.scope.buildUrl();
expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=now-1h&to=now&var-app=mupp&var-server=srv-01');

View File

@ -7,7 +7,7 @@ define([
var points, series;
var yAxisFormats = ['short', 'ms'];
var testData = {
info: { alias: 'test' },
alias: 'test',
datapoints: [
[1,2],[null,3],[10,4],[8,5]
]
@ -26,6 +26,15 @@ define([
expect(points.length).to.be(4);
expect(points[1][1]).to.be(0);
});
it('if last is null current should pick next to last', function() {
series = new TimeSeries({
datapoints: [[10,1], [null, 2]]
});
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.current).to.be(10);
});
});
describe('series overrides', function() {
@ -36,7 +45,7 @@ define([
describe('fill & points', function() {
beforeEach(function() {
series.info.alias = 'test';
series.alias = 'test';
series.applySeriesOverrides([{ alias: 'test', fill: 0, points: true }]);
});
@ -48,7 +57,7 @@ define([
describe('series option overrides, bars, true & lines false', function() {
beforeEach(function() {
series.info.alias = 'test';
series.alias = 'test';
series.applySeriesOverrides([{ alias: 'test', bars: true, lines: false }]);
});
@ -60,7 +69,7 @@ define([
describe('series option overrides, linewidth, stack', function() {
beforeEach(function() {
series.info.alias = 'test';
series.alias = 'test';
series.applySeriesOverrides([{ alias: 'test', linewidth: 5, stack: false }]);
});
@ -70,9 +79,20 @@ define([
});
});
describe('series option overrides, fill below to', function() {
beforeEach(function() {
series.alias = 'test';
series.applySeriesOverrides([{ alias: 'test', fillBelowTo: 'min' }]);
});
it('should disable line fill and add fillBelowTo', function() {
expect(series.fillBelowTo).to.be('min');
});
});
describe('series option overrides, pointradius, steppedLine', function() {
beforeEach(function() {
series.info.alias = 'test';
series.alias = 'test';
series.applySeriesOverrides([{ alias: 'test', pointradius: 5, steppedLine: true }]);
});
@ -84,7 +104,7 @@ define([
describe('override match on regex', function() {
beforeEach(function() {
series.info.alias = 'test_01';
series.alias = 'test_01';
series.applySeriesOverrides([{ alias: '/.*01/', lines: false }]);
});
@ -95,12 +115,12 @@ define([
describe('override series y-axis, and z-index', function() {
beforeEach(function() {
series.info.alias = 'test';
series.alias = 'test';
series.applySeriesOverrides([{ alias: 'test', yaxis: 2, zindex: 2 }]);
});
it('should set yaxis', function() {
expect(series.info.yaxis).to.be(2);
expect(series.yaxis).to.be(2);
});
it('should set zindex', function() {

View File

@ -32,8 +32,6 @@ require.config({
bootstrap: '../vendor/bootstrap/bootstrap',
'bootstrap-tagsinput': '../vendor/tagsinput/bootstrap-tagsinput',
'jquery-ui': '../vendor/jquery/jquery-ui-1.10.3',
'extend-jquery': 'components/extend-jquery',
'jquery.flot': '../vendor/jquery/jquery.flot',
@ -44,6 +42,7 @@ require.config({
'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
'jquery.flot.time': '../vendor/jquery/jquery.flot.time',
'jquery.flot.crosshair': '../vendor/jquery/jquery.flot.crosshair',
'jquery.flot.fillbelow': '../vendor/jquery/jquery.flot.fillbelow',
modernizr: '../vendor/modernizr-2.6.1',
},
@ -70,7 +69,6 @@ require.config({
exports: 'Crypto'
},
'jquery-ui': ['jquery'],
'jquery.flot': ['jquery'],
'jquery.flot.pie': ['jquery', 'jquery.flot'],
'jquery.flot.events': ['jquery', 'jquery.flot'],
@ -79,10 +77,11 @@ require.config({
'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
'jquery.flot.time': ['jquery', 'jquery.flot'],
'jquery.flot.crosshair':['jquery', 'jquery.flot'],
'jquery.flot.fillbelow':['jquery', 'jquery.flot'],
'angular-route': ['angular'],
'angular-cookies': ['angular'],
'angular-dragdrop': ['jquery','jquery-ui','angular'],
'angular-dragdrop': ['jquery', 'angular'],
'angular-loader': ['angular'],
'angular-mocks': ['angular'],
'angular-resource': ['angular'],
@ -130,7 +129,7 @@ require([
'specs/influxQueryBuilder-specs',
'specs/influxdb-datasource-specs',
'specs/graph-ctrl-specs',
'specs/grafanaGraph-specs',
'specs/graph-specs',
'specs/graph-tooltip-specs',
'specs/seriesOverridesCtrl-specs',
'specs/sharePanelCtrl-specs',

Some files were not shown because too many files have changed in this diff Show More