mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' of github.com:torkelo/grafana-private into pro
Conflicts: src/app/components/require.config.js
This commit is contained in:
commit
e5219af481
28
CHANGELOG.md
28
CHANGELOG.md
@ -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)
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
|
45
src/app/components/panelmeta.js
Normal file
45
src/app/components/panelmeta.js
Normal 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;
|
||||
|
||||
});
|
@ -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'],
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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');
|
||||
};
|
||||
|
||||
});
|
||||
|
@ -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']);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
});
|
||||
};
|
||||
});
|
||||
|
@ -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('&') ;
|
||||
|
@ -101,7 +101,6 @@
|
||||
"legend_counts": true,
|
||||
"timezone": "browser",
|
||||
"percentage": false,
|
||||
"zerofill": true,
|
||||
"nullPointMode": "connected",
|
||||
"steppedLine": false,
|
||||
"tooltip": {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
95
src/app/dashboards/scripted_gen_and_save.js
Normal file
95
src/app/dashboards/scripted_gen_and_save.js
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
@ -65,7 +65,6 @@
|
||||
"avg": false
|
||||
},
|
||||
"percentage": false,
|
||||
"zerofill": true,
|
||||
"nullPointMode": "connected",
|
||||
"steppedLine": false,
|
||||
"tooltip": {
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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 () {});
|
||||
|
105
src/app/directives/dropdown.typeahead.js
Normal file
105
src/app/directives/dropdown.typeahead.js
Normal 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);
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
@ -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
|
||||
};
|
||||
});
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -32,7 +32,11 @@ function (angular) {
|
||||
};
|
||||
|
||||
input.spectrum(options);
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
input.spectrum('destroy');
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
3
src/app/features/all.js
Normal file
3
src/app/features/all.js
Normal file
@ -0,0 +1,3 @@
|
||||
define([
|
||||
'./panellinkeditor/module',
|
||||
], function () {});
|
39
src/app/features/panellinkeditor/linkSrv.js
Normal file
39
src/app/features/panellinkeditor/linkSrv.js
Normal 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;
|
||||
};
|
||||
|
||||
});
|
||||
});
|
49
src/app/features/panellinkeditor/module.html
Normal file
49
src/app/features/panellinkeditor/module.html
Normal 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>
|
51
src/app/features/panellinkeditor/module.js
Normal file
51
src/app/features/panellinkeditor/module.js
Normal 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);
|
||||
};
|
||||
|
||||
});
|
||||
});
|
@ -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;
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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() {
|
182
src/app/panels/graph/graph.tooltip.js
Normal file
182
src/app/panels/graph/graph.tooltip.js
Normal 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;
|
||||
});
|
@ -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>
|
162
src/app/panels/graph/legend.js
Normal file
162
src/app/panels/graph/legend.js
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
26
src/app/panels/graph/legend.popover.html
Normal file
26
src/app/panels/graph/legend.popover.html
Normal 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();"> </i>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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]);
|
||||
|
@ -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>
|
||||
|
76
src/app/panels/singlestat/editor.html
Normal file
76
src/app/panels/singlestat/editor.html
Normal 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>
|
||||
|
26
src/app/panels/singlestat/module.html
Normal file
26
src/app/panels/singlestat/module.html
Normal 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>
|
197
src/app/panels/singlestat/module.js
Normal file
197
src/app/panels/singlestat/module.js
Normal 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();
|
||||
});
|
||||
});
|
204
src/app/panels/singlestat/singleStatPanel.js
Normal file
204
src/app/panels/singlestat/singleStatPanel.js
Normal 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);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
@ -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();
|
||||
|
@ -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 -->
|
||||
|
@ -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 });
|
||||
|
23
src/app/partials/confirm_modal.html
Normal file
23
src/app/partials/confirm_modal.html
Normal 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>
|
||||
|
@ -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-eye-open"></i> <span>view</span></a> -->
|
||||
<!-- <a class="pointer"><i class="icon-cog"></i> <span>edit</span></a> -->
|
||||
<!-- <a class="pointer"><i class="icon-resize-horizontal"></i> <span>span</span></a> -->
|
||||
<!-- <a class="pointer"><i class="icon-copy"></i> <span>duplicate</span></a> -->
|
||||
<!-- <a class="pointer"><i class="icon-share"></i> <span>share</span></a> -->
|
||||
<!-- <a class="pointer"><i class="icon-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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
||||
|
50
src/app/partials/help_modal.html
Normal file
50
src/app/partials/help_modal.html
Normal 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>
|
@ -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()">
|
||||
|
@ -14,4 +14,5 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -12,3 +12,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<panel-link-editor panel="panel"></panel-link-editor>
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
@ -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');
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -7,9 +7,9 @@ define([
|
||||
'./templateValuesSrv',
|
||||
'./panelSrv',
|
||||
'./timer',
|
||||
'./panelMove',
|
||||
'./keyboardManager',
|
||||
'./annotationsSrv',
|
||||
'./popoverSrv',
|
||||
'./playlistSrv',
|
||||
'./unsavedChangesSrv',
|
||||
'./dashboard/dashboardKeyBindings',
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ function () {
|
||||
var query = 'select ';
|
||||
var seriesName = target.series;
|
||||
|
||||
if(!seriesName.match('^/.*/')) {
|
||||
if(!seriesName.match('^/.*/') && !seriesName.match(/^merge\(.*\)/)) {
|
||||
seriesName = '"' + seriesName+ '"';
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}]);
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -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);
|
||||
|
46
src/app/services/popoverSrv.js
Normal file
46
src/app/services/popoverSrv.js
Normal 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);
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -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);
|
||||
|
@ -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)) {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
51
src/css/less/singlestat.less
Normal file
51
src/css/less/singlestat.less
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
.submenu-controls-visible:not(.hide-controls) {
|
||||
.panel-fullscreen {
|
||||
top: 91px;
|
||||
top: 88px;
|
||||
}
|
||||
}
|
||||
|
||||
|
10
src/plugins/custom.panel.example/editor.html
Normal file
10
src/plugins/custom.panel.example/editor.html
Normal 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>
|
3
src/plugins/custom.panel.example/module.html
Normal file
3
src/plugins/custom.panel.example/module.html
Normal file
@ -0,0 +1,3 @@
|
||||
<div ng-controller='CustomPanelCtrl'>
|
||||
<h2>Custom panel</h2>
|
||||
</div>
|
31
src/plugins/custom.panel.example/module.js
Normal file
31
src/plugins/custom.panel.example/module.js
Normal 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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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' };
|
||||
|
@ -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)');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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');
|
||||
|
@ -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() {
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user