merge with master

This commit is contained in:
Torkel Ödegaard
2016-09-28 13:02:15 +02:00
164 changed files with 7241 additions and 1917 deletions

View File

@@ -41,6 +41,7 @@ import 'app/core/routes/routes';
import './filters/filters';
import coreModule from './core_module';
import appEvents from './app_events';
import colors from './utils/colors';
export {
@@ -60,4 +61,5 @@ export {
dashboardSelector,
queryPartEditorDirective,
WizardFlow,
colors,
};

View File

@@ -23,10 +23,10 @@ function (_, $, coreModule) {
getOptions: "&",
onChange: "&",
},
link: function($scope, elem, attrs) {
link: function($scope, elem) {
var $input = $(inputTemplate);
var $button = $(attrs.styleMode === 'select' ? selectTemplate : linkTemplate);
var segment = $scope.segment;
var $button = $(segment.selectMode ? selectTemplate : linkTemplate);
var options = null;
var cancelBlur = null;
var linkMode = true;
@@ -170,6 +170,7 @@ function (_, $, coreModule) {
},
link: {
pre: function postLink($scope, elem, attrs) {
var cachedOptions;
$scope.valueToSegment = function(value) {
var option = _.find($scope.options, {value: value});
@@ -177,7 +178,9 @@ function (_, $, coreModule) {
cssClass: attrs.cssClass,
custom: attrs.custom,
value: option ? option.text : value,
selectMode: attrs.selectMode,
};
return uiSegmentSrv.newSegment(segment);
};
@@ -188,13 +191,20 @@ function (_, $, coreModule) {
});
return $q.when(optionSegments);
} else {
return $scope.getOptions();
return $scope.getOptions().then(function(options) {
cachedOptions = options;
return _.map(options, function(option) {
return uiSegmentSrv.newSegment({value: option.text});
});
});
}
};
$scope.onSegmentChange = function() {
if ($scope.options) {
var option = _.find($scope.options, {text: $scope.segment.value});
var options = $scope.options || cachedOptions;
if (options) {
var option = _.find(options, {text: $scope.segment.value});
if (option && option.value !== $scope.property) {
$scope.property = option.value;
} else if (attrs.custom !== 'false') {

View File

@@ -114,6 +114,10 @@ export class BackendSrv {
var requestIsLocal = options.url.indexOf('/') === 0;
var firstAttempt = options.retry === 0;
if (requestIsLocal && !options.hasSubUrl && options.retry === 0) {
options.url = config.appSubUrl + options.url;
}
if (requestIsLocal && options.headers && options.headers.Authorization) {
options.headers['X-DS-Authorization'] = options.headers.Authorization;
delete options.headers.Authorization;

View File

@@ -28,6 +28,7 @@ function (angular, _, coreModule) {
this.type = options.type;
this.fake = options.fake;
this.value = options.value;
this.selectMode = options.selectMode;
this.type = options.type;
this.expandable = options.expandable;
this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));

View File

@@ -31,6 +31,8 @@ export default class TimeSeries {
allIsZero: boolean;
decimals: number;
scaledDecimals: number;
hasMsResolution: boolean;
isOutsideRange: boolean;
lines: any;
bars: any;
@@ -54,6 +56,7 @@ export default class TimeSeries {
this.stats = {};
this.legend = true;
this.unit = opts.unit;
this.hasMsResolution = this.isMsResolutionNeeded();
}
applySeriesOverrides(overrides) {

View File

@@ -0,0 +1,12 @@
export default [
"#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0",
"#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477",
"#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0",
"#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93",
"#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7",
"#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B",
"#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7"
];

View File

@@ -174,7 +174,10 @@ function($, _, moment) {
lowLimitMs = kbn.interval_to_ms(lowLimitInterval);
}
else {
return userInterval;
return {
intervalMs: kbn.interval_to_ms(userInterval),
interval: userInterval,
};
}
}
@@ -183,7 +186,10 @@ function($, _, moment) {
intervalMs = lowLimitMs;
}
return kbn.secondsToHms(intervalMs / 1000);
return {
intervalMs: intervalMs,
interval: kbn.secondsToHms(intervalMs / 1000),
};
};
kbn.describe_interval = function (string) {

View File

@@ -227,8 +227,8 @@ export class AlertTabCtrl {
var datasourceName = foundTarget.datasource || this.panel.datasource;
this.datasourceSrv.get(datasourceName).then(ds => {
if (ds.meta.id !== 'graphite') {
this.error = 'Currently the alerting backend only supports Graphite queries';
if (!ds.meta.alerting) {
this.error = 'The datasource does not support alerting queries';
} else if (this.templateSrv.variableExists(foundTarget.target)) {
this.error = 'Template variables are not supported in alert queries';
} else {

View File

@@ -30,6 +30,7 @@ export class DashboardModel {
snapshot: any;
schemaVersion: number;
version: number;
revision: number;
links: any;
gnetId: any;
meta: any;
@@ -42,6 +43,7 @@ export class DashboardModel {
this.events = new Emitter();
this.id = data.id || null;
this.revision = data.revision;
this.title = data.title || 'No Title';
this.autoUpdate = data.autoUpdate;
this.description = data.description;

View File

@@ -8,7 +8,7 @@ function (angular, _, require, config) {
var module = angular.module('grafana.controllers');
module.controller('ShareModalCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, $element, templateSrv, linkSrv) {
module.controller('ShareModalCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, templateSrv, linkSrv) {
$scope.options = { forCurrent: true, includeTemplateVars: true, theme: 'current' };
$scope.editor = { index: $scope.tabIndex || 0};

View File

@@ -2,7 +2,7 @@
<div ng-repeat="variable in ctrl.variables" ng-hide="variable.hide === 2" class="submenu-item gf-form-inline">
<div class="gf-form">
<label class="gf-form-label template-variable" ng-hide="variable.hide === 1">
{{variable.label || variable.name}}:
{{variable.label || variable.name}}
</label>
<value-select-dropdown ng-if="variable.type !== 'adhoc'" variable="variable" on-updated="ctrl.variableUpdated(variable)" get-values-for-tag="ctrl.getValuesForTag(variable, tagKey)"></value-select-dropdown>
</div>

View File

@@ -16,7 +16,7 @@ var template = `
Panel data source
</label>
<metric-segment segment="ctrl.dsSegment" style-mode="select"
<metric-segment segment="ctrl.dsSegment"
get-options="ctrl.getOptions()"
on-change="ctrl.datasourceChanged()"></metric-segment>
</div>
@@ -67,7 +67,7 @@ export class MetricsDsSelectorCtrl {
this.current = {name: dsValue + ' not found', value: null};
}
this.dsSegment = uiSegmentSrv.newSegment(this.current.name);
this.dsSegment = uiSegmentSrv.newSegment({value: this.current.name, selectMode: true});
}
getOptions() {

View File

@@ -25,6 +25,7 @@ class MetricsPanelCtrl extends PanelCtrl {
range: any;
rangeRaw: any;
interval: any;
intervalMs: any;
resolution: any;
timeInfo: any;
skipDataOnInit: boolean;
@@ -123,11 +124,22 @@ class MetricsPanelCtrl extends PanelCtrl {
this.resolution = Math.ceil($(window).width() * (this.panel.span / 12));
}
var panelInterval = this.panel.interval;
var datasourceInterval = (this.datasource || {}).interval;
this.interval = kbn.calculateInterval(this.range, this.resolution, panelInterval || datasourceInterval);
this.calculateInterval();
};
calculateInterval() {
var intervalOverride = this.panel.interval;
// if no panel interval check datasource
if (!intervalOverride && this.datasource && this.datasource.interval) {
intervalOverride = this.datasource.interval;
}
var res = kbn.calculateInterval(this.range, this.resolution, intervalOverride);
this.interval = res.interval;
this.intervalMs = res.intervalMs;
}
applyPanelTimeOverrides() {
this.timeInfo = '';
@@ -183,6 +195,7 @@ class MetricsPanelCtrl extends PanelCtrl {
range: this.range,
rangeRaw: this.rangeRaw,
interval: this.interval,
intervalMs: this.intervalMs,
targets: this.panel.targets,
format: this.panel.renderer === 'png' ? 'png' : 'json',
maxDataPoints: this.resolution,

View File

@@ -25,7 +25,7 @@
</div>
<div class="row">
<div class="col-md-6">
<div class="col-lg-6">
<div class="playlist-search-containerwrapper">
<div class="max-width-32">
<h5 class="page-headering playlist-column-header">Available</h5>
@@ -72,7 +72,7 @@
</div>
</div>
<div class="col-md-6">
<div class="col-lg-6">
<h5 class="page headering playlist-column-header">Selected</h5>
<table class="grafana-options-table playlist-available-list">
<tr ng-repeat="playlistItem in ctrl.playlistItems">

View File

@@ -14,7 +14,7 @@ export class PlaylistSearchCtrl {
/** @ngInject */
constructor(private $scope, private $location, private $timeout, private backendSrv, private contextSrv) {
this.query = { query: '', tag: [], starred: false };
this.query = {query: '', tag: [], starred: false, limit: 30};
$timeout(() => {
this.query.query = '';

View File

@@ -3,7 +3,9 @@
<div class="page-container">
<div class="page-header">
<h1>Plugins</h1>
<h1>
Plugins <span class="muted small">(currently installed)</span>
</h1>
<div class="page-header-tabs">
<ul class="gf-tabs">
@@ -25,7 +27,7 @@
</ul>
<a class="get-more-plugins-link" href="https://grafana.net/plugins?utm_source=grafana_plugin_list" target="_blank">
Find plugins on
Find more plugins on
</a>
</div>
</div>

View File

@@ -18,7 +18,7 @@ export class ConstantVariable implements Variable {
current: {},
};
/** @ngInject */
/** @ngInject **/
constructor(private model, private variableSrv) {
assignModelProperties(this, model, this.defaults);
}

View File

@@ -10,6 +10,7 @@ export class DatasourceVariable implements Variable {
query: string;
options: any;
current: any;
refresh: any;
defaults = {
type: 'datasource',
@@ -20,11 +21,13 @@ export class DatasourceVariable implements Variable {
regex: '',
options: [],
query: '',
refresh: 1,
};
/** @ngInject */
/** @ngInject **/
constructor(private model, private datasourceSrv, private variableSrv) {
assignModelProperties(this, model, this.defaults);
this.refresh = 1;
}
getModel() {

View File

@@ -6,7 +6,7 @@ import {variableTypes} from './variable';
export class VariableEditorCtrl {
/** @ngInject */
/** @ngInject **/
constructor(private $scope, private datasourceSrv, private variableSrv, templateSrv) {
$scope.variableTypes = variableTypes;
$scope.ctrl = {};

View File

@@ -28,7 +28,7 @@ export class IntervalVariable implements Variable {
auto_count: 30,
};
/** @ngInject */
/** @ngInject **/
constructor(private model, private timeSrv, private templateSrv, private variableSrv) {
assignModelProperties(this, model, this.defaults);
this.refresh = 2;
@@ -54,8 +54,8 @@ export class IntervalVariable implements Variable {
this.options.unshift({ text: 'auto', value: '$__auto_interval' });
}
var interval = kbn.calculateInterval(this.timeSrv.timeRange(), this.auto_count, (this.auto_min ? ">"+this.auto_min : null));
this.templateSrv.setGrafanaVariable('$__auto_interval', interval);
var res = kbn.calculateInterval(this.timeSrv.timeRange(), this.auto_count, (this.auto_min ? ">"+this.auto_min : null));
this.templateSrv.setGrafanaVariable('$__auto_interval', res.interval);
}
updateOptions() {

View File

@@ -40,6 +40,7 @@ export class QueryVariable implements Variable {
tagValuesQuery: null,
};
/** @ngInject **/
constructor(private model, private datasourceSrv, private templateSrv, private variableSrv, private $q) {
// copy model properties to this instance
assignModelProperties(this, model, this.defaults);

View File

@@ -62,6 +62,7 @@ describe('VariableSrv init', function() {
options: [{text: "test", value: "test"}]
}];
scenario.urlParams["var-apps"] = "new";
scenario.metricSources = [];
});
it('should update current value', () => {
@@ -110,6 +111,30 @@ describe('VariableSrv init', function() {
});
});
describeInitScenario('when datasource variable is initialized', scenario => {
scenario.setup(() => {
scenario.variables = [{
type: 'datasource',
query: 'graphite',
name: 'test',
current: {value: 'backend4_pee', text: 'backend4_pee'},
regex: '/pee$/'
}
];
scenario.metricSources = [
{name: 'backend1', meta: {id: 'influx'}},
{name: 'backend2_pee', meta: {id: 'graphite'}},
{name: 'backend3', meta: {id: 'graphite'}},
{name: 'backend4_pee', meta: {id: 'graphite'}},
];
});
it('should update current value', function() {
var variable = ctx.variableSrv.variables[0];
expect(variable.options.length).to.be(2);
});
});
describeInitScenario('when template variable is present in url multiple times', scenario => {
scenario.setup(() => {
scenario.variables = [{

View File

@@ -43,6 +43,10 @@ function (angular, _, kbn) {
}
};
this.variableInitialized = function(variable) {
this._index[variable.name] = variable;
};
this.getAdhocFilters = function(datasourceName) {
var variable = this._adhocVariables[datasourceName];
if (variable) {

View File

@@ -1,417 +0,0 @@
define([
'angular',
'lodash',
'jquery',
'app/core/utils/kbn',
],
function (angular, _, $, kbn) {
'use strict';
var module = angular.module('grafana.services');
module.service('templateValuesSrv', function($q, $rootScope, datasourceSrv, $location, templateSrv, timeSrv) {
var self = this;
this.variableLock = {};
function getNoneOption() { return { text: 'None', value: '', isNone: true }; }
// update time variant variables
$rootScope.onAppEvent('refresh', function() {
// look for interval variables
var intervalVariable = _.find(self.variables, { type: 'interval' });
if (intervalVariable) {
self.updateAutoInterval(intervalVariable);
}
// update variables with refresh === 2
var promises = self.variables
.filter(function(variable) {
return variable.refresh === 2;
}).map(function(variable) {
var previousOptions = variable.options.slice();
return self.updateOptions(variable).then(function () {
return self.variableUpdated(variable).then(function () {
// check if current options changed due to refresh
if (angular.toJson(previousOptions) !== angular.toJson(variable.options)) {
$rootScope.appEvent('template-variable-value-updated');
}
});
});
});
return $q.all(promises);
}, $rootScope);
this.init = function(dashboard) {
this.dashboard = dashboard;
this.variables = dashboard.templating.list;
templateSrv.init(this.variables);
var queryParams = $location.search();
var promises = [];
// use promises to delay processing variables that
// depend on other variables.
this.variableLock = {};
_.forEach(this.variables, function(variable) {
self.variableLock[variable.name] = $q.defer();
});
for (var i = 0; i < this.variables.length; i++) {
var variable = this.variables[i];
promises.push(this.processVariable(variable, queryParams));
}
return $q.all(promises);
};
this.processVariable = function(variable, queryParams) {
var dependencies = [];
var lock = self.variableLock[variable.name];
// determine our dependencies.
if (variable.type === "query") {
_.forEach(this.variables, function(v) {
// both query and datasource can contain variable
if (templateSrv.containsVariable(variable.query, v.name) ||
templateSrv.containsVariable(variable.datasource, v.name)) {
dependencies.push(self.variableLock[v.name].promise);
}
});
}
return $q.all(dependencies).then(function() {
var urlValue = queryParams['var-' + variable.name];
if (urlValue !== void 0) {
return self.setVariableFromUrl(variable, urlValue).then(lock.resolve);
}
else if (variable.refresh === 1 || variable.refresh === 2) {
return self.updateOptions(variable).then(function() {
if (_.isEmpty(variable.current) && variable.options.length) {
self.setVariableValue(variable, variable.options[0]);
}
lock.resolve();
});
}
else if (variable.type === 'interval') {
self.updateAutoInterval(variable);
lock.resolve();
} else {
lock.resolve();
}
}).finally(function() {
delete self.variableLock[variable.name];
});
};
this.setVariableFromUrl = function(variable, urlValue) {
var promise = $q.when(true);
if (variable.refresh) {
promise = this.updateOptions(variable);
}
return promise.then(function() {
var option = _.find(variable.options, function(op) {
return op.text === urlValue || op.value === urlValue;
});
option = option || { text: urlValue, value: urlValue };
self.updateAutoInterval(variable);
return self.setVariableValue(variable, option, true);
});
};
this.updateAutoInterval = function(variable) {
if (!variable.auto) { return; }
// add auto option if missing
if (variable.options.length && variable.options[0].text !== 'auto') {
variable.options.unshift({ text: 'auto', value: '$__auto_interval' });
}
var interval = kbn.calculateInterval(timeSrv.timeRange(), variable.auto_count, (variable.auto_min ? ">"+variable.auto_min : null));
templateSrv.setGrafanaVariable('$__auto_interval', interval);
};
this.setVariableValue = function(variable, option) {
variable.current = angular.copy(option);
if (_.isArray(variable.current.text)) {
variable.current.text = variable.current.text.join(' + ');
}
self.selectOptionsForCurrentValue(variable);
templateSrv.updateTemplateData();
return this.updateOptionsInChildVariables(variable);
};
this.variableUpdated = function(variable) {
templateSrv.updateTemplateData();
return self.updateOptionsInChildVariables(variable);
};
this.updateOptionsInChildVariables = function(updatedVariable) {
// if there is a variable lock ignore cascading update because we are in a boot up scenario
if (self.variableLock[updatedVariable.name]) {
return $q.when();
}
var promises = _.map(self.variables, function(otherVariable) {
if (otherVariable === updatedVariable) {
return;
}
if (templateSrv.containsVariable(otherVariable.regex, updatedVariable.name) ||
templateSrv.containsVariable(otherVariable.query, updatedVariable.name) ||
templateSrv.containsVariable(otherVariable.datasource, updatedVariable.name)) {
return self.updateOptions(otherVariable);
}
});
return $q.all(promises);
};
this._updateNonQueryVariable = function(variable) {
if (variable.type === 'datasource') {
self.updateDataSourceVariable(variable);
return;
}
if (variable.type === 'constant') {
variable.options = [{text: variable.query, value: variable.query}];
return;
}
if (variable.type === 'adhoc') {
variable.current = {};
variable.options = [];
return;
}
// extract options in comma separated string
variable.options = _.map(variable.query.split(/[,]+/), function(text) {
return { text: text.trim(), value: text.trim() };
});
if (variable.type === 'interval') {
self.updateAutoInterval(variable);
return;
}
if (variable.type === 'custom' && variable.includeAll) {
self.addAllOption(variable);
}
};
this.updateDataSourceVariable = function(variable) {
var options = [];
var sources = datasourceSrv.getMetricSources({skipVariables: true});
var regex;
if (variable.regex) {
regex = kbn.stringToJsRegex(templateSrv.replace(variable.regex));
}
for (var i = 0; i < sources.length; i++) {
var source = sources[i];
// must match on type
if (source.meta.id !== variable.query) {
continue;
}
if (regex && !regex.exec(source.name)) {
continue;
}
options.push({text: source.name, value: source.name});
}
if (options.length === 0) {
options.push({text: 'No data sources found', value: ''});
}
variable.options = options;
};
this.updateOptions = function(variable) {
if (variable.type !== 'query') {
self._updateNonQueryVariable(variable);
return self.validateVariableSelectionState(variable);
}
return datasourceSrv.get(variable.datasource)
.then(_.partial(this.updateOptionsFromMetricFindQuery, variable))
.then(_.partial(this.updateTags, variable))
.then(_.partial(this.validateVariableSelectionState, variable));
};
this.selectOptionsForCurrentValue = function(variable) {
var i, y, value, option;
var selected = [];
for (i = 0; i < variable.options.length; i++) {
option = variable.options[i];
option.selected = false;
if (_.isArray(variable.current.value)) {
for (y = 0; y < variable.current.value.length; y++) {
value = variable.current.value[y];
if (option.value === value) {
option.selected = true;
selected.push(option);
}
}
} else if (option.value === variable.current.value) {
option.selected = true;
selected.push(option);
}
}
return selected;
};
this.validateVariableSelectionState = function(variable) {
if (!variable.current) {
if (!variable.options.length) { return $q.when(); }
return self.setVariableValue(variable, variable.options[0], false);
}
if (_.isArray(variable.current.value)) {
var selected = self.selectOptionsForCurrentValue(variable);
// if none pick first
if (selected.length === 0) {
selected = variable.options[0];
} else {
selected = {
value: _.map(selected, function(val) {return val.value;}),
text: _.map(selected, function(val) {return val.text;}).join(' + '),
};
}
return self.setVariableValue(variable, selected, false);
} else {
var currentOption = _.find(variable.options, {text: variable.current.text});
if (currentOption) {
return self.setVariableValue(variable, currentOption, false);
} else {
if (!variable.options.length) { return $q.when(null); }
return self.setVariableValue(variable, variable.options[0]);
}
}
};
this.updateTags = function(variable, datasource) {
if (variable.useTags) {
return datasource.metricFindQuery(variable.tagsQuery).then(function (results) {
variable.tags = [];
for (var i = 0; i < results.length; i++) {
variable.tags.push(results[i].text);
}
return datasource;
});
} else {
delete variable.tags;
}
return datasource;
};
this.updateOptionsFromMetricFindQuery = function(variable, datasource) {
return datasource.metricFindQuery(variable.query).then(function (results) {
variable.options = self.metricNamesToVariableValues(variable, results);
if (variable.includeAll) {
self.addAllOption(variable);
}
if (!variable.options.length) {
variable.options.push(getNoneOption());
}
return datasource;
});
};
this.getValuesForTag = function(variable, tagKey) {
return datasourceSrv.get(variable.datasource).then(function(datasource) {
var query = variable.tagValuesQuery.replace('$tag', tagKey);
return datasource.metricFindQuery(query).then(function (results) {
return _.map(results, function(value) {
return value.text;
});
});
});
};
this.metricNamesToVariableValues = function(variable, metricNames) {
var regex, options, i, matches;
options = [];
if (variable.regex) {
regex = kbn.stringToJsRegex(templateSrv.replace(variable.regex));
}
for (i = 0; i < metricNames.length; i++) {
var item = metricNames[i];
var value = item.value || item.text;
var text = item.text || item.value;
if (_.isNumber(value)) {
value = value.toString();
}
if (_.isNumber(text)) {
text = text.toString();
}
if (regex) {
matches = regex.exec(value);
if (!matches) { continue; }
if (matches.length > 1) {
value = matches[1];
text = value;
}
}
options.push({text: text, value: value});
}
options = _.uniq(options, 'value');
return this.sortVariableValues(options, variable.sort);
};
this.addAllOption = function(variable) {
variable.options.unshift({text: 'All', value: "$__all"});
};
this.sortVariableValues = function(options, sortOrder) {
if (sortOrder === 0) {
return options;
}
var sortType = Math.ceil(sortOrder / 2);
var reverseSort = (sortOrder % 2 === 0);
if (sortType === 1) {
options = _.sortBy(options, 'text');
} else if (sortType === 2) {
options = _.sortBy(options, function(opt) {
var matches = opt.text.match(/.*?(\d+).*/);
if (!matches) {
return 0;
} else {
return parseInt(matches[1], 10);
}
});
}
if (reverseSort) {
options = options.reverse();
}
return options;
};
});
});

View File

@@ -8,7 +8,6 @@ import {Variable, variableTypes} from './variable';
export class VariableSrv {
dashboard: any;
variables: any;
variableLock: any;
/** @ngInject */
constructor(private $rootScope, private $q, private $location, private $injector, private templateSrv) {
@@ -18,7 +17,6 @@ export class VariableSrv {
}
init(dashboard) {
this.variableLock = {};
this.dashboard = dashboard;
// create working class models representing variables
@@ -30,13 +28,15 @@ export class VariableSrv {
// init variables
for (let variable of this.variables) {
this.variableLock[variable.name] = this.$q.defer();
variable.initLock = this.$q.defer();
}
var queryParams = this.$location.search();
return this.$q.all(this.variables.map(variable => {
return this.processVariable(variable, queryParams);
}));
})).then(() => {
this.templateSrv.updateTemplateData();
});
}
onDashboardRefresh() {
@@ -59,27 +59,27 @@ export class VariableSrv {
processVariable(variable, queryParams) {
var dependencies = [];
var lock = this.variableLock[variable.name];
for (let otherVariable of this.variables) {
if (variable.dependsOn(otherVariable)) {
dependencies.push(this.variableLock[otherVariable.name].promise);
dependencies.push(otherVariable.initLock.promise);
}
}
return this.$q.all(dependencies).then(() => {
var urlValue = queryParams['var-' + variable.name];
if (urlValue !== void 0) {
return variable.setValueFromUrl(urlValue).then(lock.resolve);
return variable.setValueFromUrl(urlValue).then(variable.initLock.resolve);
}
if (variable.refresh === 1 || variable.refresh === 2) {
return variable.updateOptions().then(lock.resolve);
return variable.updateOptions().then(variable.initLock.resolve);
}
lock.resolve();
variable.initLock.resolve();
}).finally(() => {
delete this.variableLock[variable.name];
this.templateSrv.variableInitialized(variable);
delete variable.initLock;
});
}
@@ -111,7 +111,7 @@ export class VariableSrv {
variableUpdated(variable) {
// if there is a variable lock ignore cascading update because we are in a boot up scenario
if (this.variableLock[variable.name]) {
if (variable.initLock) {
return this.$q.when();
}
@@ -155,8 +155,7 @@ export class VariableSrv {
validateVariableSelectionState(variable) {
if (!variable.current) {
if (!variable.options.length) { return this.$q.when(); }
return variable.setValue(variable.options[0]);
variable.current = {};
}
if (_.isArray(variable.current.value)) {

View File

@@ -0,0 +1,287 @@
{
"revision": 2,
"title": "TestData - Alerts",
"tags": [
"grafana-test"
],
"style": "dark",
"timezone": "browser",
"editable": true,
"hideControls": false,
"sharedCrosshair": false,
"rows": [
{
"collapse": false,
"editable": true,
"height": 255.625,
"panels": [
{
"alert": {
"conditions": [
{
"evaluator": {
"params": [
60
],
"type": "gt"
},
"query": {
"params": [
"A",
"5m",
"now"
]
},
"reducer": {
"params": [],
"type": "avg"
},
"type": "query"
}
],
"enabled": true,
"frequency": "60s",
"handler": 1,
"name": "TestData - Always OK",
"noDataState": "no_data",
"notifications": []
},
"aliasColors": {},
"bars": false,
"datasource": "Grafana TestData",
"editable": true,
"error": false,
"fill": 1,
"id": 3,
"isNew": true,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 6,
"stack": false,
"steppedLine": false,
"targets": [
{
"refId": "A",
"scenario": "random_walk",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
}
],
"thresholds": [
{
"value": 60,
"op": "gt",
"fill": true,
"line": true,
"colorMode": "critical"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Always OK",
"tooltip": {
"msResolution": false,
"shared": true,
"sort": 0,
"value_type": "cumulative"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": "",
"logBase": 1,
"max": "125",
"min": "0",
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"alert": {
"conditions": [
{
"evaluator": {
"params": [
177
],
"type": "gt"
},
"query": {
"params": [
"A",
"5m",
"now"
]
},
"reducer": {
"params": [],
"type": "avg"
},
"type": "query"
}
],
"enabled": true,
"frequency": "60s",
"handler": 1,
"name": "TestData - Always Alerting",
"noDataState": "no_data",
"notifications": []
},
"aliasColors": {},
"bars": false,
"datasource": "Grafana TestData",
"editable": true,
"error": false,
"fill": 1,
"id": 4,
"isNew": true,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 6,
"stack": false,
"steppedLine": false,
"targets": [
{
"refId": "A",
"scenario": "random_walk",
"scenarioId": "csv_metric_values",
"stringInput": "200,445,100,150,200,220,190",
"target": ""
}
],
"thresholds": [
{
"colorMode": "critical",
"fill": true,
"line": true,
"op": "gt",
"value": 177
}
],
"timeFrom": null,
"timeShift": null,
"title": "Always Alerting",
"tooltip": {
"msResolution": false,
"shared": true,
"sort": 0,
"value_type": "cumulative"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": "",
"logBase": 1,
"max": null,
"min": "0",
"show": true
},
{
"format": "short",
"label": "",
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
}
],
"title": "New row"
}
],
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"templating": {
"list": []
},
"annotations": {
"list": []
},
"schemaVersion": 13,
"version": 4,
"links": [],
"gnetId": null
}

View File

@@ -0,0 +1,483 @@
{
"revision": 4,
"title": "TestData - Graph Panel Last 1h",
"tags": [
"grafana-test"
],
"style": "dark",
"timezone": "browser",
"editable": true,
"hideControls": false,
"sharedCrosshair": false,
"rows": [
{
"collapse": false,
"editable": true,
"height": "250px",
"panels": [
{
"aliasColors": {},
"bars": false,
"datasource": "Grafana TestData",
"editable": true,
"error": false,
"fill": 1,
"id": 1,
"isNew": true,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 4,
"stack": false,
"steppedLine": false,
"targets": [
{
"refId": "A",
"scenario": "random_walk",
"scenarioId": "no_data_points",
"target": ""
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "No Data Points Warning",
"tooltip": {
"msResolution": false,
"shared": true,
"sort": 0,
"value_type": "cumulative"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"aliasColors": {},
"bars": false,
"datasource": "Grafana TestData",
"editable": true,
"error": false,
"fill": 1,
"id": 2,
"isNew": true,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 4,
"stack": false,
"steppedLine": false,
"targets": [
{
"refId": "A",
"scenario": "random_walk",
"scenarioId": "datapoints_outside_range",
"target": ""
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Datapoints Outside Range Warning",
"tooltip": {
"msResolution": false,
"shared": true,
"sort": 0,
"value_type": "cumulative"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"aliasColors": {},
"bars": false,
"datasource": "Grafana TestData",
"editable": true,
"error": false,
"fill": 1,
"id": 3,
"isNew": true,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 4,
"stack": false,
"steppedLine": false,
"targets": [
{
"refId": "A",
"scenario": "random_walk",
"scenarioId": "random_walk",
"target": ""
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Random walk series",
"tooltip": {
"msResolution": false,
"shared": true,
"sort": 0,
"value_type": "cumulative"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
}
],
"title": "New row"
},
{
"collapse": false,
"editable": true,
"height": "250px",
"panels": [
{
"aliasColors": {},
"bars": false,
"datasource": "Grafana TestData",
"editable": true,
"error": false,
"fill": 1,
"id": 4,
"isNew": true,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 8,
"stack": false,
"steppedLine": false,
"targets": [
{
"refId": "A",
"scenario": "random_walk",
"scenarioId": "random_walk",
"target": ""
}
],
"thresholds": [],
"timeFrom": "2s",
"timeShift": null,
"title": "Millisecond res x-axis and tooltip",
"tooltip": {
"msResolution": false,
"shared": true,
"sort": 0,
"value_type": "cumulative"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"title": "",
"error": false,
"span": 4,
"editable": true,
"type": "text",
"isNew": true,
"id": 6,
"mode": "markdown",
"content": "Just verify that the tooltip time has millisecond resolution ",
"links": []
}
],
"title": "New row"
},
{
"title": "New row",
"height": 336,
"editable": true,
"collapse": false,
"panels": [
{
"title": "2 yaxis and axis lables",
"error": false,
"span": 7.99561403508772,
"editable": true,
"type": "graph",
"isNew": true,
"id": 5,
"targets": [
{
"target": "",
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0"
},
{
"target": "",
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "2000,3000,4000,1000,3000,10000"
}
],
"datasource": "Grafana TestData",
"renderer": "flot",
"yaxes": [
{
"label": "Perecent",
"show": true,
"logBase": 1,
"min": null,
"max": null,
"format": "percent"
},
{
"label": "Pressure",
"show": true,
"logBase": 1,
"min": null,
"max": null,
"format": "short"
}
],
"xaxis": {
"show": true,
"mode": "time",
"name": null,
"values": []
},
"lines": true,
"fill": 1,
"linewidth": 2,
"points": false,
"pointradius": 5,
"bars": false,
"stack": false,
"percentage": false,
"legend": {
"show": true,
"values": false,
"min": false,
"max": false,
"current": false,
"total": false,
"avg": false
},
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {
"value_type": "cumulative",
"shared": true,
"sort": 0,
"msResolution": false
},
"timeFrom": null,
"timeShift": null,
"aliasColors": {},
"seriesOverrides": [
{
"alias": "B-series",
"yaxis": 2
}
],
"thresholds": [],
"links": []
},
{
"title": "",
"error": false,
"span": 4.00438596491228,
"editable": true,
"type": "text",
"isNew": true,
"id": 7,
"mode": "markdown",
"content": "Verify that axis labels look ok",
"links": []
}
]
}
],
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"templating": {
"list": []
},
"annotations": {
"list": []
},
"refresh": false,
"schemaVersion": 13,
"version": 3,
"links": [],
"gnetId": null
}

View File

@@ -0,0 +1,62 @@
///<reference path="../../../../headers/common.d.ts" />
import _ from 'lodash';
import angular from 'angular';
class TestDataDatasource {
/** @ngInject */
constructor(private backendSrv, private $q) {}
query(options) {
var queries = _.filter(options.targets, item => {
return item.hide !== true;
}).map(item => {
return {
refId: item.refId,
scenarioId: item.scenarioId,
intervalMs: options.intervalMs,
maxDataPoints: options.maxDataPoints,
stringInput: item.stringInput,
jsonInput: angular.fromJson(item.jsonInput),
};
});
if (queries.length === 0) {
return this.$q.when({data: []});
}
return this.backendSrv.post('/api/tsdb/query', {
from: options.range.from.valueOf().toString(),
to: options.range.to.valueOf().toString(),
queries: queries,
}).then(res => {
var data = [];
if (res.results) {
_.forEach(res.results, queryRes => {
for (let series of queryRes.series) {
data.push({
target: series.name,
datapoints: series.points
});
}
});
}
return {data: data};
});
}
annotationQuery(options) {
return this.backendSrv.get('/api/annotations', {
from: options.range.from.valueOf(),
to: options.range.to.valueOf(),
limit: options.limit,
type: options.type,
});
}
}
export {TestDataDatasource};

View File

@@ -0,0 +1,22 @@
///<reference path="../../../../headers/common.d.ts" />
import {TestDataDatasource} from './datasource';
import {TestDataQueryCtrl} from './query_ctrl';
class TestDataAnnotationsQueryCtrl {
annotation: any;
constructor() {
}
static template = '<h2>test data</h2>';
}
export {
TestDataDatasource,
TestDataDatasource as Datasource,
TestDataQueryCtrl as QueryCtrl,
TestDataAnnotationsQueryCtrl as AnnotationsQueryCtrl,
};

View File

@@ -0,0 +1,20 @@
{
"type": "datasource",
"name": "Grafana TestDataDB",
"id": "grafana-testdata-datasource",
"metrics": true,
"alerting": true,
"annotations": true,
"info": {
"author": {
"name": "Grafana Project",
"url": "http://grafana.org"
},
"logos": {
"small": "",
"large": ""
}
}
}

View File

@@ -0,0 +1,35 @@
///<reference path="../../../../headers/common.d.ts" />
import _ from 'lodash';
import {TestDataDatasource} from './datasource';
import {QueryCtrl} from 'app/plugins/sdk';
export class TestDataQueryCtrl extends QueryCtrl {
static templateUrl = 'partials/query.editor.html';
scenarioList: any;
scenario: any;
/** @ngInject **/
constructor($scope, $injector, private backendSrv) {
super($scope, $injector);
this.target.scenarioId = this.target.scenarioId || 'random_walk';
this.scenarioList = [];
}
$onInit() {
return this.backendSrv.get('/api/tsdb/testdata/scenarios').then(res => {
this.scenarioList = res;
this.scenario = _.find(this.scenarioList, {id: this.target.scenarioId});
});
}
scenarioChanged() {
this.scenario = _.find(this.scenarioList, {id: this.target.scenarioId});
this.target.stringInput = this.scenario.stringInput;
this.refresh();
}
}

View File

@@ -0,0 +1,36 @@
///<reference path="../../../headers/common.d.ts" />
export class ConfigCtrl {
static template = '';
appEditCtrl: any;
constructor(private backendSrv) {
this.appEditCtrl.setPreUpdateHook(this.initDatasource.bind(this));
}
initDatasource() {
return this.backendSrv.get('/api/datasources').then(res => {
var found = false;
for (let ds of res) {
if (ds.type === "grafana-testdata-datasource") {
found = true;
}
}
if (!found) {
var dsInstance = {
name: 'Grafana TestData',
type: 'grafana-testdata-datasource',
access: 'direct',
jsonData: {}
};
return this.backendSrv.post('/api/datasources', dsInstance);
}
return Promise.resolve();
});
}
}

View File

@@ -0,0 +1,22 @@
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="false">
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword">Scenario</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="ctrl.target.scenarioId" ng-options="v.id as v.name for v in ctrl.scenarioList" ng-change="ctrl.scenarioChanged()"></select>
</div>
</div>
<div class="gf-form gf-form gf-form--grow" ng-if="ctrl.scenario.stringInput">
<label class="gf-form-label query-keyword">String Input</label>
<input type="text" class="gf-form-input" placeholder="{{ctrl.scenario.stringInput}}" ng-model="ctrl.target.stringInput" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword">Alias</label>
<input type="text" class="gf-form-input max-width-7" placeholder="optional" ng-model="ctrl.target.alias" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
</query-editor-row>

View File

@@ -0,0 +1,32 @@
{
"type": "app",
"name": "Grafana TestData",
"id": "testdata",
"info": {
"description": "Grafana test data app",
"author": {
"name": "Grafana Project",
"url": "http://grafana.org"
},
"version": "1.0.13",
"updated": "2016-09-26"
},
"includes": [
{
"type": "dashboard",
"name": "TestData - Graph Last 1h",
"path": "dashboards/graph_last_1h.json"
},
{
"type": "dashboard",
"name": "TestData - Alerts",
"path": "dashboards/alerts.json"
}
],
"dependencies": {
"grafanaVersion": "4.x.x"
}
}

View File

@@ -216,11 +216,6 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
});
};
function escapeForJson(value) {
var luceneQuery = JSON.stringify(value);
return luceneQuery.substr(1, luceneQuery.length - 2);
}
this.getFields = function(query) {
return this._get('/_mapping').then(function(result) {
var typeMap = {
@@ -285,7 +280,6 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
var header = this.getQueryHeader('count', range.from, range.to);
var esQuery = angular.toJson(this.queryBuilder.getTermsQuery(queryDef));
esQuery = esQuery.replace("$lucene_query", escapeForJson(queryDef.query));
esQuery = esQuery.replace(/\$timeFrom/g, range.from.valueOf());
esQuery = esQuery.replace(/\$timeTo/g, range.to.valueOf());
esQuery = header + '\n' + esQuery + '\n';

View File

@@ -221,12 +221,6 @@ function (queryDef) {
"size": 0,
"query": {
"filtered": {
"query": {
"query_string": {
"analyze_wildcard": true,
"query": '$lucene_query',
}
},
"filter": {
"bool": {
"must": [{"range": this.getRangeFilter()}]
@@ -235,6 +229,16 @@ function (queryDef) {
}
}
};
if (queryDef.query) {
query.query.filtered.query = {
"query_string": {
"analyze_wildcard": true,
"query": queryDef.query,
}
};
}
query.aggs = {
"1": {
"terms": {

View File

@@ -9,6 +9,8 @@ class GrafanaDatasource {
return this.backendSrv.get('/api/metrics/test', {
from: options.range.from.valueOf(),
to: options.range.to.valueOf(),
scenario: 'random_walk',
interval: options.intervalMs,
maxDataPoints: options.maxDataPoints
});
}

View File

@@ -134,13 +134,7 @@ for (var i = 0; i < 128; i++) {
i >= 97 && i <= 122; // a-z
}
var identifierPartTable = [];
for (var i2 = 0; i2 < 128; i2++) {
identifierPartTable[i2] =
identifierStartTable[i2] || // $, _, A-Z, a-z
i2 >= 48 && i2 <= 57; // 0-9
}
var identifierPartTable = identifierStartTable;
export function Lexer(expression) {
this.input = expression;
@@ -423,256 +417,260 @@ Lexer.prototype = {
if (char === '-') {
value += char;
index += 1;
char = this.peek(index);
}
char = this.peek(index);
}
// Numbers must start either with a decimal digit or a point.
if (char !== "." && !isDecimalDigit(char)) {
return null;
}
// Numbers must start either with a decimal digit or a point.
if (char !== "." && !isDecimalDigit(char)) {
return null;
}
if (char !== ".") {
value += this.peek(index);
index += 1;
char = this.peek(index);
if (char !== ".") {
value += this.peek(index);
index += 1;
char = this.peek(index);
if (value === "0") {
// Base-16 numbers.
if (char === "x" || char === "X") {
index += 1;
value += char;
while (index < length) {
char = this.peek(index);
if (!isHexDigit(char)) {
break;
}
value += char;
index += 1;
}
if (value.length <= 2) { // 0x
return {
type: 'number',
value: value,
isMalformed: true,
pos: this.char
};
}
if (index < length) {
char = this.peek(index);
if (isIdentifierStart(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 16,
isMalformed: false,
pos: this.char
};
}
// Base-8 numbers.
if (isOctalDigit(char)) {
index += 1;
value += char;
bad = false;
while (index < length) {
char = this.peek(index);
// Numbers like '019' (note the 9) are not valid octals
// but we still parse them and mark as malformed.
if (isDecimalDigit(char)) {
bad = true;
} else if (!isOctalDigit(char)) {
break;
}
value += char;
index += 1;
}
if (index < length) {
char = this.peek(index);
if (isIdentifierStart(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 8,
isMalformed: false
};
}
// Decimal numbers that start with '0' such as '09' are illegal
// but we still parse them and return as malformed.
if (isDecimalDigit(char)) {
index += 1;
value += char;
}
}
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
if (value === "0") {
// Base-16 numbers.
if (char === "x" || char === "X") {
index += 1;
value += char;
index += 1;
}
}
// Decimal digits.
if (char === ".") {
value += char;
index += 1;
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
}
// Exponent part.
if (char === "e" || char === "E") {
value += char;
index += 1;
char = this.peek(index);
if (char === "+" || char === "-") {
value += this.peek(index);
index += 1;
}
char = this.peek(index);
if (isDecimalDigit(char)) {
value += char;
index += 1;
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
if (!isHexDigit(char)) {
break;
}
value += char;
index += 1;
}
} else {
return null;
}
}
if (index < length) {
char = this.peek(index);
if (!this.isPunctuator(char)) {
return null;
}
}
if (value.length <= 2) { // 0x
return {
type: 'number',
value: value,
isMalformed: true,
pos: this.char
};
}
return {
type: 'number',
value: value,
base: 10,
pos: this.char,
isMalformed: !isFinite(+value)
};
},
if (index < length) {
char = this.peek(index);
if (isIdentifierStart(char)) {
return null;
}
}
isPunctuator: function (ch1) {
switch (ch1) {
case ".":
case "(":
case ")":
case ",":
case "{":
case "}":
return true;
}
return false;
},
scanPunctuator: function () {
var ch1 = this.peek();
if (this.isPunctuator(ch1)) {
return {
type: ch1,
value: ch1,
pos: this.char
};
}
return null;
},
/*
* Extract a string out of the next sequence of characters and/or
* lines or return 'null' if its not possible. Since strings can
* span across multiple lines this method has to move the char
* pointer.
*
* This method recognizes pseudo-multiline JavaScript strings:
*
* var str = "hello\
* world";
*/
scanStringLiteral: function () {
/*jshint loopfunc:true */
var quote = this.peek();
// String must start with a quote.
if (quote !== "\"" && quote !== "'") {
return null;
}
var value = "";
this.skip();
while (this.peek() !== quote) {
if (this.peek() === "") { // End Of Line
return {
type: 'string',
type: 'number',
value: value,
isUnclosed: true,
quote: quote,
base: 16,
isMalformed: false,
pos: this.char
};
}
var char = this.peek();
var jump = 1; // A length of a jump, after we're done
// parsing this character.
// Base-8 numbers.
if (isOctalDigit(char)) {
index += 1;
value += char;
bad = false;
value += char;
this.skip(jump);
while (index < length) {
char = this.peek(index);
// Numbers like '019' (note the 9) are not valid octals
// but we still parse them and mark as malformed.
if (isDecimalDigit(char)) {
bad = true;
} if (!isOctalDigit(char)) {
// if the char is a non punctuator then its not a valid number
if (!this.isPunctuator(char)) {
return null;
}
break;
}
value += char;
index += 1;
}
if (index < length) {
char = this.peek(index);
if (isIdentifierStart(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 8,
isMalformed: bad
};
}
// Decimal numbers that start with '0' such as '09' are illegal
// but we still parse them and return as malformed.
if (isDecimalDigit(char)) {
index += 1;
value += char;
}
}
this.skip();
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
}
// Decimal digits.
if (char === ".") {
value += char;
index += 1;
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
}
// Exponent part.
if (char === "e" || char === "E") {
value += char;
index += 1;
char = this.peek(index);
if (char === "+" || char === "-") {
value += this.peek(index);
index += 1;
}
char = this.peek(index);
if (isDecimalDigit(char)) {
value += char;
index += 1;
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
} else {
return null;
}
}
if (index < length) {
char = this.peek(index);
if (!this.isPunctuator(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 10,
pos: this.char,
isMalformed: !isFinite(+value)
};
},
isPunctuator: function (ch1) {
switch (ch1) {
case ".":
case "(":
case ")":
case ",":
case "{":
case "}":
return true;
}
return false;
},
scanPunctuator: function () {
var ch1 = this.peek();
if (this.isPunctuator(ch1)) {
return {
type: 'string',
value: value,
isUnclosed: false,
quote: quote,
type: ch1,
value: ch1,
pos: this.char
};
},
}
};
return null;
},
/*
* Extract a string out of the next sequence of characters and/or
* lines or return 'null' if its not possible. Since strings can
* span across multiple lines this method has to move the char
* pointer.
*
* This method recognizes pseudo-multiline JavaScript strings:
*
* var str = "hello\
* world";
*/
scanStringLiteral: function () {
/*jshint loopfunc:true */
var quote = this.peek();
// String must start with a quote.
if (quote !== "\"" && quote !== "'") {
return null;
}
var value = "";
this.skip();
while (this.peek() !== quote) {
if (this.peek() === "") { // End Of Line
return {
type: 'string',
value: value,
isUnclosed: true,
quote: quote,
pos: this.char
};
}
var char = this.peek();
var jump = 1; // A length of a jump, after we're done
// parsing this character.
value += char;
this.skip(jump);
}
this.skip();
return {
type: 'string',
value: value,
isUnclosed: false,
quote: quote,
pos: this.char
};
},
};

View File

@@ -100,10 +100,7 @@ Parser.prototype = {
},
metricExpression: function() {
if (!this.match('templateStart') &&
!this.match('identifier') &&
!this.match('number') &&
!this.match('{')) {
if (!this.match('templateStart') && !this.match('identifier') && !this.match('number') && !this.match('{')) {
return null;
}

View File

@@ -8,6 +8,7 @@
],
"metrics": true,
"alerting": true,
"annotations": true,
"info": {
@@ -20,4 +21,4 @@
"large": "img/graphite_logo.png"
}
}
}
}

View File

@@ -62,6 +62,14 @@ describe('when lexing graphite expression', function() {
expect(tokens[4].type).to.be('identifier');
});
it('should tokenize metric expression with segment that start with number', function() {
var lexer = new Lexer("metric.001-server");
var tokens = lexer.tokenize();
expect(tokens[0].type).to.be('identifier');
expect(tokens[2].type).to.be('identifier');
expect(tokens.length).to.be(3);
});
it('should tokenize func call with numbered metric and number arg', function() {
var lexer = new Lexer("scale(metric.10, 15)");
var tokens = lexer.tokenize();

View File

@@ -113,7 +113,6 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
throw response.error;
}
delete self.lastErrors.query;
_.each(response.data.data.result, function(metricData) {
result.push(self.transformMetricData(metricData, activeTargets[index], start, end));
});
@@ -124,6 +123,10 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
};
this.performTimeSeriesQuery = function(query, start, end) {
if (start > end) {
throw { message: 'Invalid time range' };
}
var url = '/api/v1/query_range?query=' + encodeURIComponent(query.expr) + '&start=' + start + '&end=' + end + '&step=' + query.step;
return this._request('GET', url, query.requestId);
};

View File

@@ -8,6 +8,7 @@
],
"metrics": true,
"alerting": true,
"annotations": true,
"info": {

View File

@@ -40,6 +40,32 @@
<div class="section gf-form-group">
<h5 class="section-heading">X-Axis</h5>
<gf-form-switch class="gf-form" label="Show" label-class="width-5" checked="ctrl.panel.xaxis.show" on-change="ctrl.render()"></gf-form-switch>
<div class="gf-form">
<label class="gf-form-label width-5">Mode</label>
<div class="gf-form-select-wrapper max-width-15">
<select class="gf-form-input" ng-model="ctrl.panel.xaxis.mode" ng-options="v as k for (k, v) in ctrl.xAxisModes" ng-change="ctrl.xAxisOptionChanged()"> </select>
</div>
</div>
<!-- Table mode -->
<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'field'">
<label class="gf-form-label width-5">Name</label>
<metric-segment-model property="ctrl.panel.xaxis.name" get-options="ctrl.getDataFieldNames(false)" on-change="ctrl.xAxisOptionChanged()" custom="false" css-class="width-10" select-mode="true"></metric-segment-model>
</div>
<!-- Series mode -->
<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'field'">
<label class="gf-form-label width-5">Value</label>
<metric-segment-model property="ctrl.panel.xaxis.values[0]" get-options="ctrl.getDataFieldNames(true)" on-change="ctrl.xAxisOptionChanged()" custom="false" css-class="width-10" select-mode="true"></metric-segment-model>
</div>
<!-- Series mode -->
<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'series'">
<label class="gf-form-label width-5">Value</label>
<metric-segment-model property="ctrl.panel.xaxis.values[0]" options="ctrl.xAxisStatOptions" on-change="ctrl.xAxisOptionChanged()" custom="false" css-class="width-10" select-mode="true"></metric-segment-model>
</div>
</div>
</div>

View File

@@ -0,0 +1,85 @@
///<reference path="../../../headers/common.d.ts" />
import kbn from 'app/core/utils/kbn';
export class AxesEditorCtrl {
panel: any;
panelCtrl: any;
unitFormats: any;
logScales: any;
xAxisModes: any;
xAxisStatOptions: any;
xNameSegment: any;
/** @ngInject **/
constructor(private $scope, private $q) {
this.panelCtrl = $scope.ctrl;
this.panel = this.panelCtrl.panel;
$scope.ctrl = this;
this.unitFormats = kbn.getUnitFormats();
this.logScales = {
'linear': 1,
'log (base 2)': 2,
'log (base 10)': 10,
'log (base 32)': 32,
'log (base 1024)': 1024
};
this.xAxisModes = {
'Time': 'time',
'Series': 'series',
// 'Data field': 'field',
};
this.xAxisStatOptions = [
{text: 'Avg', value: 'avg'},
{text: 'Min', value: 'min'},
{text: 'Max', value: 'min'},
{text: 'Total', value: 'total'},
{text: 'Count', value: 'count'},
];
if (this.panel.xaxis.mode === 'custom') {
if (!this.panel.xaxis.name) {
this.panel.xaxis.name = 'specify field';
}
}
}
setUnitFormat(axis, subItem) {
axis.format = subItem.value;
this.panelCtrl.render();
}
render() {
this.panelCtrl.render();
}
xAxisOptionChanged() {
this.panelCtrl.processor.setPanelDefaultsForNewXAxisMode();
this.panelCtrl.onDataReceived(this.panelCtrl.dataList);
}
getDataFieldNames(onlyNumbers) {
var props = this.panelCtrl.processor.getDataFieldNames(this.panelCtrl.dataList, onlyNumbers);
var items = props.map(prop => {
return {text: prop, value: prop};
});
return this.$q.when(items);
}
}
/** @ngInject **/
export function axesEditorComponent() {
'use strict';
return {
restrict: 'E',
scope: true,
templateUrl: 'public/app/plugins/panel/graph/axes_editor.html',
controller: AxesEditorCtrl,
};
}

View File

@@ -0,0 +1,192 @@
///<reference path="../../../headers/common.d.ts" />
import kbn from 'app/core/utils/kbn';
import _ from 'lodash';
import moment from 'moment';
import TimeSeries from 'app/core/time_series2';
import {colors} from 'app/core/core';
export class DataProcessor {
constructor(private panel) {
}
getSeriesList(options) {
if (!options.dataList || options.dataList.length === 0) {
return [];
}
// auto detect xaxis mode
var firstItem;
if (options.dataList && options.dataList.length > 0) {
firstItem = options.dataList[0];
let autoDetectMode = this.getAutoDetectXAxisMode(firstItem);
if (this.panel.xaxis.mode !== autoDetectMode) {
this.panel.xaxis.mode = autoDetectMode;
this.setPanelDefaultsForNewXAxisMode();
}
}
switch (this.panel.xaxis.mode) {
case 'series':
case 'time': {
return options.dataList.map((item, index) => {
return this.timeSeriesHandler(item, index, options);
});
}
case 'field': {
return this.customHandler(firstItem);
}
}
}
getAutoDetectXAxisMode(firstItem) {
switch (firstItem.type) {
case 'docs': return 'field';
case 'table': return 'field';
default: {
if (this.panel.xaxis.mode === 'series') {
return 'series';
}
return 'time';
}
}
}
setPanelDefaultsForNewXAxisMode() {
switch (this.panel.xaxis.mode) {
case 'time': {
this.panel.bars = false;
this.panel.lines = true;
this.panel.points = false;
this.panel.legend.show = true;
this.panel.tooltip.shared = true;
this.panel.xaxis.values = [];
break;
}
case 'series': {
this.panel.bars = true;
this.panel.lines = false;
this.panel.points = false;
this.panel.stack = false;
this.panel.legend.show = false;
this.panel.tooltip.shared = false;
this.panel.xaxis.values = ['total'];
break;
}
}
}
timeSeriesHandler(seriesData, index, options) {
var datapoints = seriesData.datapoints || [];
var alias = seriesData.target;
var colorIndex = index % colors.length;
var color = this.panel.aliasColors[alias] || colors[colorIndex];
var series = new TimeSeries({datapoints: datapoints, alias: alias, color: color, unit: seriesData.unit});
if (datapoints && datapoints.length > 0) {
var last = datapoints[datapoints.length - 1][1];
var from = options.range.from;
if (last - from < -10000) {
series.isOutsideRange = true;
}
}
return series;
}
customHandler(dataItem) {
let nameField = this.panel.xaxis.name;
if (!nameField) {
throw {message: 'No field name specified to use for x-axis, check your axes settings'};
}
return [];
}
validateXAxisSeriesValue() {
switch (this.panel.xaxis.mode) {
case 'series': {
if (this.panel.xaxis.values.length === 0) {
this.panel.xaxis.values = ['total'];
return;
}
var validOptions = this.getXAxisValueOptions({});
var found = _.find(validOptions, {value: this.panel.xaxis.values[0]});
if (!found) {
this.panel.xaxis.values = ['total'];
}
return;
}
}
}
getDataFieldNames(dataList, onlyNumbers) {
if (dataList.length === 0) {
return [];
}
let fields = [];
var firstItem = dataList[0];
if (firstItem.type === 'docs'){
if (firstItem.datapoints.length === 0) {
return [];
}
let fieldParts = [];
function getPropertiesRecursive(obj) {
_.forEach(obj, (value, key) => {
if (_.isObject(value)) {
fieldParts.push(key);
getPropertiesRecursive(value);
} else {
if (!onlyNumbers || _.isNumber(value)) {
let field = fieldParts.concat(key).join('.');
fields.push(field);
}
}
});
fieldParts.pop();
}
getPropertiesRecursive(firstItem.datapoints[0]);
return fields;
}
}
getXAxisValueOptions(options) {
switch (this.panel.xaxis.mode) {
case 'time': {
return [];
}
case 'series': {
return [
{text: 'Avg', value: 'avg'},
{text: 'Min', value: 'min'},
{text: 'Max', value: 'min'},
{text: 'Total', value: 'total'},
{text: 'Count', value: 'count'},
];
}
}
}
pluckDeep(obj: any, property: string) {
let propertyParts = property.split('.');
let value = obj;
for (let i = 0; i < propertyParts.length; ++i) {
if (value[propertyParts[i]]) {
value = value[propertyParts[i]];
} else {
return undefined;
}
}
return value;
}
}

View File

@@ -1,533 +0,0 @@
define([
'angular',
'jquery',
'moment',
'lodash',
'app/core/utils/kbn',
'./graph_tooltip',
'./threshold_manager',
'jquery.flot',
'jquery.flot.selection',
'jquery.flot.time',
'jquery.flot.stack',
'jquery.flot.stackpercent',
'jquery.flot.fillbelow',
'jquery.flot.crosshair',
'./jquery.flot.events',
],
function (angular, $, moment, _, kbn, GraphTooltip, thresholdManExports) {
'use strict';
var module = angular.module('grafana.directives');
var labelWidthCache = {};
module.directive('grafanaGraph', function($rootScope, timeSrv) {
return {
restrict: 'A',
template: '<div> </div>',
link: function(scope, elem) {
var ctrl = scope.ctrl;
var dashboard = ctrl.dashboard;
var panel = ctrl.panel;
var data, annotations;
var sortedSeries;
var legendSideLastValue = null;
var rootScope = scope.$root;
var panelWidth = 0;
var thresholdManager = new thresholdManExports.ThresholdManager(ctrl);
rootScope.onAppEvent('setCrosshair', function(event, info) {
// do not need to to this if event is from this panel
if (info.scope === scope) {
return;
}
if(dashboard.sharedCrosshair) {
var plot = elem.data().plot;
if (plot) {
plot.setCrosshair({ x: info.pos.x, y: info.pos.y });
}
}
}, scope);
rootScope.onAppEvent('clearCrosshair', function() {
var plot = elem.data().plot;
if (plot) {
plot.clearCrosshair();
}
}, scope);
// Receive render events
ctrl.events.on('render', function(renderData) {
data = renderData || data;
if (!data) {
return;
}
annotations = data.annotations || annotations;
render_panel();
});
function getLegendHeight(panelHeight) {
if (!panel.legend.show || panel.legend.rightSide) {
return 0;
}
if (panel.legend.alignAsTable) {
var legendSeries = _.filter(data, function(series) {
return series.hideFromLegend(panel.legend) === false;
});
var total = 23 + (21 * legendSeries.length);
return Math.min(total, Math.floor(panelHeight/2));
} else {
return 26;
}
}
function setElementHeight() {
try {
var height = ctrl.height - getLegendHeight(ctrl.height);
elem.css('height', height + 'px');
return true;
} catch(e) { // IE throws errors sometimes
console.log(e);
return false;
}
}
function shouldAbortRender() {
if (!data) {
return true;
}
if (!setElementHeight()) { return true; }
if (panelWidth === 0) {
return true;
}
}
function getLabelWidth(text, elem) {
var labelWidth = labelWidthCache[text];
if (!labelWidth) {
labelWidth = labelWidthCache[text] = elem.width();
}
return labelWidth;
}
function drawHook(plot) {
// Update legend values
var yaxis = plot.getYAxes();
for (var i = 0; i < data.length; i++) {
var series = data[i];
var axis = yaxis[series.yaxis - 1];
var formater = kbn.valueFormats[panel.yaxes[series.yaxis - 1].format];
// decimal override
if (_.isNumber(panel.decimals)) {
series.updateLegendValues(formater, panel.decimals, null);
} else {
// auto decimals
// legend and tooltip gets one more decimal precision
// than graph legend ticks
var tickDecimals = (axis.tickDecimals || -1) + 1;
series.updateLegendValues(formater, tickDecimals, axis.scaledDecimals + 2);
}
if(!rootScope.$$phase) { scope.$digest(); }
}
// add left axis labels
if (panel.yaxes[0].label) {
var yaxisLabel = $("<div class='axisLabel left-yaxis-label'></div>")
.text(panel.yaxes[0].label)
.appendTo(elem);
yaxisLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[0].label, yaxisLabel) / 2) + 'px';
}
// add right axis labels
if (panel.yaxes[1].label) {
var rightLabel = $("<div class='axisLabel right-yaxis-label'></div>")
.text(panel.yaxes[1].label)
.appendTo(elem);
rightLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[1].label, rightLabel) / 2) + 'px';
}
thresholdManager.draw(plot);
}
function processOffsetHook(plot, gridMargin) {
var left = panel.yaxes[0];
var right = panel.yaxes[1];
if (left.show && left.label) { gridMargin.left = 20; }
if (right.show && right.label) { gridMargin.right = 20; }
}
// Function for rendering panel
function render_panel() {
panelWidth = elem.width();
if (shouldAbortRender()) {
return;
}
// give space to alert editing
thresholdManager.prepare(elem, data);
var stack = panel.stack ? true : null;
// Populate element
var options = {
hooks: {
draw: [drawHook],
processOffset: [processOffsetHook],
},
legend: { show: false },
series: {
stackpercent: panel.stack ? panel.percentage : false,
stack: panel.percentage ? null : stack,
lines: {
show: panel.lines,
zero: false,
fill: translateFillOption(panel.fill),
lineWidth: panel.linewidth,
steps: panel.steppedLine
},
bars: {
show: panel.bars,
fill: 1,
barWidth: 1,
zero: false,
lineWidth: 0
},
points: {
show: panel.points,
fill: 1,
fillColor: false,
radius: panel.points ? panel.pointradius : 2
},
shadowSize: 0
},
yaxes: [],
xaxis: {},
grid: {
minBorderMargin: 0,
markings: [],
backgroundColor: null,
borderWidth: 0,
hoverable: true,
color: '#c8c8c8',
margin: { left: 0, right: 0 },
},
selection: {
mode: "x",
color: '#666'
},
crosshair: {
mode: panel.tooltip.shared || dashboard.sharedCrosshair ? "x" : null
}
};
for (var i = 0; i < data.length; i++) {
var series = data[i];
series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode);
// if hidden remove points and disable stack
if (ctrl.hiddenSeries[series.alias]) {
series.data = [];
series.stack = false;
}
}
if (data.length && data[0].stats.timeStep) {
options.series.bars.barWidth = data[0].stats.timeStep / 1.5;
}
addTimeAxis(options);
thresholdManager.addPlotOptions(options, panel);
addAnnotations(options);
configureAxisOptions(data, options);
sortedSeries = _.sortBy(data, function(series) { return series.zindex; });
function callPlot(incrementRenderCounter) {
try {
$.plot(elem, sortedSeries, options);
if (ctrl.renderError) {
delete ctrl.error;
delete ctrl.inspector;
}
} catch (e) {
console.log('flotcharts error', e);
ctrl.error = e.message || "Render Error";
ctrl.renderError = true;
ctrl.inspector = {error: e};
}
if (incrementRenderCounter) {
ctrl.renderingCompleted();
}
}
if (shouldDelayDraw(panel)) {
// temp fix for legends on the side, need to render twice to get dimensions right
callPlot(false);
setTimeout(function() { callPlot(true); }, 50);
legendSideLastValue = panel.legend.rightSide;
}
else {
callPlot(true);
}
}
function translateFillOption(fill) {
return fill === 0 ? 0.001 : fill/10;
}
function shouldDelayDraw(panel) {
if (panel.legend.rightSide) {
return true;
}
if (legendSideLastValue !== null && panel.legend.rightSide !== legendSideLastValue) {
return true;
}
}
function addTimeAxis(options) {
var ticks = panelWidth / 100;
var min = _.isUndefined(ctrl.range.from) ? null : ctrl.range.from.valueOf();
var max = _.isUndefined(ctrl.range.to) ? null : ctrl.range.to.valueOf();
options.xaxis = {
timezone: dashboard.getTimezone(),
show: panel.xaxis.show,
mode: "time",
min: min,
max: max,
label: "Datetime",
ticks: ticks,
timeformat: time_format(ticks, min, max),
};
}
function addAnnotations(options) {
if(!annotations || annotations.length === 0) {
return;
}
var types = {};
for (var i = 0; i < annotations.length; i++) {
var item = annotations[i];
if (!types[item.source.name]) {
types[item.source.name] = {
color: item.source.iconColor,
position: 'BOTTOM',
markerSize: 5,
};
}
}
options.events = {
levels: _.keys(types).length + 1,
data: annotations,
types: types,
};
}
//Override min/max to provide more flexible autoscaling
function autoscaleSpanOverride(yaxis, data, options) {
var expr;
if (yaxis.min != null && data != null) {
expr = parseThresholdExpr(yaxis.min);
options.min = autoscaleYAxisMin(expr, data.stats);
}
if (yaxis.max != null && data != null) {
expr = parseThresholdExpr(yaxis.max);
options.max = autoscaleYAxisMax(expr, data.stats);
}
}
function parseThresholdExpr(expr) {
var match, operator, value, precision;
expr = String(expr);
match = expr.match(/\s*([<=>~]*)\s*(\-?\d+(\.\d+)?)/);
if (match) {
operator = match[1];
value = parseFloat(match[2]);
//Precision based on input
precision = match[3] ? match[3].length - 1 : 0;
return {
operator: operator,
value: value,
precision: precision
};
} else {
return undefined;
}
}
function autoscaleYAxisMax(expr, dataStats) {
var operator = expr.operator,
value = expr.value,
precision = expr.precision;
if (operator === ">") {
return dataStats.max < value ? value : null;
} else if (operator === "<") {
return dataStats.max > value ? value : null;
} else if (operator === "~") {
return kbn.roundValue(dataStats.avg + value, precision);
} else if (operator === "=") {
return kbn.roundValue(dataStats.current + value, precision);
} else if (!operator && !isNaN(value)) {
return kbn.roundValue(value, precision);
} else {
return null;
}
}
function autoscaleYAxisMin(expr, dataStats) {
var operator = expr.operator,
value = expr.value,
precision = expr.precision;
if (operator === ">") {
return dataStats.min < value ? value : null;
} else if (operator === "<") {
return dataStats.min > value ? value : null;
} else if (operator === "~") {
return kbn.roundValue(dataStats.avg - value, precision);
} else if (operator === "=") {
return kbn.roundValue(dataStats.current - value, precision);
} else if (!operator && !isNaN(value)) {
return kbn.roundValue(value, precision);
} else {
return null;
}
}
function configureAxisOptions(data, options) {
var defaults = {
position: 'left',
show: panel.yaxes[0].show,
min: panel.yaxes[0].min,
index: 1,
logBase: panel.yaxes[0].logBase || 1,
max: panel.percentage && panel.stack ? 100 : panel.yaxes[0].max,
};
autoscaleSpanOverride(panel.yaxes[0], data[0], defaults);
options.yaxes.push(defaults);
if (_.find(data, {yaxis: 2})) {
var secondY = _.clone(defaults);
secondY.index = 2,
secondY.show = panel.yaxes[1].show;
secondY.logBase = panel.yaxes[1].logBase || 1,
secondY.position = 'right';
secondY.min = panel.yaxes[1].min;
secondY.max = panel.percentage && panel.stack ? 100 : panel.yaxes[1].max;
autoscaleSpanOverride(panel.yaxes[1], data[1], secondY);
options.yaxes.push(secondY);
applyLogScale(options.yaxes[1], data);
configureAxisMode(options.yaxes[1], panel.percentage && panel.stack ? "percent" : panel.yaxes[1].format);
}
applyLogScale(options.yaxes[0], data);
configureAxisMode(options.yaxes[0], panel.percentage && panel.stack ? "percent" : panel.yaxes[0].format);
}
function applyLogScale(axis, data) {
if (axis.logBase === 1) {
return;
}
var series, i;
var max = axis.max;
if (max === null) {
for (i = 0; i < data.length; i++) {
series = data[i];
if (series.yaxis === axis.index) {
if (max < series.stats.max) {
max = series.stats.max;
}
}
}
if (max === void 0) {
max = Number.MAX_VALUE;
}
}
axis.min = axis.min !== null ? axis.min : 0;
axis.ticks = [0, 1];
var nextTick = 1;
while (true) {
nextTick = nextTick * axis.logBase;
axis.ticks.push(nextTick);
if (nextTick > max) {
break;
}
}
if (axis.logBase === 10) {
axis.transform = function(v) { return Math.log(v+0.1); };
axis.inverseTransform = function (v) { return Math.pow(10,v); };
} else {
axis.transform = function(v) { return Math.log(v+0.1) / Math.log(axis.logBase); };
axis.inverseTransform = function (v) { return Math.pow(axis.logBase,v); };
}
}
function configureAxisMode(axis, format) {
axis.tickFormatter = function(val, axis) {
return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
};
}
function time_format(ticks, min, max) {
if (min && max && ticks) {
var range = max - min;
var secPerTick = (range/ticks) / 1000;
var oneDay = 86400000;
var oneYear = 31536000000;
if (secPerTick <= 45) {
return "%H:%M:%S";
}
if (secPerTick <= 7200 || range <= oneDay) {
return "%H:%M";
}
if (secPerTick <= 80000) {
return "%m/%d %H:%M";
}
if (secPerTick <= 2419200 || range <= oneYear) {
return "%m/%d";
}
return "%Y-%m";
}
return "%H:%M";
}
new GraphTooltip(elem, dashboard, scope, function() {
return sortedSeries;
});
elem.bind("plotselected", function (event, ranges) {
scope.$apply(function() {
timeSrv.setTime({
from : moment.utc(ranges.xaxis.from),
to : moment.utc(ranges.xaxis.to),
});
});
});
}
};
});
});

View File

@@ -0,0 +1,521 @@
///<reference path="../../../headers/common.d.ts" />
import 'jquery.flot';
import 'jquery.flot.selection';
import 'jquery.flot.time';
import 'jquery.flot.stack';
import 'jquery.flot.stackpercent';
import 'jquery.flot.fillbelow';
import 'jquery.flot.crosshair';
import './jquery.flot.events';
import angular from 'angular';
import $ from 'jquery';
import moment from 'moment';
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import GraphTooltip from './graph_tooltip';
import {ThresholdManager} from './threshold_manager';
var module = angular.module('grafana.directives');
var labelWidthCache = {};
module.directive('grafanaGraph', function($rootScope, timeSrv) {
return {
restrict: 'A',
template: '',
link: function(scope, elem) {
var ctrl = scope.ctrl;
var dashboard = ctrl.dashboard;
var panel = ctrl.panel;
var data, annotations;
var sortedSeries;
var legendSideLastValue = null;
var rootScope = scope.$root;
var panelWidth = 0;
var thresholdManager = new ThresholdManager(ctrl);
rootScope.onAppEvent('setCrosshair', function(event, info) {
// do not need to to this if event is from this panel
if (info.scope === scope) {
return;
}
if (dashboard.sharedCrosshair) {
var plot = elem.data().plot;
if (plot) {
plot.setCrosshair({ x: info.pos.x, y: info.pos.y });
}
}
}, scope);
rootScope.onAppEvent('clearCrosshair', function() {
var plot = elem.data().plot;
if (plot) {
plot.clearCrosshair();
}
}, scope);
// Receive render events
ctrl.events.on('render', function(renderData) {
data = renderData || data;
if (!data) {
return;
}
annotations = data.annotations || annotations;
render_panel();
});
function getLegendHeight(panelHeight) {
if (!panel.legend.show || panel.legend.rightSide) {
return 0;
}
if (panel.legend.alignAsTable) {
var legendSeries = _.filter(data, function(series) {
return series.hideFromLegend(panel.legend) === false;
});
var total = 23 + (21 * legendSeries.length);
return Math.min(total, Math.floor(panelHeight/2));
} else {
return 26;
}
}
function setElementHeight() {
try {
var height = ctrl.height - getLegendHeight(ctrl.height);
elem.css('height', height + 'px');
return true;
} catch (e) { // IE throws errors sometimes
console.log(e);
return false;
}
}
function shouldAbortRender() {
if (!data) {
return true;
}
if (!setElementHeight()) { return true; }
if (panelWidth === 0) {
return true;
}
}
function getLabelWidth(text, elem) {
var labelWidth = labelWidthCache[text];
if (!labelWidth) {
labelWidth = labelWidthCache[text] = elem.width();
}
return labelWidth;
}
function drawHook(plot) {
// Update legend values
var yaxis = plot.getYAxes();
for (var i = 0; i < data.length; i++) {
var series = data[i];
var axis = yaxis[series.yaxis - 1];
var formater = kbn.valueFormats[panel.yaxes[series.yaxis - 1].format];
// decimal override
if (_.isNumber(panel.decimals)) {
series.updateLegendValues(formater, panel.decimals, null);
} else {
// auto decimals
// legend and tooltip gets one more decimal precision
// than graph legend ticks
var tickDecimals = (axis.tickDecimals || -1) + 1;
series.updateLegendValues(formater, tickDecimals, axis.scaledDecimals + 2);
}
if (!rootScope.$$phase) { scope.$digest(); }
}
// add left axis labels
if (panel.yaxes[0].label) {
var yaxisLabel = $("<div class='axisLabel left-yaxis-label'></div>")
.text(panel.yaxes[0].label)
.appendTo(elem);
yaxisLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[0].label, yaxisLabel) / 2) + 'px';
}
// add right axis labels
if (panel.yaxes[1].label) {
var rightLabel = $("<div class='axisLabel right-yaxis-label'></div>")
.text(panel.yaxes[1].label)
.appendTo(elem);
rightLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[1].label, rightLabel) / 2) + 'px';
}
thresholdManager.draw(plot);
}
function processOffsetHook(plot, gridMargin) {
var left = panel.yaxes[0];
var right = panel.yaxes[1];
if (left.show && left.label) { gridMargin.left = 20; }
if (right.show && right.label) { gridMargin.right = 20; }
// apply y-axis min/max options
var yaxis = plot.getYAxes();
for (var i = 0; i < yaxis.length; i++) {
var axis = yaxis[i];
var panelOptions = panel.yaxes[i];
axis.options.max = panelOptions.max;
axis.options.min = panelOptions.min;
}
}
// Function for rendering panel
function render_panel() {
panelWidth = elem.width();
if (shouldAbortRender()) {
return;
}
// give space to alert editing
thresholdManager.prepare(elem, data);
var stack = panel.stack ? true : null;
// Populate element
var options: any = {
hooks: {
draw: [drawHook],
processOffset: [processOffsetHook],
},
legend: { show: false },
series: {
stackpercent: panel.stack ? panel.percentage : false,
stack: panel.percentage ? null : stack,
lines: {
show: panel.lines,
zero: false,
fill: translateFillOption(panel.fill),
lineWidth: panel.linewidth,
steps: panel.steppedLine
},
bars: {
show: panel.bars,
fill: 1,
barWidth: 1,
zero: false,
lineWidth: 0
},
points: {
show: panel.points,
fill: 1,
fillColor: false,
radius: panel.points ? panel.pointradius : 2
},
shadowSize: 0
},
yaxes: [],
xaxis: {},
grid: {
minBorderMargin: 0,
markings: [],
backgroundColor: null,
borderWidth: 0,
hoverable: true,
color: '#c8c8c8',
margin: { left: 0, right: 0 },
},
selection: {
mode: "x",
color: '#666'
},
crosshair: {
mode: panel.tooltip.shared || dashboard.sharedCrosshair ? "x" : null
}
};
for (let i = 0; i < data.length; i++) {
var series = data[i];
series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode);
// if hidden remove points and disable stack
if (ctrl.hiddenSeries[series.alias]) {
series.data = [];
series.stack = false;
}
}
switch (panel.xaxis.mode) {
case 'series': {
options.series.bars.barWidth = 0.7;
options.series.bars.align = 'center';
for (let i = 0; i < data.length; i++) {
var series = data[i];
series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]];
}
addXSeriesAxis(options);
break;
}
case 'table': {
options.series.bars.barWidth = 0.7;
options.series.bars.align = 'center';
addXTableAxis(options);
break;
}
default: {
if (data.length && data[0].stats.timeStep) {
options.series.bars.barWidth = data[0].stats.timeStep / 1.5;
}
addTimeAxis(options);
break;
}
}
thresholdManager.addPlotOptions(options, panel);
addAnnotations(options);
configureAxisOptions(data, options);
sortedSeries = _.sortBy(data, function(series) { return series.zindex; });
function callPlot(incrementRenderCounter) {
try {
$.plot(elem, sortedSeries, options);
if (ctrl.renderError) {
delete ctrl.error;
delete ctrl.inspector;
}
} catch (e) {
console.log('flotcharts error', e);
ctrl.error = e.message || "Render Error";
ctrl.renderError = true;
ctrl.inspector = {error: e};
}
if (incrementRenderCounter) {
ctrl.renderingCompleted();
}
}
if (shouldDelayDraw(panel)) {
// temp fix for legends on the side, need to render twice to get dimensions right
callPlot(false);
setTimeout(function() { callPlot(true); }, 50);
legendSideLastValue = panel.legend.rightSide;
} else {
callPlot(true);
}
}
function translateFillOption(fill) {
return fill === 0 ? 0.001 : fill/10;
}
function shouldDelayDraw(panel) {
if (panel.legend.rightSide) {
return true;
}
if (legendSideLastValue !== null && panel.legend.rightSide !== legendSideLastValue) {
return true;
}
}
function addTimeAxis(options) {
var ticks = panelWidth / 100;
var min = _.isUndefined(ctrl.range.from) ? null : ctrl.range.from.valueOf();
var max = _.isUndefined(ctrl.range.to) ? null : ctrl.range.to.valueOf();
options.xaxis = {
timezone: dashboard.getTimezone(),
show: panel.xaxis.show,
mode: "time",
min: min,
max: max,
label: "Datetime",
ticks: ticks,
timeformat: time_format(ticks, min, max),
};
}
function addXSeriesAxis(options) {
var ticks = _.map(data, function(series, index) {
return [index + 1, series.alias];
});
options.xaxis = {
timezone: dashboard.getTimezone(),
show: panel.xaxis.show,
mode: null,
min: 0,
max: ticks.length + 1,
label: "Datetime",
ticks: ticks
};
}
function addXTableAxis(options) {
var ticks = _.map(data, function(series, seriesIndex) {
return _.map(series.datapoints, function(point, pointIndex) {
var tickIndex = seriesIndex * series.datapoints.length + pointIndex;
return [tickIndex + 1, point[1]];
});
});
ticks = _.flatten(ticks, true);
options.xaxis = {
timezone: dashboard.getTimezone(),
show: panel.xaxis.show,
mode: null,
min: 0,
max: ticks.length + 1,
label: "Datetime",
ticks: ticks
};
}
function addAnnotations(options) {
if (!annotations || annotations.length === 0) {
return;
}
var types = {};
for (var i = 0; i < annotations.length; i++) {
var item = annotations[i];
if (!types[item.source.name]) {
types[item.source.name] = {
color: item.source.iconColor,
position: 'BOTTOM',
markerSize: 5,
};
}
}
options.events = {
levels: _.keys(types).length + 1,
data: annotations,
types: types,
};
}
function configureAxisOptions(data, options) {
var defaults = {
position: 'left',
show: panel.yaxes[0].show,
index: 1,
logBase: panel.yaxes[0].logBase || 1,
max: 100, // correct later
};
options.yaxes.push(defaults);
if (_.find(data, {yaxis: 2})) {
var secondY = _.clone(defaults);
secondY.index = 2;
secondY.show = panel.yaxes[1].show;
secondY.logBase = panel.yaxes[1].logBase || 1;
secondY.position = 'right';
options.yaxes.push(secondY);
configureAxisMode(options.yaxes[1], panel.percentage && panel.stack ? "percent" : panel.yaxes[1].format);
}
applyLogScale(options.yaxes[0], data);
configureAxisMode(options.yaxes[0], panel.percentage && panel.stack ? "percent" : panel.yaxes[0].format);
}
function applyLogScale(axis, data) {
if (axis.logBase === 1) {
return;
}
var series, i;
var max = axis.max;
if (max === null) {
for (i = 0; i < data.length; i++) {
series = data[i];
if (series.yaxis === axis.index) {
if (max < series.stats.max) {
max = series.stats.max;
}
}
}
if (max === void 0) {
max = Number.MAX_VALUE;
}
}
axis.min = axis.min !== null ? axis.min : 0;
axis.ticks = [0, 1];
var nextTick = 1;
while (true) {
nextTick = nextTick * axis.logBase;
axis.ticks.push(nextTick);
if (nextTick > max) {
break;
}
}
if (axis.logBase === 10) {
axis.transform = function(v) { return Math.log(v+0.1); };
axis.inverseTransform = function (v) { return Math.pow(10,v); };
} else {
axis.transform = function(v) { return Math.log(v+0.1) / Math.log(axis.logBase); };
axis.inverseTransform = function (v) { return Math.pow(axis.logBase,v); };
}
}
function configureAxisMode(axis, format) {
axis.tickFormatter = function(val, axis) {
return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
};
}
function time_format(ticks, min, max) {
if (min && max && ticks) {
var range = max - min;
var secPerTick = (range/ticks) / 1000;
var oneDay = 86400000;
var oneYear = 31536000000;
if (secPerTick <= 45) {
return "%H:%M:%S";
}
if (secPerTick <= 7200 || range <= oneDay) {
return "%H:%M";
}
if (secPerTick <= 80000) {
return "%m/%d %H:%M";
}
if (secPerTick <= 2419200 || range <= oneYear) {
return "%m/%d";
}
return "%Y-%m";
}
return "%H:%M";
}
new GraphTooltip(elem, dashboard, scope, function() {
return sortedSeries;
});
elem.bind("plotselected", function (event, ranges) {
scope.$apply(function() {
timeSrv.setTime({
from : moment.utc(ranges.xaxis.from),
to : moment.utc(ranges.xaxis.to),
});
});
});
}
};
});

View File

@@ -121,20 +121,20 @@ function ($, _) {
var seriesList = getSeriesFn();
var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat;
if (panel.tooltip.msResolution) {
tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
} else {
tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
}
if (dashboard.sharedCrosshair) {
ctrl.publishAppEvent('setCrosshair', { pos: pos, scope: scope });
ctrl.publishAppEvent('setCrosshair', {pos: pos, scope: scope});
}
if (seriesList.length === 0) {
return;
}
if (seriesList[0].hasMsResolution) {
tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
} else {
tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
}
if (panel.tooltip.shared) {
plot.unhighlight();

View File

@@ -8,26 +8,26 @@ import './thresholds_form';
import template from './template';
import angular from 'angular';
import moment from 'moment';
import kbn from 'app/core/utils/kbn';
import _ from 'lodash';
import TimeSeries from 'app/core/time_series2';
import config from 'app/core/config';
import * as fileExport from 'app/core/utils/file_export';
import {MetricsPanelCtrl, alertTab} from 'app/plugins/sdk';
import {DataProcessor} from './data_processor';
import {axesEditorComponent} from './axes_editor';
class GraphCtrl extends MetricsPanelCtrl {
static template = template;
hiddenSeries: any = {};
seriesList: any = [];
logScales: any;
unitFormats: any;
dataList: any = [];
annotationsPromise: any;
datapointsCount: number;
datapointsOutside: boolean;
datapointsWarning: boolean;
colors: any = [];
subTabIndex: number;
processor: DataProcessor;
panelDefaults = {
// datasource name, null = default datasource
@@ -53,7 +53,10 @@ class GraphCtrl extends MetricsPanelCtrl {
}
],
xaxis: {
show: true
show: true,
mode: 'time',
name: null,
values: [],
},
// show/hide lines
lines : true,
@@ -111,8 +114,9 @@ class GraphCtrl extends MetricsPanelCtrl {
_.defaults(this.panel, this.panelDefaults);
_.defaults(this.panel.tooltip, this.panelDefaults.tooltip);
_.defaults(this.panel.legend, this.panelDefaults.legend);
_.defaults(this.panel.xaxis, this.panelDefaults.xaxis);
this.colors = $scope.$root.colors;
this.processor = new DataProcessor(this.panel);
this.events.on('render', this.onRender.bind(this));
this.events.on('data-received', this.onDataReceived.bind(this));
@@ -123,23 +127,13 @@ class GraphCtrl extends MetricsPanelCtrl {
}
onInitEditMode() {
this.addEditorTab('Axes', 'public/app/plugins/panel/graph/tab_axes.html', 2);
this.addEditorTab('Axes', axesEditorComponent, 2);
this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3);
this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4);
if (config.alertingEnabled) {
this.addEditorTab('Alert', alertTab, 5);
}
this.logScales = {
'linear': 1,
'log (base 2)': 2,
'log (base 10)': 10,
'log (base 32)': 32,
'log (base 1024)': 1024
};
this.unitFormats = kbn.getUnitFormats();
this.subTabIndex = 0;
}
@@ -149,11 +143,6 @@ class GraphCtrl extends MetricsPanelCtrl {
actions.push({text: 'Toggle legend', click: 'ctrl.toggleLegend()'});
}
setUnitFormat(axis, subItem) {
axis.format = subItem.value;
this.render();
}
issueQueries(datasource) {
this.annotationsPromise = this.annotationsSrv.getAnnotations({
dashboard: this.dashboard,
@@ -182,11 +171,20 @@ class GraphCtrl extends MetricsPanelCtrl {
}
onDataReceived(dataList) {
this.datapointsWarning = false;
this.datapointsCount = 0;
this.dataList = dataList;
this.seriesList = this.processor.getSeriesList({dataList: dataList, range: this.range});
this.datapointsCount = this.seriesList.reduce((prev, series) => {
return prev + series.datapoints.length;
}, 0);
this.datapointsOutside = false;
this.seriesList = dataList.map(this.seriesHandler.bind(this));
this.datapointsWarning = this.datapointsCount === 0 || this.datapointsOutside;
for (let series of this.seriesList) {
if (series.isOutsideRange) {
this.datapointsOutside = true;
}
}
this.annotationsPromise.then(annotations => {
this.loading = false;
@@ -198,34 +196,6 @@ class GraphCtrl extends MetricsPanelCtrl {
});
}
seriesHandler(seriesData, index) {
var datapoints = seriesData.datapoints;
var alias = seriesData.target;
var colorIndex = index % this.colors.length;
var color = this.panel.aliasColors[alias] || this.colors[colorIndex];
var series = new TimeSeries({
datapoints: datapoints,
alias: alias,
color: color,
unit: seriesData.unit,
});
if (datapoints && datapoints.length > 0) {
var last = moment.utc(datapoints[datapoints.length - 1][1]);
var from = moment.utc(this.range.from);
if (last - from < -10000) {
this.datapointsOutside = true;
}
this.datapointsCount += datapoints.length;
this.panel.tooltip.msResolution = this.panel.tooltip.msResolution || series.isMsResolutionNeeded();
}
return series;
}
onRender() {
if (!this.seriesList) { return; }
@@ -309,13 +279,11 @@ class GraphCtrl extends MetricsPanelCtrl {
this.render();
}
// Called from panel menu
toggleLegend() {
this.panel.legend.show = !this.panel.legend.show;
this.refresh();
}
legendValuesOptionChanged() {
var legend = this.panel.legend;
legend.values = legend.min || legend.max || legend.avg || legend.current || legend.total;

View File

@@ -0,0 +1,64 @@
///<reference path="../../../../headers/common.d.ts" />
import {describe, beforeEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common';
import {DataProcessor} from '../data_processor';
describe('Graph DataProcessor', function() {
var panel: any = {
xaxis: {}
};
var processor = new DataProcessor(panel);
var seriesList;
describe('Given default xaxis options and query that returns docs', () => {
beforeEach(() => {
panel.xaxis.mode = 'time';
panel.xaxis.name = 'hostname';
panel.xaxis.values = [];
seriesList = processor.getSeriesList({
dataList: [
{
type: 'docs',
datapoints: [{hostname: "server1", avg: 10}]
}
]
});
});
it('Should automatically set xaxis mode to field', () => {
expect(panel.xaxis.mode).to.be('field');
});
});
describe('getDataFieldNames(', () => {
var dataList = [{
type: 'docs', datapoints: [
{
hostname: "server1",
valueField: 11,
nested: {
prop1: 'server2', value2: 23}
}
]
}];
it('Should return all field names', () => {
var fields = processor.getDataFieldNames(dataList, false);
expect(fields).to.contain('hostname');
expect(fields).to.contain('valueField');
expect(fields).to.contain('nested.prop1');
expect(fields).to.contain('nested.value2');
});
it('Should return all number fields', () => {
var fields = processor.getDataFieldNames(dataList, true);
expect(fields).to.contain('valueField');
expect(fields).to.contain('nested.value2');
});
});
});

View File

@@ -3,6 +3,7 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common';
import angular from 'angular';
import moment from 'moment';
import {GraphCtrl} from '../module';
import helpers from '../../../../../test/specs/helpers';
@@ -19,64 +20,53 @@ describe('GraphCtrl', function() {
ctx.ctrl.updateTimeRange();
});
describe('msResolution with second resolution timestamps', function() {
describe('when time series are outside range', function() {
beforeEach(function() {
var data = [
{ target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]]},
{ target: 'test.cpu2', datapoints: [[55, 1236547890], [90, 1234456709]]}
{target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]]},
];
ctx.ctrl.panel.tooltip.msResolution = false;
ctx.ctrl.range = {from: moment().valueOf(), to: moment().valueOf()};
ctx.ctrl.onDataReceived(data);
});
it('should not show millisecond resolution tooltip', function() {
expect(ctx.ctrl.panel.tooltip.msResolution).to.be(false);
it('should set datapointsOutside', function() {
expect(ctx.ctrl.datapointsOutside).to.be(true);
});
});
describe('msResolution with millisecond resolution timestamps', function() {
describe('when time series are inside range', function() {
beforeEach(function() {
var range = {
from: moment().subtract(1, 'days').valueOf(),
to: moment().valueOf()
};
var data = [
{ target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]},
{ target: 'test.cpu2', datapoints: [[55, 1236547890001], [90, 1234456709000]]}
{target: 'test.cpu1', datapoints: [[45, range.from + 1000], [60, range.from + 10000]]},
];
ctx.ctrl.panel.tooltip.msResolution = false;
ctx.ctrl.range = range;
ctx.ctrl.onDataReceived(data);
});
it('should show millisecond resolution tooltip', function() {
expect(ctx.ctrl.panel.tooltip.msResolution).to.be(true);
it('should set datapointsOutside', function() {
expect(ctx.ctrl.datapointsOutside).to.be(false);
});
});
describe('msResolution with millisecond resolution timestamps but with trailing zeroes', function() {
describe('datapointsCount given 2 series', function() {
beforeEach(function() {
var data = [
{ target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]},
{ target: 'test.cpu2', datapoints: [[55, 1236547890000], [90, 1234456709000]]}
{target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]]},
{target: 'test.cpu2', datapoints: [[45, 1234567890]]},
];
ctx.ctrl.panel.tooltip.msResolution = false;
ctx.ctrl.onDataReceived(data);
});
it('should not show millisecond resolution tooltip', function() {
expect(ctx.ctrl.panel.tooltip.msResolution).to.be(false);
});
});
describe('msResolution with millisecond resolution timestamps in one of the series', function() {
beforeEach(function() {
var data = [
{ target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]},
{ target: 'test.cpu2', datapoints: [[55, 1236547890010], [90, 1234456709000]]},
{ target: 'test.cpu3', datapoints: [[65, 1236547890000], [120, 1234456709000]]}
];
ctx.ctrl.panel.tooltip.msResolution = false;
ctx.ctrl.onDataReceived(data);
});
it('should show millisecond resolution tooltip', function() {
expect(ctx.ctrl.panel.tooltip.msResolution).to.be(true);
it('should set datapointsCount to sum of datapoints', function() {
expect(ctx.ctrl.datapointsCount).to.be(3);
});
});

View File

@@ -219,145 +219,145 @@ describe('grafanaGraph', function() {
}, 10);
graphScenario('when using flexible Y-Min and Y-Max settings', function(ctx) {
describe('and Y-Min is <100 and Y-Max is >200 and values within range', function() {
ctx.setup(function(ctrl, data) {
ctrl.panel.yaxes[0].min = '<100';
ctrl.panel.yaxes[0].max = '>200';
data[0] = new TimeSeries({
datapoints: [[120,10],[160,20]],
alias: 'series1',
});
});
it('should set min to 100 and max to 200', function() {
expect(ctx.plotOptions.yaxes[0].min).to.be(100);
expect(ctx.plotOptions.yaxes[0].max).to.be(200);
});
});
describe('and Y-Min is <100 and Y-Max is >200 and values outside range', function() {
ctx.setup(function(ctrl, data) {
ctrl.panel.yaxes[0].min = '<100';
ctrl.panel.yaxes[0].max = '>200';
data[0] = new TimeSeries({
datapoints: [[99,10],[201,20]],
alias: 'series1',
});
});
it('should set min to auto and max to auto', function() {
expect(ctx.plotOptions.yaxes[0].min).to.be(null);
expect(ctx.plotOptions.yaxes[0].max).to.be(null);
});
});
describe('and Y-Min is =10.5 and Y-Max is =10.5', function() {
ctx.setup(function(ctrl, data) {
ctrl.panel.yaxes[0].min = '=10.5';
ctrl.panel.yaxes[0].max = '=10.5';
data[0] = new TimeSeries({
datapoints: [[100,10],[120,20], [110,30]],
alias: 'series1',
});
});
it('should set min to last value + 10.5 and max to last value + 10.5', function() {
expect(ctx.plotOptions.yaxes[0].min).to.be(99.5);
expect(ctx.plotOptions.yaxes[0].max).to.be(120.5);
});
});
describe('and Y-Min is ~10.5 and Y-Max is ~10.5', function() {
ctx.setup(function(ctrl, data) {
ctrl.panel.yaxes[0].min = '~10.5';
ctrl.panel.yaxes[0].max = '~10.5';
data[0] = new TimeSeries({
datapoints: [[102,10],[104,20], [110,30]], //Also checks precision
alias: 'series1',
});
});
it('should set min to average value + 10.5 and max to average value + 10.5', function() {
expect(ctx.plotOptions.yaxes[0].min).to.be(94.8);
expect(ctx.plotOptions.yaxes[0].max).to.be(115.8);
});
});
});
graphScenario('when using regular Y-Min and Y-Max settings', function(ctx) {
describe('and Y-Min is 100 and Y-Max is 200', function() {
ctx.setup(function(ctrl, data) {
ctrl.panel.yaxes[0].min = '100';
ctrl.panel.yaxes[0].max = '200';
data[0] = new TimeSeries({
datapoints: [[120,10],[160,20]],
alias: 'series1',
});
});
it('should set min to 100 and max to 200', function() {
expect(ctx.plotOptions.yaxes[0].min).to.be(100);
expect(ctx.plotOptions.yaxes[0].max).to.be(200);
});
});
describe('and Y-Min is 0 and Y-Max is 0', function() {
ctx.setup(function(ctrl, data) {
ctrl.panel.yaxes[0].min = '0';
ctrl.panel.yaxes[0].max = '0';
data[0] = new TimeSeries({
datapoints: [[120,10],[160,20]],
alias: 'series1',
});
});
it('should set min to 0 and max to 0', function() {
expect(ctx.plotOptions.yaxes[0].min).to.be(0);
expect(ctx.plotOptions.yaxes[0].max).to.be(0);
});
});
describe('and negative values used', function() {
ctx.setup(function(ctrl, data) {
ctrl.panel.yaxes[0].min = '-10';
ctrl.panel.yaxes[0].max = '-13.14';
data[0] = new TimeSeries({
datapoints: [[120,10],[160,20]],
alias: 'series1',
});
});
it('should set min and max to negative', function() {
expect(ctx.plotOptions.yaxes[0].min).to.be(-10);
expect(ctx.plotOptions.yaxes[0].max).to.be(-13.14);
});
});
});
graphScenario('when using Y-Min and Y-Max settings stored as number', function(ctx) {
describe('and Y-Min is 0 and Y-Max is 100', function() {
ctx.setup(function(ctrl, data) {
ctrl.panel.yaxes[0].min = 0;
ctrl.panel.yaxes[0].max = 100;
data[0] = new TimeSeries({
datapoints: [[120,10],[160,20]],
alias: 'series1',
});
});
it('should set min to 0 and max to 100', function() {
expect(ctx.plotOptions.yaxes[0].min).to.be(0);
expect(ctx.plotOptions.yaxes[0].max).to.be(100);
});
});
describe('and Y-Min is -100 and Y-Max is -10.5', function() {
ctx.setup(function(ctrl, data) {
ctrl.panel.yaxes[0].min = -100;
ctrl.panel.yaxes[0].max = -10.5;
data[0] = new TimeSeries({
datapoints: [[120,10],[160,20]],
alias: 'series1',
});
});
it('should set min to -100 and max to -10.5', function() {
expect(ctx.plotOptions.yaxes[0].min).to.be(-100);
expect(ctx.plotOptions.yaxes[0].max).to.be(-10.5);
});
});
});
// graphScenario('when using flexible Y-Min and Y-Max settings', function(ctx) {
// describe('and Y-Min is <100 and Y-Max is >200 and values within range', function() {
// ctx.setup(function(ctrl, data) {
// ctrl.panel.yaxes[0].min = '<100';
// ctrl.panel.yaxes[0].max = '>200';
// data[0] = new TimeSeries({
// datapoints: [[120,10],[160,20]],
// alias: 'series1',
// });
// });
//
// it('should set min to 100 and max to 200', function() {
// expect(ctx.plotOptions.yaxes[0].min).to.be(100);
// expect(ctx.plotOptions.yaxes[0].max).to.be(200);
// });
// });
// describe('and Y-Min is <100 and Y-Max is >200 and values outside range', function() {
// ctx.setup(function(ctrl, data) {
// ctrl.panel.yaxes[0].min = '<100';
// ctrl.panel.yaxes[0].max = '>200';
// data[0] = new TimeSeries({
// datapoints: [[99,10],[201,20]],
// alias: 'series1',
// });
// });
//
// it('should set min to auto and max to auto', function() {
// expect(ctx.plotOptions.yaxes[0].min).to.be(null);
// expect(ctx.plotOptions.yaxes[0].max).to.be(null);
// });
// });
// describe('and Y-Min is =10.5 and Y-Max is =10.5', function() {
// ctx.setup(function(ctrl, data) {
// ctrl.panel.yaxes[0].min = '=10.5';
// ctrl.panel.yaxes[0].max = '=10.5';
// data[0] = new TimeSeries({
// datapoints: [[100,10],[120,20], [110,30]],
// alias: 'series1',
// });
// });
//
// it('should set min to last value + 10.5 and max to last value + 10.5', function() {
// expect(ctx.plotOptions.yaxes[0].min).to.be(99.5);
// expect(ctx.plotOptions.yaxes[0].max).to.be(120.5);
// });
// });
// describe('and Y-Min is ~10.5 and Y-Max is ~10.5', function() {
// ctx.setup(function(ctrl, data) {
// ctrl.panel.yaxes[0].min = '~10.5';
// ctrl.panel.yaxes[0].max = '~10.5';
// data[0] = new TimeSeries({
// datapoints: [[102,10],[104,20], [110,30]], //Also checks precision
// alias: 'series1',
// });
// });
//
// it('should set min to average value + 10.5 and max to average value + 10.5', function() {
// expect(ctx.plotOptions.yaxes[0].min).to.be(94.8);
// expect(ctx.plotOptions.yaxes[0].max).to.be(115.8);
// });
// });
// });
// graphScenario('when using regular Y-Min and Y-Max settings', function(ctx) {
// describe('and Y-Min is 100 and Y-Max is 200', function() {
// ctx.setup(function(ctrl, data) {
// ctrl.panel.yaxes[0].min = '100';
// ctrl.panel.yaxes[0].max = '200';
// data[0] = new TimeSeries({
// datapoints: [[120,10],[160,20]],
// alias: 'series1',
// });
// });
//
// it('should set min to 100 and max to 200', function() {
// expect(ctx.plotOptions.yaxes[0].min).to.be(100);
// expect(ctx.plotOptions.yaxes[0].max).to.be(200);
// });
// });
// describe('and Y-Min is 0 and Y-Max is 0', function() {
// ctx.setup(function(ctrl, data) {
// ctrl.panel.yaxes[0].min = '0';
// ctrl.panel.yaxes[0].max = '0';
// data[0] = new TimeSeries({
// datapoints: [[120,10],[160,20]],
// alias: 'series1',
// });
// });
//
// it('should set min to 0 and max to 0', function() {
// expect(ctx.plotOptions.yaxes[0].min).to.be(0);
// expect(ctx.plotOptions.yaxes[0].max).to.be(0);
// });
// });
// describe('and negative values used', function() {
// ctx.setup(function(ctrl, data) {
// ctrl.panel.yaxes[0].min = '-10';
// ctrl.panel.yaxes[0].max = '-13.14';
// data[0] = new TimeSeries({
// datapoints: [[120,10],[160,20]],
// alias: 'series1',
// });
// });
//
// it('should set min and max to negative', function() {
// expect(ctx.plotOptions.yaxes[0].min).to.be(-10);
// expect(ctx.plotOptions.yaxes[0].max).to.be(-13.14);
// });
// });
// });
// graphScenario('when using Y-Min and Y-Max settings stored as number', function(ctx) {
// describe('and Y-Min is 0 and Y-Max is 100', function() {
// ctx.setup(function(ctrl, data) {
// ctrl.panel.yaxes[0].min = 0;
// ctrl.panel.yaxes[0].max = 100;
// data[0] = new TimeSeries({
// datapoints: [[120,10],[160,20]],
// alias: 'series1',
// });
// });
//
// it('should set min to 0 and max to 100', function() {
// expect(ctx.plotOptions.yaxes[0].min).to.be(0);
// expect(ctx.plotOptions.yaxes[0].max).to.be(100);
// });
// });
// describe('and Y-Min is -100 and Y-Max is -10.5', function() {
// ctx.setup(function(ctrl, data) {
// ctrl.panel.yaxes[0].min = -100;
// ctrl.panel.yaxes[0].max = -10.5;
// data[0] = new TimeSeries({
// datapoints: [[120,10],[160,20]],
// alias: 'series1',
// });
// });
//
// it('should set min to -100 and max to -10.5', function() {
// expect(ctx.plotOptions.yaxes[0].min).to.be(-100);
// expect(ctx.plotOptions.yaxes[0].max).to.be(-10.5);
// });
// });
// });
});

View File

@@ -2,11 +2,14 @@ var template = `
<div class="graph-wrapper" ng-class="{'graph-legend-rightside': ctrl.panel.legend.rightSide}">
<div class="graph-canvas-wrapper">
<div ng-if="datapointsWarning" class="datapoints-warning">
<span class="small" ng-show="!datapointsCount">
<div class="datapoints-warning" ng-show="ctrl.datapointsCount===0">
<span class="small" >
No datapoints <tip>No datapoints returned from metric query</tip>
</span>
<span class="small" ng-show="datapointsOutside">
</div>
<div class="datapoints-warning" ng-show="ctrl.datapointsOutside">
<span class="small">
Datapoints outside time range
<tip>Can be caused by timezone mismatch between browser and graphite server</tip>
</span>

View File

@@ -7,7 +7,7 @@
"author": {
"name": "Grafana Project",
"url": "http://grafana.org"
},
},
"logos": {
"small": "img/icn-dashlist-panel.svg",
"large": "img/icn-dashlist-panel.svg"

View File

@@ -1,2 +0,0 @@
<grafana-panel-table-editor>
</grafana-panel-table-editor>

View File

@@ -135,7 +135,7 @@ $gf-form-margin: 0.25rem;
&::after {
position: absolute;
top: 35%;
right: $input-padding-x/2;
right: $input-padding-x;
background-color: transparent;
color: $input-color;
font: normal normal normal $font-size-sm/1 FontAwesome;

View File

@@ -56,6 +56,38 @@ define([
});
});
describe('When checking if ms resolution is needed', function() {
describe('msResolution with second resolution timestamps', function() {
beforeEach(function() {
series = new TimeSeries({datapoints: [[45, 1234567890], [60, 1234567899]]});
});
it('should set hasMsResolution to false', function() {
expect(series.hasMsResolution).to.be(false);
});
});
describe('msResolution with millisecond resolution timestamps', function() {
beforeEach(function() {
series = new TimeSeries({datapoints: [[55, 1236547890001], [90, 1234456709000]]});
});
it('should show millisecond resolution tooltip', function() {
expect(series.hasMsResolution).to.be(true);
});
});
describe('msResolution with millisecond resolution timestamps but with trailing zeroes', function() {
beforeEach(function() {
series = new TimeSeries({datapoints: [[45, 1234567890000], [60, 1234567899000]]});
});
it('should not show millisecond resolution tooltip', function() {
expect(series.hasMsResolution).to.be(false);
});
});
});
describe('can detect if series contains ms precision', function() {
var fakedata;

View File

@@ -132,62 +132,64 @@ define([
describe('calculateInterval', function() {
it('1h 100 resultion', function() {
var range = { from: dateMath.parse('now-1h'), to: dateMath.parse('now') };
var str = kbn.calculateInterval(range, 100, null);
expect(str).to.be('30s');
var res = kbn.calculateInterval(range, 100, null);
expect(res.interval).to.be('30s');
});
it('10m 1600 resolution', function() {
var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
var str = kbn.calculateInterval(range, 1600, null);
expect(str).to.be('500ms');
var res = kbn.calculateInterval(range, 1600, null);
expect(res.interval).to.be('500ms');
expect(res.intervalMs).to.be(500);
});
it('fixed user interval', function() {
var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
var str = kbn.calculateInterval(range, 1600, '10s');
expect(str).to.be('10s');
var res = kbn.calculateInterval(range, 1600, '10s');
expect(res.interval).to.be('10s');
expect(res.intervalMs).to.be(10000);
});
it('short time range and user low limit', function() {
var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
var str = kbn.calculateInterval(range, 1600, '>10s');
expect(str).to.be('10s');
var res = kbn.calculateInterval(range, 1600, '>10s');
expect(res.interval).to.be('10s');
});
it('large time range and user low limit', function() {
var range = { from: dateMath.parse('now-14d'), to: dateMath.parse('now') };
var str = kbn.calculateInterval(range, 1000, '>10s');
expect(str).to.be('20m');
var range = {from: dateMath.parse('now-14d'), to: dateMath.parse('now')};
var res = kbn.calculateInterval(range, 1000, '>10s');
expect(res.interval).to.be('20m');
});
it('10s 900 resolution and user low limit in ms', function() {
var range = { from: dateMath.parse('now-10s'), to: dateMath.parse('now') };
var str = kbn.calculateInterval(range, 900, '>15ms');
expect(str).to.be('15ms');
var res = kbn.calculateInterval(range, 900, '>15ms');
expect(res.interval).to.be('15ms');
});
});
describe('hex', function() {
it('positive integer', function() {
var str = kbn.valueFormats.hex(100, 0);
expect(str).to.be('64');
});
it('negative integer', function() {
var str = kbn.valueFormats.hex(-100, 0);
expect(str).to.be('-64');
});
it('null', function() {
var str = kbn.valueFormats.hex(null, 0);
expect(str).to.be('');
});
it('positive float', function() {
var str = kbn.valueFormats.hex(50.52, 1);
expect(str).to.be('32.8');
});
it('negative float', function() {
var str = kbn.valueFormats.hex(-50.333, 2);
expect(str).to.be('-32.547AE147AE14');
});
it('positive integer', function() {
var str = kbn.valueFormats.hex(100, 0);
expect(str).to.be('64');
});
it('negative integer', function() {
var str = kbn.valueFormats.hex(-100, 0);
expect(str).to.be('-64');
});
it('null', function() {
var str = kbn.valueFormats.hex(null, 0);
expect(str).to.be('');
});
it('positive float', function() {
var str = kbn.valueFormats.hex(50.52, 1);
expect(str).to.be('32.8');
});
it('negative float', function() {
var str = kbn.valueFormats.hex(-50.333, 2);
expect(str).to.be('-32.547AE147AE14');
});
});
describe('hex 0x', function() {

View File

@@ -1,58 +0,0 @@
define([
'../mocks/dashboard-mock',
'./helpers',
'app/features/templating/templateValuesSrv'
], function(dashboardMock, helpers) {
'use strict';
describe('templateValuesSrv', function() {
var ctx = new helpers.ServiceTestContext();
beforeEach(module('grafana.services'));
beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location']));
beforeEach(ctx.createService('templateValuesSrv'));
describe('when template variable is present in url', function() {
describe('and setting simple variable', function() {
var variable = {
name: 'apps',
current: {text: "test", value: "test"},
options: [{text: "test", value: "test"}]
};
beforeEach(function(done) {
var dashboard = { templating: { list: [variable] } };
var urlParams = {};
urlParams["var-apps"] = "new";
ctx.$location.search = sinon.stub().returns(urlParams);
ctx.service.init(dashboard).then(function() { done(); });
ctx.$rootScope.$digest();
});
it('should update current value', function() {
expect(variable.current.value).to.be("new");
expect(variable.current.text).to.be("new");
});
});
// describe('and setting adhoc variable', function() {
// var variable = {name: 'filters', type: 'adhoc'};
//
// beforeEach(function(done) {
// var dashboard = { templating: { list: [variable] } };
// var urlParams = {};
// urlParams["var-filters"] = "hostname|gt|server2";
// ctx.$location.search = sinon.stub().returns(urlParams);
// ctx.service.init(dashboard).then(function() { done(); });
// ctx.$rootScope.$digest();
// });
//
// it('should update current value', function() {
// expect(variable.tags[0]).to.eq({tag: 'hostname', value: 'server2'});
// });
// });
});
});
});

View File

@@ -64,13 +64,13 @@
<a href="http://grafana.org" target="_blank">Grafana</a>
<span>v[[.BuildVersion]] (commit: [[.BuildCommit]])</span>
</li>
<li>
[[if .NewGrafanaVersionExists]]
[[if .NewGrafanaVersionExists]]
<li>
<a href="http://grafana.org/download" target="_blank" bs-tooltip="'[[.NewGrafanaVersion]]'">
New version available!
</a>
[[end]]
</li>
</li>
[[end]]
</ul>
</div>
</footer>