Merge branch 'adhoc-filters'

This commit is contained in:
Torkel Ödegaard 2016-09-20 17:34:57 +02:00
commit 0906312046
46 changed files with 3310 additions and 1998 deletions

View File

@ -9,6 +9,7 @@ export class User {
isGrafanaAdmin: any;
isSignedIn: any;
orgRole: any;
timezone: string;
constructor() {
if (config.bootData.user) {

View File

@ -9,6 +9,10 @@ function($, _, moment) {
var kbn = {};
kbn.valueFormats = {};
kbn.regexEscape = function(value) {
return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&');
};
///// HELPER FUNCTIONS /////
kbn.round_interval = function(interval) {

View File

@ -2,7 +2,7 @@ define([
'./panellinks/module',
'./dashlinks/module',
'./annotations/annotations_srv',
'./templating/templateSrv',
'./templating/all',
'./dashboard/all',
'./playlist/all',
'./snapshot/all',

View File

@ -0,0 +1,171 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import angular from 'angular';
import coreModule from 'app/core/core_module';
export class AdHocFiltersCtrl {
segments: any;
variable: any;
removeTagFilterSegment: any;
/** @ngInject */
constructor(private uiSegmentSrv, private datasourceSrv, private $q, private templateSrv, private $rootScope) {
this.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove filter --'});
this.buildSegmentModel();
}
buildSegmentModel() {
this.segments = [];
if (this.variable.value && !_.isArray(this.variable.value)) {
}
for (let tag of this.variable.filters) {
if (this.segments.length > 0) {
this.segments.push(this.uiSegmentSrv.newCondition('AND'));
}
if (tag.key !== undefined && tag.value !== undefined) {
this.segments.push(this.uiSegmentSrv.newKey(tag.key));
this.segments.push(this.uiSegmentSrv.newOperator(tag.operator));
this.segments.push(this.uiSegmentSrv.newKeyValue(tag.value));
}
}
this.segments.push(this.uiSegmentSrv.newPlusButton());
}
getOptions(segment, index) {
if (segment.type === 'operator') {
return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<', '>', '=~', '!~']));
}
if (segment.type === 'condition') {
return this.$q.when([this.uiSegmentSrv.newSegment('AND')]);
}
return this.datasourceSrv.get(this.variable.datasource).then(ds => {
var options: any = {};
var promise = null;
if (segment.type !== 'value') {
promise = ds.getTagKeys();
} else {
options.key = this.segments[index-2].value;
promise = ds.getTagValues(options);
}
return promise.then(results => {
results = _.map(results, segment => {
return this.uiSegmentSrv.newSegment({value: segment.text});
});
// add remove option for keys
if (segment.type === 'key') {
results.splice(0, 0, angular.copy(this.removeTagFilterSegment));
}
return results;
});
});
}
segmentChanged(segment, index) {
this.segments[index] = segment;
// handle remove tag condition
if (segment.value === this.removeTagFilterSegment.value) {
this.segments.splice(index, 3);
if (this.segments.length === 0) {
this.segments.push(this.uiSegmentSrv.newPlusButton());
} else if (this.segments.length > 2) {
this.segments.splice(Math.max(index-1, 0), 1);
if (this.segments[this.segments.length-1].type !== 'plus-button') {
this.segments.push(this.uiSegmentSrv.newPlusButton());
}
}
} else {
if (segment.type === 'plus-button') {
if (index > 2) {
this.segments.splice(index, 0, this.uiSegmentSrv.newCondition('AND'));
}
this.segments.push(this.uiSegmentSrv.newOperator('='));
this.segments.push(this.uiSegmentSrv.newFake('select tag value', 'value', 'query-segment-value'));
segment.type = 'key';
segment.cssClass = 'query-segment-key';
}
if ((index+1) === this.segments.length) {
this.segments.push(this.uiSegmentSrv.newPlusButton());
}
}
this.updateVariableModel();
}
updateVariableModel() {
var filters = [];
var filterIndex = -1;
var operator = "";
var hasFakes = false;
this.segments.forEach(segment => {
if (segment.type === 'value' && segment.fake) {
hasFakes = true;
return;
}
switch (segment.type) {
case 'key': {
filters.push({key: segment.value});
filterIndex += 1;
break;
}
case 'value': {
filters[filterIndex].value = segment.value;
break;
}
case 'operator': {
filters[filterIndex].operator = segment.value;
break;
}
case 'condition': {
filters[filterIndex].condition = segment.value;
break;
}
}
});
if (hasFakes) {
return;
}
this.variable.setFilters(filters);
this.$rootScope.$emit('template-variable-value-updated');
this.$rootScope.$broadcast('refresh');
}
}
var template = `
<div class="gf-form-inline">
<div class="gf-form" ng-repeat="segment in ctrl.segments">
<metric-segment segment="segment" get-options="ctrl.getOptions(segment, $index)"
on-change="ctrl.segmentChanged(segment, $index)"></metric-segment>
</div>
</div>
`;
export function adHocFiltersComponent() {
return {
restrict: 'E',
template: template,
controller: AdHocFiltersCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
variable: "="
}
};
}
coreModule.directive('adHocFilters', adHocFiltersComponent);

View File

@ -7,7 +7,7 @@ define([
'./rowCtrl',
'./shareModalCtrl',
'./shareSnapshotCtrl',
'./dashboardSrv',
'./dashboard_srv',
'./keybindings',
'./viewStateSrv',
'./timeSrv',
@ -20,4 +20,5 @@ define([
'./import/dash_import',
'./export/export_modal',
'./dash_list_ctrl',
'./ad_hoc_filters',
], function () {});

View File

@ -1,552 +0,0 @@
define([
'angular',
'jquery',
'lodash',
'moment',
],
function (angular, $, _, moment) {
'use strict';
var module = angular.module('grafana.services');
module.factory('dashboardSrv', function(contextSrv) {
function DashboardModel (data, meta) {
if (!data) {
data = {};
}
this.id = data.id || null;
this.title = data.title || 'No Title';
this.autoUpdate = data.autoUpdate;
this.description = data.description;
this.tags = data.tags || [];
this.style = data.style || "dark";
this.timezone = data.timezone || '';
this.editable = data.editable !== false;
this.hideControls = data.hideControls || false;
this.sharedCrosshair = data.sharedCrosshair || false;
this.rows = data.rows || [];
this.time = data.time || { from: 'now-6h', to: 'now' };
this.timepicker = data.timepicker || {};
this.templating = this._ensureListExist(data.templating);
this.annotations = this._ensureListExist(data.annotations);
this.refresh = data.refresh;
this.snapshot = data.snapshot;
this.schemaVersion = data.schemaVersion || 0;
this.version = data.version || 0;
this.links = data.links || [];
this.gnetId = data.gnetId || null;
this._updateSchema(data);
this._initMeta(meta);
}
var p = DashboardModel.prototype;
p._initMeta = function(meta) {
meta = meta || {};
meta.canShare = meta.canShare !== false;
meta.canSave = meta.canSave !== false;
meta.canStar = meta.canStar !== false;
meta.canEdit = meta.canEdit !== false;
if (!this.editable) {
meta.canEdit = false;
meta.canDelete = false;
meta.canSave = false;
this.hideControls = true;
}
this.meta = meta;
};
// cleans meta data and other non peristent state
p.getSaveModelClone = function() {
var copy = $.extend(true, {}, this);
delete copy.meta;
return copy;
};
p._ensureListExist = function (data) {
if (!data) { data = {}; }
if (!data.list) { data.list = []; }
return data;
};
p.getNextPanelId = function() {
var i, j, row, panel, max = 0;
for (i = 0; i < this.rows.length; i++) {
row = this.rows[i];
for (j = 0; j < row.panels.length; j++) {
panel = row.panels[j];
if (panel.id > max) { max = panel.id; }
}
}
return max + 1;
};
p.forEachPanel = function(callback) {
var i, j, row;
for (i = 0; i < this.rows.length; i++) {
row = this.rows[i];
for (j = 0; j < row.panels.length; j++) {
callback(row.panels[j], j, row, i);
}
}
};
p.getPanelById = function(id) {
for (var i = 0; i < this.rows.length; i++) {
var row = this.rows[i];
for (var j = 0; j < row.panels.length; j++) {
var panel = row.panels[j];
if (panel.id === id) {
return panel;
}
}
}
return null;
};
p.rowSpan = function(row) {
return _.reduce(row.panels, function(p,v) {
return p + v.span;
},0);
};
p.addPanel = function(panel, row) {
var rowSpan = this.rowSpan(row);
var panelCount = row.panels.length;
var space = (12 - rowSpan) - panel.span;
panel.id = this.getNextPanelId();
// try to make room of there is no space left
if (space <= 0) {
if (panelCount === 1) {
row.panels[0].span = 6;
panel.span = 6;
}
else if (panelCount === 2) {
row.panels[0].span = 4;
row.panels[1].span = 4;
panel.span = 4;
}
}
row.panels.push(panel);
};
p.isSubmenuFeaturesEnabled = function() {
var visableTemplates = _.filter(this.templating.list, function(template) {
return template.hideVariable === undefined || template.hideVariable === false;
});
return visableTemplates.length > 0 || this.annotations.list.length > 0 || this.links.length > 0;
};
p.getPanelInfoById = function(panelId) {
var result = {};
_.each(this.rows, function(row) {
_.each(row.panels, function(panel, index) {
if (panel.id === panelId) {
result.panel = panel;
result.row = row;
result.index = index;
}
});
});
if (!result.panel) {
return null;
}
return result;
};
p.duplicatePanel = function(panel, row) {
var rowIndex = _.indexOf(this.rows, row);
var newPanel = angular.copy(panel);
newPanel.id = this.getNextPanelId();
delete newPanel.repeat;
delete newPanel.repeatIteration;
delete newPanel.repeatPanelId;
delete newPanel.scopedVars;
var currentRow = this.rows[rowIndex];
currentRow.panels.push(newPanel);
return newPanel;
};
p.formatDate = function(date, format) {
date = moment.isMoment(date) ? date : moment(date);
format = format || 'YYYY-MM-DD HH:mm:ss';
this.timezone = this.getTimezone();
return this.timezone === 'browser' ?
moment(date).format(format) :
moment.utc(date).format(format);
};
p.getRelativeTime = function(date) {
date = moment.isMoment(date) ? date : moment(date);
return this.timezone === 'browser' ?
moment(date).fromNow() :
moment.utc(date).fromNow();
};
p.getNextQueryLetter = function(panel) {
var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
return _.find(letters, function(refId) {
return _.every(panel.targets, function(other) {
return other.refId !== refId;
});
});
};
p.isTimezoneUtc = function() {
return this.getTimezone() === 'utc';
};
p.getTimezone = function() {
return this.timezone ? this.timezone : contextSrv.user.timezone;
};
p._updateSchema = function(old) {
var i, j, k;
var oldVersion = this.schemaVersion;
var panelUpgrades = [];
this.schemaVersion = 13;
if (oldVersion === this.schemaVersion) {
return;
}
// version 2 schema changes
if (oldVersion < 2) {
if (old.services) {
if (old.services.filter) {
this.time = old.services.filter.time;
this.templating.list = old.services.filter.list || [];
}
delete this.services;
}
panelUpgrades.push(function(panel) {
// rename panel type
if (panel.type === 'graphite') {
panel.type = 'graph';
}
if (panel.type !== 'graph') {
return;
}
if (_.isBoolean(panel.legend)) { panel.legend = { show: panel.legend }; }
if (panel.grid) {
if (panel.grid.min) {
panel.grid.leftMin = panel.grid.min;
delete panel.grid.min;
}
if (panel.grid.max) {
panel.grid.leftMax = panel.grid.max;
delete panel.grid.max;
}
}
if (panel.y_format) {
panel.y_formats[0] = panel.y_format;
delete panel.y_format;
}
if (panel.y2_format) {
panel.y_formats[1] = panel.y2_format;
delete panel.y2_format;
}
});
}
// schema version 3 changes
if (oldVersion < 3) {
// ensure panel ids
var maxId = this.getNextPanelId();
panelUpgrades.push(function(panel) {
if (!panel.id) {
panel.id = maxId;
maxId += 1;
}
});
}
// schema version 4 changes
if (oldVersion < 4) {
// move aliasYAxis changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'graph') { return; }
_.each(panel.aliasYAxis, function(value, key) {
panel.seriesOverrides = [{ alias: key, yaxis: value }];
});
delete panel.aliasYAxis;
});
}
if (oldVersion < 6) {
// move pulldowns to new schema
var annotations = _.find(old.pulldowns, { type: 'annotations' });
if (annotations) {
this.annotations = {
list: annotations.annotations || [],
};
}
// update template variables
for (i = 0 ; i < this.templating.list.length; i++) {
var variable = this.templating.list[i];
if (variable.datasource === void 0) { variable.datasource = null; }
if (variable.type === 'filter') { variable.type = 'query'; }
if (variable.type === void 0) { variable.type = 'query'; }
if (variable.allFormat === void 0) { variable.allFormat = 'glob'; }
}
}
if (oldVersion < 7) {
if (old.nav && old.nav.length) {
this.timepicker = old.nav[0];
delete this.nav;
}
// ensure query refIds
panelUpgrades.push(function(panel) {
_.each(panel.targets, function(target) {
if (!target.refId) {
target.refId = this.getNextQueryLetter(panel);
}
}.bind(this));
});
}
if (oldVersion < 8) {
panelUpgrades.push(function(panel) {
_.each(panel.targets, function(target) {
// update old influxdb query schema
if (target.fields && target.tags && target.groupBy) {
if (target.rawQuery) {
delete target.fields;
delete target.fill;
} else {
target.select = _.map(target.fields, function(field) {
var parts = [];
parts.push({type: 'field', params: [field.name]});
parts.push({type: field.func, params: []});
if (field.mathExpr) {
parts.push({type: 'math', params: [field.mathExpr]});
}
if (field.asExpr) {
parts.push({type: 'alias', params: [field.asExpr]});
}
return parts;
});
delete target.fields;
_.each(target.groupBy, function(part) {
if (part.type === 'time' && part.interval) {
part.params = [part.interval];
delete part.interval;
}
if (part.type === 'tag' && part.key) {
part.params = [part.key];
delete part.key;
}
});
if (target.fill) {
target.groupBy.push({type: 'fill', params: [target.fill]});
delete target.fill;
}
}
}
});
});
}
// schema version 9 changes
if (oldVersion < 9) {
// move aliasYAxis changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'singlestat' && panel.thresholds !== "") { return; }
if (panel.thresholds) {
var k = panel.thresholds.split(",");
if (k.length >= 3) {
k.shift();
panel.thresholds = k.join(",");
}
}
});
}
// schema version 10 changes
if (oldVersion < 10) {
// move aliasYAxis changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'table') { return; }
_.each(panel.styles, function(style) {
if (style.thresholds && style.thresholds.length >= 3) {
var k = style.thresholds;
k.shift();
style.thresholds = k;
}
});
});
}
if (oldVersion < 12) {
// update template variables
_.each(this.templating.list, function(templateVariable) {
if (templateVariable.refresh) { templateVariable.refresh = 1; }
if (!templateVariable.refresh) { templateVariable.refresh = 0; }
if (templateVariable.hideVariable) {
templateVariable.hide = 2;
} else if (templateVariable.hideLabel) {
templateVariable.hide = 1;
} else {
templateVariable.hide = 0;
}
});
}
if (oldVersion < 12) {
// update graph yaxes changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'graph') { return; }
if (!panel.grid) { return; }
if (!panel.yaxes) {
panel.yaxes = [
{
show: panel['y-axis'],
min: panel.grid.leftMin,
max: panel.grid.leftMax,
logBase: panel.grid.leftLogBase,
format: panel.y_formats[0],
label: panel.leftYAxisLabel,
},
{
show: panel['y-axis'],
min: panel.grid.rightMin,
max: panel.grid.rightMax,
logBase: panel.grid.rightLogBase,
format: panel.y_formats[1],
label: panel.rightYAxisLabel,
}
];
panel.xaxis = {
show: panel['x-axis'],
};
delete panel.grid.leftMin;
delete panel.grid.leftMax;
delete panel.grid.leftLogBase;
delete panel.grid.rightMin;
delete panel.grid.rightMax;
delete panel.grid.rightLogBase;
delete panel.y_formats;
delete panel.leftYAxisLabel;
delete panel.rightYAxisLabel;
delete panel['y-axis'];
delete panel['x-axis'];
}
});
}
if (oldVersion < 13) {
// update graph yaxes changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'graph') { return; }
panel.thresholds = [];
var t1 = {}, t2 = {};
if (panel.grid.threshold1 !== null) {
t1.value = panel.grid.threshold1;
if (panel.grid.thresholdLine) {
t1.line = true;
t1.lineColor = panel.grid.threshold1Color;
} else {
t1.fill = true;
t1.fillColor = panel.grid.threshold1Color;
}
}
if (panel.grid.threshold2 !== null) {
t2.value = panel.grid.threshold2;
if (panel.grid.thresholdLine) {
t2.line = true;
t2.lineColor = panel.grid.threshold2Color;
} else {
t2.fill = true;
t2.fillColor = panel.grid.threshold2Color;
}
}
if (_.isNumber(t1.value)) {
if (_.isNumber(t2.value)) {
if (t1.value > t2.value) {
t1.op = t2.op = '<';
panel.thresholds.push(t2);
panel.thresholds.push(t1);
} else {
t1.op = t2.op = '>';
panel.thresholds.push(t2);
panel.thresholds.push(t1);
}
} else {
t1.op = '>';
panel.thresholds.push(t1);
}
}
delete panel.grid.threshold1;
delete panel.grid.threshold1Color;
delete panel.grid.threshold2;
delete panel.grid.threshold2Color;
delete panel.grid.thresholdLine;
});
}
if (panelUpgrades.length === 0) {
return;
}
for (i = 0; i < this.rows.length; i++) {
var row = this.rows[i];
for (j = 0; j < row.panels.length; j++) {
for (k = 0; k < panelUpgrades.length; k++) {
panelUpgrades[k].call(this, row.panels[j]);
}
}
}
};
return {
create: function(dashboard, meta) {
return new DashboardModel(dashboard, meta);
},
setCurrent: function(dashboard) {
this.currentDashboard = dashboard;
},
getCurrent: function() {
return this.currentDashboard;
},
};
});
});

View File

@ -15,7 +15,7 @@ export class DashboardCtrl {
private $rootScope,
dashboardKeybindings,
timeSrv,
templateValuesSrv,
variableSrv,
dashboardSrv,
unsavedChangesSrv,
dynamicDashboardSrv,
@ -46,7 +46,7 @@ export class DashboardCtrl {
// template values service needs to initialize completely before
// the rest of the dashboard can load
templateValuesSrv.init(dashboard)
variableSrv.init(dashboard)
// template values failes are non fatal
.catch($scope.onInitFailed.bind(this, 'Templating init failed', false))
// continue
@ -87,7 +87,6 @@ export class DashboardCtrl {
};
$scope.templateVariableUpdated = function() {
console.log('dynamic update');
dynamicDashboardSrv.update($scope.dashboard);
};

View File

@ -0,0 +1,590 @@
///<reference path="../../headers/common.d.ts" />
import config from 'app/core/config';
import angular from 'angular';
import moment from 'moment';
import _ from 'lodash';
import $ from 'jquery';
import {Emitter} from 'app/core/core';
import {contextSrv} from 'app/core/services/context_srv';
import coreModule from 'app/core/core_module';
export class DashboardModel {
id: any;
title: any;
autoUpdate: any;
description: any;
tags: any;
style: any;
timezone: any;
editable: any;
hideControls: any;
sharedCrosshair: any;
rows: any;
time: any;
timepicker: any;
templating: any;
annotations: any;
refresh: any;
snapshot: any;
schemaVersion: number;
version: number;
links: any;
gnetId: any;
meta: any;
events: any;
constructor(data, meta) {
if (!data) {
data = {};
}
this.events = new Emitter();
this.id = data.id || null;
this.title = data.title || 'No Title';
this.autoUpdate = data.autoUpdate;
this.description = data.description;
this.tags = data.tags || [];
this.style = data.style || "dark";
this.timezone = data.timezone || '';
this.editable = data.editable !== false;
this.hideControls = data.hideControls || false;
this.sharedCrosshair = data.sharedCrosshair || false;
this.rows = data.rows || [];
this.time = data.time || { from: 'now-6h', to: 'now' };
this.timepicker = data.timepicker || {};
this.templating = this.ensureListExist(data.templating);
this.annotations = this.ensureListExist(data.annotations);
this.refresh = data.refresh;
this.snapshot = data.snapshot;
this.schemaVersion = data.schemaVersion || 0;
this.version = data.version || 0;
this.links = data.links || [];
this.gnetId = data.gnetId || null;
this.updateSchema(data);
this.initMeta(meta);
}
private initMeta(meta) {
meta = meta || {};
meta.canShare = meta.canShare !== false;
meta.canSave = meta.canSave !== false;
meta.canStar = meta.canStar !== false;
meta.canEdit = meta.canEdit !== false;
if (!this.editable) {
meta.canEdit = false;
meta.canDelete = false;
meta.canSave = false;
this.hideControls = true;
}
this.meta = meta;
}
// cleans meta data and other non peristent state
getSaveModelClone() {
// temp remove stuff
var events = this.events;
var meta = this.meta;
delete this.events;
delete this.meta;
events.emit('prepare-save-model');
var copy = $.extend(true, {}, this);
// restore properties
this.events = events;
this.meta = meta;
return copy;
}
private ensureListExist(data) {
if (!data) { data = {}; }
if (!data.list) { data.list = []; }
return data;
}
getNextPanelId() {
var i, j, row, panel, max = 0;
for (i = 0; i < this.rows.length; i++) {
row = this.rows[i];
for (j = 0; j < row.panels.length; j++) {
panel = row.panels[j];
if (panel.id > max) { max = panel.id; }
}
}
return max + 1;
}
forEachPanel(callback) {
var i, j, row;
for (i = 0; i < this.rows.length; i++) {
row = this.rows[i];
for (j = 0; j < row.panels.length; j++) {
callback(row.panels[j], j, row, i);
}
}
}
getPanelById(id) {
for (var i = 0; i < this.rows.length; i++) {
var row = this.rows[i];
for (var j = 0; j < row.panels.length; j++) {
var panel = row.panels[j];
if (panel.id === id) {
return panel;
}
}
}
return null;
}
rowSpan(row) {
return _.reduce(row.panels, function(p,v) {
return p + v.span;
},0);
};
addPanel(panel, row) {
var rowSpan = this.rowSpan(row);
var panelCount = row.panels.length;
var space = (12 - rowSpan) - panel.span;
panel.id = this.getNextPanelId();
// try to make room of there is no space left
if (space <= 0) {
if (panelCount === 1) {
row.panels[0].span = 6;
panel.span = 6;
} else if (panelCount === 2) {
row.panels[0].span = 4;
row.panels[1].span = 4;
panel.span = 4;
}
}
row.panels.push(panel);
}
isSubmenuFeaturesEnabled() {
var visableTemplates = _.filter(this.templating.list, function(template) {
return template.hideVariable === undefined || template.hideVariable === false;
});
return visableTemplates.length > 0 || this.annotations.list.length > 0 || this.links.length > 0;
}
getPanelInfoById(panelId) {
var result: any = {};
_.each(this.rows, function(row) {
_.each(row.panels, function(panel, index) {
if (panel.id === panelId) {
result.panel = panel;
result.row = row;
result.index = index;
}
});
});
if (!result.panel) {
return null;
}
return result;
}
duplicatePanel(panel, row) {
var rowIndex = _.indexOf(this.rows, row);
var newPanel = angular.copy(panel);
newPanel.id = this.getNextPanelId();
delete newPanel.repeat;
delete newPanel.repeatIteration;
delete newPanel.repeatPanelId;
delete newPanel.scopedVars;
var currentRow = this.rows[rowIndex];
currentRow.panels.push(newPanel);
return newPanel;
}
formatDate(date, format) {
date = moment.isMoment(date) ? date : moment(date);
format = format || 'YYYY-MM-DD HH:mm:ss';
this.timezone = this.getTimezone();
return this.timezone === 'browser' ?
moment(date).format(format) :
moment.utc(date).format(format);
}
getRelativeTime(date) {
date = moment.isMoment(date) ? date : moment(date);
return this.timezone === 'browser' ?
moment(date).fromNow() :
moment.utc(date).fromNow();
}
getNextQueryLetter(panel) {
var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
return _.find(letters, function(refId) {
return _.every(panel.targets, function(other) {
return other.refId !== refId;
});
});
}
isTimezoneUtc() {
return this.getTimezone() === 'utc';
}
getTimezone() {
return this.timezone ? this.timezone : contextSrv.user.timezone;
}
private updateSchema(old) {
var i, j, k;
var oldVersion = this.schemaVersion;
var panelUpgrades = [];
this.schemaVersion = 13;
if (oldVersion === this.schemaVersion) {
return;
}
// version 2 schema changes
if (oldVersion < 2) {
if (old.services) {
if (old.services.filter) {
this.time = old.services.filter.time;
this.templating.list = old.services.filter.list || [];
}
}
panelUpgrades.push(function(panel) {
// rename panel type
if (panel.type === 'graphite') {
panel.type = 'graph';
}
if (panel.type !== 'graph') {
return;
}
if (_.isBoolean(panel.legend)) { panel.legend = { show: panel.legend }; }
if (panel.grid) {
if (panel.grid.min) {
panel.grid.leftMin = panel.grid.min;
delete panel.grid.min;
}
if (panel.grid.max) {
panel.grid.leftMax = panel.grid.max;
delete panel.grid.max;
}
}
if (panel.y_format) {
panel.y_formats[0] = panel.y_format;
delete panel.y_format;
}
if (panel.y2_format) {
panel.y_formats[1] = panel.y2_format;
delete panel.y2_format;
}
});
}
// schema version 3 changes
if (oldVersion < 3) {
// ensure panel ids
var maxId = this.getNextPanelId();
panelUpgrades.push(function(panel) {
if (!panel.id) {
panel.id = maxId;
maxId += 1;
}
});
}
// schema version 4 changes
if (oldVersion < 4) {
// move aliasYAxis changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'graph') { return; }
_.each(panel.aliasYAxis, function(value, key) {
panel.seriesOverrides = [{ alias: key, yaxis: value }];
});
delete panel.aliasYAxis;
});
}
if (oldVersion < 6) {
// move pulldowns to new schema
var annotations = _.find(old.pulldowns, { type: 'annotations' });
if (annotations) {
this.annotations = {
list: annotations.annotations || [],
};
}
// update template variables
for (i = 0 ; i < this.templating.list.length; i++) {
var variable = this.templating.list[i];
if (variable.datasource === void 0) { variable.datasource = null; }
if (variable.type === 'filter') { variable.type = 'query'; }
if (variable.type === void 0) { variable.type = 'query'; }
if (variable.allFormat === void 0) { variable.allFormat = 'glob'; }
}
}
if (oldVersion < 7) {
if (old.nav && old.nav.length) {
this.timepicker = old.nav[0];
}
// ensure query refIds
panelUpgrades.push(function(panel) {
_.each(panel.targets, function(target) {
if (!target.refId) {
target.refId = this.getNextQueryLetter(panel);
}
}.bind(this));
});
}
if (oldVersion < 8) {
panelUpgrades.push(function(panel) {
_.each(panel.targets, function(target) {
// update old influxdb query schema
if (target.fields && target.tags && target.groupBy) {
if (target.rawQuery) {
delete target.fields;
delete target.fill;
} else {
target.select = _.map(target.fields, function(field) {
var parts = [];
parts.push({type: 'field', params: [field.name]});
parts.push({type: field.func, params: []});
if (field.mathExpr) {
parts.push({type: 'math', params: [field.mathExpr]});
}
if (field.asExpr) {
parts.push({type: 'alias', params: [field.asExpr]});
}
return parts;
});
delete target.fields;
_.each(target.groupBy, function(part) {
if (part.type === 'time' && part.interval) {
part.params = [part.interval];
delete part.interval;
}
if (part.type === 'tag' && part.key) {
part.params = [part.key];
delete part.key;
}
});
if (target.fill) {
target.groupBy.push({type: 'fill', params: [target.fill]});
delete target.fill;
}
}
}
});
});
}
// schema version 9 changes
if (oldVersion < 9) {
// move aliasYAxis changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'singlestat' && panel.thresholds !== "") { return; }
if (panel.thresholds) {
var k = panel.thresholds.split(",");
if (k.length >= 3) {
k.shift();
panel.thresholds = k.join(",");
}
}
});
}
// schema version 10 changes
if (oldVersion < 10) {
// move aliasYAxis changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'table') { return; }
_.each(panel.styles, function(style) {
if (style.thresholds && style.thresholds.length >= 3) {
var k = style.thresholds;
k.shift();
style.thresholds = k;
}
});
});
}
if (oldVersion < 12) {
// update template variables
_.each(this.templating.list, function(templateVariable) {
if (templateVariable.refresh) { templateVariable.refresh = 1; }
if (!templateVariable.refresh) { templateVariable.refresh = 0; }
if (templateVariable.hideVariable) {
templateVariable.hide = 2;
} else if (templateVariable.hideLabel) {
templateVariable.hide = 1;
} else {
templateVariable.hide = 0;
}
});
}
if (oldVersion < 12) {
// update graph yaxes changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'graph') { return; }
if (!panel.grid) { return; }
if (!panel.yaxes) {
panel.yaxes = [
{
show: panel['y-axis'],
min: panel.grid.leftMin,
max: panel.grid.leftMax,
logBase: panel.grid.leftLogBase,
format: panel.y_formats[0],
label: panel.leftYAxisLabel,
},
{
show: panel['y-axis'],
min: panel.grid.rightMin,
max: panel.grid.rightMax,
logBase: panel.grid.rightLogBase,
format: panel.y_formats[1],
label: panel.rightYAxisLabel,
}
];
panel.xaxis = {
show: panel['x-axis'],
};
delete panel.grid.leftMin;
delete panel.grid.leftMax;
delete panel.grid.leftLogBase;
delete panel.grid.rightMin;
delete panel.grid.rightMax;
delete panel.grid.rightLogBase;
delete panel.y_formats;
delete panel.leftYAxisLabel;
delete panel.rightYAxisLabel;
delete panel['y-axis'];
delete panel['x-axis'];
}
});
}
if (oldVersion < 13) {
// update graph yaxes changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'graph') { return; }
panel.thresholds = [];
var t1: any = {}, t2: any = {};
if (panel.grid.threshold1 !== null) {
t1.value = panel.grid.threshold1;
if (panel.grid.thresholdLine) {
t1.line = true;
t1.lineColor = panel.grid.threshold1Color;
} else {
t1.fill = true;
t1.fillColor = panel.grid.threshold1Color;
}
}
if (panel.grid.threshold2 !== null) {
t2.value = panel.grid.threshold2;
if (panel.grid.thresholdLine) {
t2.line = true;
t2.lineColor = panel.grid.threshold2Color;
} else {
t2.fill = true;
t2.fillColor = panel.grid.threshold2Color;
}
}
if (_.isNumber(t1.value)) {
if (_.isNumber(t2.value)) {
if (t1.value > t2.value) {
t1.op = t2.op = '<';
panel.thresholds.push(t2);
panel.thresholds.push(t1);
} else {
t1.op = t2.op = '>';
panel.thresholds.push(t2);
panel.thresholds.push(t1);
}
} else {
t1.op = '>';
panel.thresholds.push(t1);
}
}
delete panel.grid.threshold1;
delete panel.grid.threshold1Color;
delete panel.grid.threshold2;
delete panel.grid.threshold2Color;
delete panel.grid.thresholdLine;
});
}
if (panelUpgrades.length === 0) {
return;
}
for (i = 0; i < this.rows.length; i++) {
var row = this.rows[i];
for (j = 0; j < row.panels.length; j++) {
for (k = 0; k < panelUpgrades.length; k++) {
panelUpgrades[k].call(this, row.panels[j]);
}
}
}
}
}
export class DashboardSrv {
currentDashboard: any;
create(dashboard, meta) {
return new DashboardModel(dashboard, meta);
}
setCurrent(dashboard) {
this.currentDashboard = dashboard;
}
getCurrent() {
return this.currentDashboard;
}
}
coreModule.service('dashboardSrv', DashboardSrv);

View File

@ -0,0 +1,379 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import {DashboardSrv} from '../dashboard_srv';
describe('dashboardSrv', function() {
var _dashboardSrv;
beforeEach(() => {
_dashboardSrv = new DashboardSrv();
});
describe('when creating new dashboard with defaults only', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({}, {});
});
it('should have title', function() {
expect(model.title).to.be('No Title');
});
it('should have meta', function() {
expect(model.meta.canSave).to.be(true);
expect(model.meta.canShare).to.be(true);
});
it('should have default properties', function() {
expect(model.rows.length).to.be(0);
});
});
describe('when getting next panel id', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
rows: [{ panels: [{ id: 5 }]}]
});
});
it('should return max id + 1', function() {
expect(model.getNextPanelId()).to.be(6);
});
});
describe('row and panel manipulation', function() {
var dashboard;
beforeEach(function() {
dashboard = _dashboardSrv.create({});
});
it('row span should sum spans', function() {
var spanLeft = dashboard.rowSpan({ panels: [{ span: 2 }, { span: 3 }] });
expect(spanLeft).to.be(5);
});
it('adding default should split span in half', function() {
dashboard.rows = [{ panels: [{ span: 12, id: 7 }] }];
dashboard.addPanel({span: 4}, dashboard.rows[0]);
expect(dashboard.rows[0].panels[0].span).to.be(6);
expect(dashboard.rows[0].panels[1].span).to.be(6);
expect(dashboard.rows[0].panels[1].id).to.be(8);
});
it('duplicate panel should try to add it to same row', function() {
var panel = { span: 4, attr: '123', id: 10 };
dashboard.rows = [{ panels: [panel] }];
dashboard.duplicatePanel(panel, dashboard.rows[0]);
expect(dashboard.rows[0].panels[0].span).to.be(4);
expect(dashboard.rows[0].panels[1].span).to.be(4);
expect(dashboard.rows[0].panels[1].attr).to.be('123');
expect(dashboard.rows[0].panels[1].id).to.be(11);
});
it('duplicate panel should remove repeat data', function() {
var panel = { span: 4, attr: '123', id: 10, repeat: 'asd', scopedVars: { test: 'asd' }};
dashboard.rows = [{ panels: [panel] }];
dashboard.duplicatePanel(panel, dashboard.rows[0]);
expect(dashboard.rows[0].panels[1].repeat).to.be(undefined);
expect(dashboard.rows[0].panels[1].scopedVars).to.be(undefined);
});
});
describe('when creating dashboard with editable false', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
editable: false
});
});
it('should set editable false', function() {
expect(model.editable).to.be(false);
});
});
describe('when creating dashboard with old schema', function() {
var model;
var graph;
var singlestat;
var table;
beforeEach(function() {
model = _dashboardSrv.create({
services: { filter: { time: { from: 'now-1d', to: 'now'}, list: [{}] }},
pulldowns: [
{type: 'filtering', enable: true},
{type: 'annotations', enable: true, annotations: [{name: 'old'}]}
],
rows: [
{
panels: [
{
type: 'graph', legend: true, aliasYAxis: { test: 2 },
y_formats: ['kbyte', 'ms'],
grid: {
min: 1,
max: 10,
rightMin: 5,
rightMax: 15,
leftLogBase: 1,
rightLogBase: 2,
threshold1: 200,
threshold2: 400,
threshold1Color: 'yellow',
threshold2Color: 'red',
},
leftYAxisLabel: 'left label',
targets: [{refId: 'A'}, {}],
},
{
type: 'singlestat', legend: true, thresholds: '10,20,30', aliasYAxis: { test: 2 }, grid: { min: 1, max: 10 },
targets: [{refId: 'A'}, {}],
},
{
type: 'table', legend: true, styles: [{ thresholds: ["10", "20", "30"]}, { thresholds: ["100", "200", "300"]}],
targets: [{refId: 'A'}, {}],
}
]
}
]
});
graph = model.rows[0].panels[0];
singlestat = model.rows[0].panels[1];
table = model.rows[0].panels[2];
});
it('should have title', function() {
expect(model.title).to.be('No Title');
});
it('should have panel id', function() {
expect(graph.id).to.be(1);
});
it('should move time and filtering list', function() {
expect(model.time.from).to.be('now-1d');
expect(model.templating.list[0].allFormat).to.be('glob');
});
it('graphite panel should change name too graph', function() {
expect(graph.type).to.be('graph');
});
it('single stat panel should have two thresholds', function() {
expect(singlestat.thresholds).to.be('20,30');
});
it('queries without refId should get it', function() {
expect(graph.targets[1].refId).to.be('B');
});
it('update legend setting', function() {
expect(graph.legend.show).to.be(true);
});
it('move aliasYAxis to series override', function() {
expect(graph.seriesOverrides[0].alias).to.be("test");
expect(graph.seriesOverrides[0].yaxis).to.be(2);
});
it('should move pulldowns to new schema', function() {
expect(model.annotations.list[0].name).to.be('old');
});
it('table panel should only have two thresholds values', function() {
expect(table.styles[0].thresholds[0]).to.be("20");
expect(table.styles[0].thresholds[1]).to.be("30");
expect(table.styles[1].thresholds[0]).to.be("200");
expect(table.styles[1].thresholds[1]).to.be("300");
});
it('graph grid to yaxes options', function() {
expect(graph.yaxes[0].min).to.be(1);
expect(graph.yaxes[0].max).to.be(10);
expect(graph.yaxes[0].format).to.be('kbyte');
expect(graph.yaxes[0].label).to.be('left label');
expect(graph.yaxes[0].logBase).to.be(1);
expect(graph.yaxes[1].min).to.be(5);
expect(graph.yaxes[1].max).to.be(15);
expect(graph.yaxes[1].format).to.be('ms');
expect(graph.yaxes[1].logBase).to.be(2);
expect(graph.grid.rightMax).to.be(undefined);
expect(graph.grid.rightLogBase).to.be(undefined);
expect(graph.y_formats).to.be(undefined);
});
it('dashboard schema version should be set to latest', function() {
expect(model.schemaVersion).to.be(13);
});
it('graph thresholds should be migrated', function() {
expect(graph.thresholds.length).to.be(2);
expect(graph.thresholds[0].op).to.be('>');
expect(graph.thresholds[0].value).to.be(400);
expect(graph.thresholds[0].fillColor).to.be('red');
expect(graph.thresholds[1].value).to.be(200);
expect(graph.thresholds[1].fillColor).to.be('yellow');
});
});
describe('when creating dashboard model with missing list for annoations or templating', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
annotations: {
enable: true,
},
templating: {
enable: true
}
});
});
it('should add empty list', function() {
expect(model.annotations.list.length).to.be(0);
expect(model.templating.list.length).to.be(0);
});
});
describe('Given editable false dashboard', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
editable: false,
});
});
it('Should set meta canEdit and canSave to false', function() {
expect(model.meta.canSave).to.be(false);
expect(model.meta.canEdit).to.be(false);
});
it('getSaveModelClone should remove meta', function() {
var clone = model.getSaveModelClone();
expect(clone.meta).to.be(undefined);
});
});
describe('when loading dashboard with old influxdb query schema', function() {
var model;
var target;
beforeEach(function() {
model = _dashboardSrv.create({
rows: [{
panels: [{
type: 'graph',
grid: {},
yaxes: [{}, {}],
targets: [{
"alias": "$tag_datacenter $tag_source $col",
"column": "value",
"measurement": "logins.count",
"fields": [
{
"func": "mean",
"name": "value",
"mathExpr": "*2",
"asExpr": "value"
},
{
"name": "one-minute",
"func": "mean",
"mathExpr": "*3",
"asExpr": "one-minute"
}
],
"tags": [],
"fill": "previous",
"function": "mean",
"groupBy": [
{
"interval": "auto",
"type": "time"
},
{
"key": "source",
"type": "tag"
},
{
"type": "tag",
"key": "datacenter"
}
],
}]
}]
}]
});
target = model.rows[0].panels[0].targets[0];
});
it('should update query schema', function() {
expect(target.fields).to.be(undefined);
expect(target.select.length).to.be(2);
expect(target.select[0].length).to.be(4);
expect(target.select[0][0].type).to.be('field');
expect(target.select[0][1].type).to.be('mean');
expect(target.select[0][2].type).to.be('math');
expect(target.select[0][3].type).to.be('alias');
});
});
describe('when creating dashboard model with missing list for annoations or templating', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
annotations: {
enable: true,
},
templating: {
enable: true
}
});
});
it('should add empty list', function() {
expect(model.annotations.list.length).to.be(0);
expect(model.templating.list.length).to.be(0);
});
});
describe('Formatting epoch timestamp when timezone is set as utc', function() {
var dashboard;
beforeEach(function() {
dashboard = _dashboardSrv.create({
timezone: 'utc',
});
});
it('Should format timestamp with second resolution by default', function() {
expect(dashboard.formatDate(1234567890000)).to.be('2009-02-13 23:31:30');
});
it('Should format timestamp with second resolution even if second format is passed as parameter', function() {
expect(dashboard.formatDate(1234567890007,'YYYY-MM-DD HH:mm:ss')).to.be('2009-02-13 23:31:30');
});
it('Should format timestamp with millisecond resolution if format is passed as parameter', function() {
expect(dashboard.formatDate(1234567890007,'YYYY-MM-DD HH:mm:ss.SSS')).to.be('2009-02-13 23:31:30.007');
});
});
});

View File

@ -1,6 +1,6 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import 'app/features/dashboard/dashboardSrv';
import {DashboardSrv} from '../dashboard_srv';
import {DynamicDashboardSrv} from '../dynamic_dashboard_srv';
function dynamicDashScenario(desc, func) {
@ -10,6 +10,7 @@ function dynamicDashScenario(desc, func) {
ctx.setup = function (setupFunc) {
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(angularMocks.module(function($provide) {
$provide.value('contextSrv', {

View File

@ -1,10 +1,13 @@
<div class="submenu-controls">
<div class="submenu-controls gf-form-query">
<ul ng-if="ctrl.dashboard.templating.list.length > 0">
<li ng-repeat="variable in ctrl.variables" ng-hide="variable.hide === 2" class="submenu-item">
<span class="submenu-item-label template-variable " ng-hide="variable.hide === 1">
<li 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}}:
</span>
<value-select-dropdown variable="variable" on-updated="ctrl.variableUpdated(variable)" get-values-for-tag="ctrl.getValuesForTag(variable, tagKey)"></value-select-dropdown>
</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>
<ad-hoc-filters ng-if="variable.type === 'adhoc'" variable="variable"></ad-hoc-filters>
</li>
</ul>

View File

@ -10,11 +10,11 @@ export class SubmenuCtrl {
/** @ngInject */
constructor(private $rootScope,
private templateValuesSrv,
private variableSrv,
private templateSrv,
private $location) {
this.annotations = this.dashboard.templating.list;
this.variables = this.dashboard.templating.list;
this.variables = this.variableSrv.variables;
}
disableAnnotation(annotation) {
@ -23,11 +23,11 @@ export class SubmenuCtrl {
}
getValuesForTag(variable, tagKey) {
return this.templateValuesSrv.getValuesForTag(variable, tagKey);
return this.variableSrv.getValuesForTag(variable, tagKey);
}
variableUpdated(variable) {
this.templateValuesSrv.variableUpdated(variable).then(() => {
this.variableSrv.variableUpdated(variable).then(() => {
this.$rootScope.$emit('template-variable-value-updated');
this.$rootScope.$broadcast('refresh');
});

View File

@ -34,10 +34,6 @@ function (angular, _, $) {
$location.search(urlParams);
});
$scope.onAppEvent('template-variable-value-updated', function() {
self.updateUrlParamsWithCurrentVariables();
});
$scope.onAppEvent('$routeUpdate', function() {
var urlState = self.getQueryStringState();
if (self.needsSync(urlState)) {
@ -57,22 +53,6 @@ function (angular, _, $) {
this.expandRowForPanel();
}
DashboardViewState.prototype.updateUrlParamsWithCurrentVariables = function() {
// update url
var params = $location.search();
// remove variable params
_.each(params, function(value, key) {
if (key.indexOf('var-') === 0) {
delete params[key];
}
});
// add new values
templateSrv.fillVariableValuesForUrl(params);
// update url
$location.search(params);
};
DashboardViewState.prototype.expandRowForPanel = function() {
if (!this.state.panelId) { return; }
@ -185,7 +165,7 @@ function (angular, _, $) {
DashboardViewState.prototype.enterFullscreen = function(panelScope) {
var ctrl = panelScope.ctrl;
ctrl.editMode = this.state.edit && this.$scope.dashboardMeta.canEdit;
ctrl.editMode = this.state.edit && this.dashboard.meta.canEdit;
ctrl.fullscreen = true;
this.oldTimeRange = ctrl.range;

View File

@ -0,0 +1,74 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import {Variable, assignModelProperties, variableTypes} from './variable';
import {VariableSrv} from './variable_srv';
export class AdhocVariable implements Variable {
filters: any[];
defaults = {
type: 'adhoc',
name: '',
label: '',
hide: 0,
datasource: null,
filters: [],
};
/** @ngInject **/
constructor(private model) {
assignModelProperties(this, model, this.defaults);
}
setValue(option) {
return Promise.resolve();
}
getModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
updateOptions() {
return Promise.resolve();
}
dependsOn(variable) {
return false;
}
setValueFromUrl(urlValue) {
if (!_.isArray(urlValue)) {
urlValue = [urlValue];
}
this.filters = urlValue.map(item => {
var values = item.split('|');
return {
key: values[0],
operator: values[1],
value: values[2],
};
});
return Promise.resolve();
}
getValueForUrl() {
return this.filters.map(filter => {
return filter.key + '|' + filter.operator + '|' + filter.value;
});
}
setFilters(filters: any[]) {
this.filters = filters;
}
}
variableTypes['adhoc'] = {
name: 'Ad hoc filters',
ctor: AdhocVariable,
description: 'Add key/value filters on the fly',
};

View File

@ -0,0 +1,20 @@
import './templateSrv';
import './editor_ctrl';
import {VariableSrv} from './variable_srv';
import {IntervalVariable} from './interval_variable';
import {QueryVariable} from './query_variable';
import {DatasourceVariable} from './datasource_variable';
import {CustomVariable} from './custom_variable';
import {ConstantVariable} from './constant_variable';
import {AdhocVariable} from './adhoc_variable';
export {
VariableSrv,
IntervalVariable,
QueryVariable,
DatasourceVariable,
CustomVariable,
ConstantVariable,
AdhocVariable,
}

View File

@ -0,0 +1,59 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import {Variable, assignModelProperties, variableTypes} from './variable';
import {VariableSrv} from './variable_srv';
export class ConstantVariable implements Variable {
query: string;
options: any[];
current: any;
defaults = {
type: 'constant',
name: '',
hide: 2,
label: '',
query: '',
current: {},
};
/** @ngInject */
constructor(private model, private variableSrv) {
assignModelProperties(this, model, this.defaults);
}
getModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
setValue(option) {
this.variableSrv.setOptionAsCurrent(this, option);
}
updateOptions() {
this.options = [{text: this.query.trim(), value: this.query.trim()}];
this.setValue(this.options[0]);
return Promise.resolve();
}
dependsOn(variable) {
return false;
}
setValueFromUrl(urlValue) {
return this.variableSrv.setOptionFromUrl(this, urlValue);
}
getValueForUrl() {
return this.current.value;
}
}
variableTypes['constant'] = {
name: 'Constant',
ctor: ConstantVariable,
description: 'Define a hidden constant variable, useful for metric prefixes in dashboards you want to share' ,
};

View File

@ -0,0 +1,80 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import {Variable, assignModelProperties, variableTypes} from './variable';
import {VariableSrv} from './variable_srv';
export class CustomVariable implements Variable {
query: string;
options: any;
includeAll: boolean;
multi: boolean;
current: any;
defaults = {
type: 'custom',
name: '',
label: '',
hide: 0,
options: [],
current: {},
query: '',
includeAll: false,
multi: false,
allValue: null,
};
/** @ngInject **/
constructor(private model, private timeSrv, private templateSrv, private variableSrv) {
assignModelProperties(this, model, this.defaults);
}
setValue(option) {
return this.variableSrv.setOptionAsCurrent(this, option);
}
getModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
updateOptions() {
// extract options in comma separated string
this.options = _.map(this.query.split(/[,]+/), function(text) {
return { text: text.trim(), value: text.trim() };
});
if (this.includeAll) {
this.addAllOption();
}
return this.variableSrv.validateVariableSelectionState(this);
}
addAllOption() {
this.options.unshift({text: 'All', value: "$__all"});
}
dependsOn(variable) {
return false;
}
setValueFromUrl(urlValue) {
return this.variableSrv.setOptionFromUrl(this, urlValue);
}
getValueForUrl() {
if (this.current.text === 'All') {
return 'All';
}
return this.current.value;
}
}
variableTypes['custom'] = {
name: 'Custom',
ctor: CustomVariable,
description: 'Define variable values manually' ,
supportsMulti: true,
};

View File

@ -0,0 +1,87 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import {Variable, assignModelProperties, variableTypes} from './variable';
import {VariableSrv} from './variable_srv';
export class DatasourceVariable implements Variable {
regex: any;
query: string;
options: any;
current: any;
defaults = {
type: 'datasource',
name: '',
hide: 0,
label: '',
current: {},
regex: '',
options: [],
query: '',
};
/** @ngInject */
constructor(private model, private datasourceSrv, private variableSrv) {
assignModelProperties(this, model, this.defaults);
}
getModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
setValue(option) {
return this.variableSrv.setOptionAsCurrent(this, option);
}
updateOptions() {
var options = [];
var sources = this.datasourceSrv.getMetricSources({skipVariables: true});
var regex;
if (this.regex) {
regex = kbn.stringToJsRegex(this.regex);
}
for (var i = 0; i < sources.length; i++) {
var source = sources[i];
// must match on type
if (source.meta.id !== this.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: ''});
}
this.options = options;
return this.variableSrv.validateVariableSelectionState(this);
}
dependsOn(variable) {
return false;
}
setValueFromUrl(urlValue) {
return this.variableSrv.setOptionFromUrl(this, urlValue);
}
getValueForUrl() {
return this.current.value;
}
}
variableTypes['datasource'] = {
name: 'Datasource',
ctor: DatasourceVariable,
description: 'Enabled you to dynamically switch the datasource for multiple panels',
};

View File

@ -1,198 +0,0 @@
define([
'angular',
'lodash',
],
function (angular, _) {
'use strict';
var module = angular.module('grafana.controllers');
module.controller('TemplateEditorCtrl', function($scope, datasourceSrv, templateSrv, templateValuesSrv) {
var replacementDefaults = {
type: 'query',
datasource: null,
refresh: 0,
sort: 1,
name: '',
hide: 0,
options: [],
includeAll: false,
multi: false,
};
$scope.variableTypes = [
{value: "query", text: "Query"},
{value: "interval", text: "Interval"},
{value: "datasource", text: "Data source"},
{value: "custom", text: "Custom"},
{value: "constant", text: "Constant"},
];
$scope.refreshOptions = [
{value: 0, text: "Never"},
{value: 1, text: "On Dashboard Load"},
{value: 2, text: "On Time Range Change"},
];
$scope.sortOptions = [
{value: 0, text: "Without Sort"},
{value: 1, text: "Alphabetical (asc)"},
{value: 2, text: "Alphabetical (desc)"},
{value: 3, text: "Numerical (asc)"},
{value: 4, text: "Numerical (desc)"},
];
$scope.hideOptions = [
{value: 0, text: ""},
{value: 1, text: "Label"},
{value: 2, text: "Variable"},
];
$scope.init = function() {
$scope.mode = 'list';
$scope.datasourceTypes = {};
$scope.datasources = _.filter(datasourceSrv.getMetricSources(), function(ds) {
$scope.datasourceTypes[ds.meta.id] = {text: ds.meta.name, value: ds.meta.id};
return !ds.meta.builtIn;
});
$scope.datasourceTypes = _.map($scope.datasourceTypes, function(value) {
return value;
});
$scope.variables = templateSrv.variables;
$scope.reset();
$scope.$watch('mode', function(val) {
if (val === 'new') {
$scope.reset();
}
});
$scope.$watch('current.datasource', function(val) {
if ($scope.mode === 'new') {
datasourceSrv.get(val).then(function(ds) {
if (ds.meta.defaultMatchFormat) {
$scope.current.allFormat = ds.meta.defaultMatchFormat;
$scope.current.multiFormat = ds.meta.defaultMatchFormat;
}
});
}
});
};
$scope.add = function() {
if ($scope.isValid()) {
$scope.variables.push($scope.current);
$scope.update();
$scope.updateSubmenuVisibility();
}
};
$scope.isValid = function() {
if (!$scope.current.name) {
$scope.appEvent('alert-warning', ['Validation', 'Template variable requires a name']);
return false;
}
if (!$scope.current.name.match(/^\w+$/)) {
$scope.appEvent('alert-warning', ['Validation', 'Only word and digit characters are allowed in variable names']);
return false;
}
var sameName = _.find($scope.variables, { name: $scope.current.name });
if (sameName && sameName !== $scope.current) {
$scope.appEvent('alert-warning', ['Validation', 'Variable with the same name already exists']);
return false;
}
return true;
};
$scope.runQuery = function() {
return templateValuesSrv.updateOptions($scope.current).then(null, function(err) {
if (err.data && err.data.message) { err.message = err.data.message; }
$scope.appEvent("alert-error", ['Templating', 'Template variables could not be initialized: ' + err.message]);
});
};
$scope.edit = function(variable) {
$scope.current = variable;
$scope.currentIsNew = false;
$scope.mode = 'edit';
$scope.current.sort = $scope.current.sort || replacementDefaults.sort;
if ($scope.current.datasource === void 0) {
$scope.current.datasource = null;
$scope.current.type = 'query';
$scope.current.allFormat = 'glob';
}
};
$scope.duplicate = function(variable) {
$scope.current = angular.copy(variable);
$scope.variables.push($scope.current);
$scope.current.name = 'copy_of_'+variable.name;
$scope.updateSubmenuVisibility();
};
$scope.update = function() {
if ($scope.isValid()) {
$scope.runQuery().then(function() {
$scope.reset();
$scope.mode = 'list';
});
}
};
$scope.reset = function() {
$scope.currentIsNew = true;
$scope.current = angular.copy(replacementDefaults);
};
$scope.showSelectionOptions = function() {
if ($scope.current) {
if ($scope.current.type === 'query') {
return true;
}
if ($scope.current.type === 'custom') {
return true;
}
}
return false;
};
$scope.typeChanged = function () {
if ($scope.current.type === 'interval') {
$scope.current.query = '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d';
$scope.current.refresh = 0;
}
if ($scope.current.type === 'query') {
$scope.current.query = '';
}
if ($scope.current.type === 'constant') {
$scope.current.query = '';
$scope.current.refresh = 0;
$scope.current.hide = 2;
}
if ($scope.current.type === 'datasource') {
$scope.current.query = $scope.datasourceTypes[0].value;
$scope.current.regex = '';
$scope.current.refresh = 1;
}
};
$scope.removeVariable = function(variable) {
var index = _.indexOf($scope.variables, variable);
$scope.variables.splice(index, 1);
$scope.updateSubmenuVisibility();
};
});
});

View File

@ -0,0 +1,155 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import {variableTypes} from './variable';
export class VariableEditorCtrl {
/** @ngInject */
constructor(private $scope, private datasourceSrv, private variableSrv, templateSrv) {
$scope.variableTypes = variableTypes;
$scope.ctrl = {};
$scope.refreshOptions = [
{value: 0, text: "Never"},
{value: 1, text: "On Dashboard Load"},
{value: 2, text: "On Time Range Change"},
];
$scope.sortOptions = [
{value: 0, text: "Disabled"},
{value: 1, text: "Alphabetical (asc)"},
{value: 2, text: "Alphabetical (desc)"},
{value: 3, text: "Numerical (asc)"},
{value: 4, text: "Numerical (desc)"},
];
$scope.hideOptions = [
{value: 0, text: ""},
{value: 1, text: "Label"},
{value: 2, text: "Variable"},
];
$scope.init = function() {
$scope.mode = 'list';
$scope.datasources = _.filter(datasourceSrv.getMetricSources(), function(ds) {
return !ds.meta.builtIn && ds.value !== null;
});
$scope.datasourceTypes = _($scope.datasources).uniqBy('meta.id').map(function(ds) {
return {text: ds.meta.name, value: ds.meta.id};
}).value();
$scope.variables = variableSrv.variables;
$scope.reset();
$scope.$watch('mode', function(val) {
if (val === 'new') {
$scope.reset();
}
});
};
$scope.add = function() {
if ($scope.isValid()) {
$scope.variables.push($scope.current);
$scope.update();
$scope.updateSubmenuVisibility();
}
};
$scope.isValid = function() {
if (!$scope.ctrl.form.$valid) {
return;
}
if (!$scope.current.name.match(/^\w+$/)) {
$scope.appEvent('alert-warning', ['Validation', 'Only word and digit characters are allowed in variable names']);
return false;
}
var sameName = _.find($scope.variables, { name: $scope.current.name });
if (sameName && sameName !== $scope.current) {
$scope.appEvent('alert-warning', ['Validation', 'Variable with the same name already exists']);
return false;
}
return true;
};
$scope.validate = function() {
$scope.infoText = '';
if ($scope.current.type === 'adhoc' && $scope.current.datasource !== null) {
$scope.infoText = 'Adhoc filters are applied automatically to all queries that target this datasource';
datasourceSrv.get($scope.current.datasource).then(ds => {
if (!ds.supportAdhocFilters) {
$scope.infoText = 'This datasource does not support adhoc filters yet.';
}
});
}
};
$scope.runQuery = function() {
return variableSrv.updateOptions($scope.current).then(null, function(err) {
if (err.data && err.data.message) { err.message = err.data.message; }
$scope.appEvent("alert-error", ['Templating', 'Template variables could not be initialized: ' + err.message]);
});
};
$scope.edit = function(variable) {
$scope.current = variable;
$scope.currentIsNew = false;
$scope.mode = 'edit';
$scope.validate();
};
$scope.duplicate = function(variable) {
var clone = _.cloneDeep(variable.getModel());
$scope.current = variableSrv.createVariableFromModel(clone);
$scope.variables.push($scope.current);
$scope.current.name = 'copy_of_'+variable.name;
$scope.updateSubmenuVisibility();
};
$scope.update = function() {
if ($scope.isValid()) {
$scope.runQuery().then(function() {
$scope.reset();
$scope.mode = 'list';
templateSrv.updateTemplateData();
});
}
};
$scope.reset = function() {
$scope.currentIsNew = true;
$scope.current = variableSrv.createVariableFromModel({type: 'query'});
};
$scope.typeChanged = function() {
var old = $scope.current;
$scope.current = variableSrv.createVariableFromModel({type: $scope.current.type});
$scope.current.name = old.name;
$scope.current.hide = old.hide;
$scope.current.label = old.label;
var oldIndex = _.indexOf(this.variables, old);
if (oldIndex !== -1) {
this.variables[oldIndex] = $scope.current;
}
$scope.validate();
};
$scope.removeVariable = function(variable) {
var index = _.indexOf($scope.variables, variable);
$scope.variables.splice(index, 1);
$scope.updateSubmenuVisibility();
};
}
}
coreModule.controller('VariableEditorCtrl', VariableEditorCtrl);

View File

@ -0,0 +1,89 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import {Variable, assignModelProperties, variableTypes} from './variable';
import {VariableSrv} from './variable_srv';
export class IntervalVariable implements Variable {
auto_count: number;
auto_min: number;
options: any;
auto: boolean;
query: string;
refresh: number;
current: any;
defaults = {
type: 'interval',
name: '',
hide: 0,
label: '',
refresh: 2,
options: [],
current: {},
query: '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d',
auto: false,
auto_min: '10s',
auto_count: 30,
};
/** @ngInject */
constructor(private model, private timeSrv, private templateSrv, private variableSrv) {
assignModelProperties(this, model, this.defaults);
this.refresh = 2;
}
getModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
setValue(option) {
this.updateAutoValue();
return this.variableSrv.setOptionAsCurrent(this, option);
}
updateAutoValue() {
if (!this.auto) {
return;
}
// add auto option if missing
if (this.options.length && this.options[0].text !== 'auto') {
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);
}
updateOptions() {
// extract options in comma separated string
this.options = _.map(this.query.split(/[,]+/), function(text) {
return {text: text.trim(), value: text.trim()};
});
this.updateAutoValue();
return this.variableSrv.validateVariableSelectionState(this);
}
dependsOn(variable) {
return false;
}
setValueFromUrl(urlValue) {
this.updateAutoValue();
return this.variableSrv.setOptionFromUrl(this, urlValue);
}
getValueForUrl() {
return this.current.value;
}
}
variableTypes['interval'] = {
name: 'Interval',
ctor: IntervalVariable,
description: 'Define a timespan interval (ex 1m, 1h, 1d)',
};

View File

@ -1,4 +1,4 @@
<div ng-controller="TemplateEditorCtrl" ng-init="init()">
<div ng-controller="VariableEditorCtrl" ng-init="init()">
<div class="tabbed-view-header">
<h2 class="tabbed-view-title">
Templating
@ -70,33 +70,23 @@
</div>
</div>
<div ng-if="mode === 'edit' || mode === 'new'">
<form ng-if="mode === 'edit' || mode === 'new'" name="ctrl.form">
<h5 class="section-heading">Variable</h5>
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form max-width-19">
<span class="gf-form-label width-6">Name</span>
<input type="text" class="gf-form-input" placeholder="name" ng-model='current.name'></input>
<input type="text" class="gf-form-input" placeholder="name" ng-model='current.name' required></input>
</div>
<div class="gf-form max-width-19">
<span class="gf-form-label width-6">
Type
<info-popover mode="right-normal">
<dl>
<dt>Query</dt>
<dd>Variable values are fetched from a metric names query to a data source</dd>
<dt>Interval</dt>
<dd>Timespan variable type</dd>
<dt>Datasource</dt>
<dd>Dynamically switch data sources using this type of variable</dd>
<dt>Custom</dt>
<dd>Define variable values manually</dd>
</dl>
<a href="http://docs.grafana.org/reference/templating" target="_blank">Templating docs</a>
{{variableTypes[current.type].description}}
</info-popover>
</span>
<div class="gf-form-select-wrapper max-width-17">
<select class="gf-form-input" ng-model="current.type" ng-options="f.value as f.text for f in variableTypes" ng-change="typeChanged()"></select>
<select class="gf-form-input" ng-model="current.type" ng-options="k as v.name for (k, v) in variableTypes" ng-change="typeChanged()"></select>
</div>
</div>
</div>
@ -112,15 +102,14 @@
</div>
</div>
</div>
</div>
<div ng-show="current.type === 'interval'" class="gf-form-group">
<div ng-if="current.type === 'interval'" class="gf-form-group">
<h5 class="section-heading">Interval Options</h5>
<div class="gf-form">
<span class="gf-form-label width-9">Values</span>
<input type="text" class="gf-form-input" placeholder="name" ng-model='current.query' placeholder="1m,10m,1h,6h,1d,7d" ng-model-onblur ng-change="runQuery()"></input>
<input type="text" class="gf-form-input" placeholder="name" ng-model='current.query' placeholder="1m,10m,1h,6h,1d,7d" ng-model-onblur ng-change="runQuery()" required></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-9">Auto option</span>
@ -144,15 +133,15 @@
</div>
</div>
<div ng-show="current.type === 'custom'" class="gf-form-group">
<div ng-if="current.type === 'custom'" class="gf-form-group">
<h5 class="section-heading">Custom Options</h5>
<div class="gf-form">
<span class="gf-form-label width-13">Values separated by comma</span>
<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue"></input>
<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue" required></input>
</div>
</div>
<div ng-show="current.type === 'constant'" class="gf-form-group">
<div ng-if="current.type === 'constant'" class="gf-form-group">
<h5 class="section-heading">Constant options</h5>
<div class="gf-form">
<span class="gf-form-label">Value</span>
@ -160,14 +149,14 @@
</div>
</div>
<div ng-show="current.type === 'query'" class="gf-form-group">
<div ng-if="current.type === 'query'" class="gf-form-group">
<h5 class="section-heading">Query Options</h5>
<div class="gf-form-inline">
<div class="gf-form max-width-21">
<span class="gf-form-label width-7" ng-show="current.type === 'query'">Data source</span>
<span class="gf-form-label width-7">Data source</span>
<div class="gf-form-select-wrapper max-width-14">
<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"></select>
<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources" required></select>
</div>
</div>
<div class="gf-form max-width-21">
@ -180,6 +169,20 @@
<div class="gf-form-select-wrapper max-width-14">
<select class="gf-form-input" ng-model="current.refresh" ng-options="f.value as f.text for f in refreshOptions"></select>
</div>
</div>
</div>
<div class="gf-form">
<span class="gf-form-label width-7">Query</span>
<input type="text" class="gf-form-input" ng-model='current.query' placeholder="metric name or tags query" ng-model-onblur ng-change="runQuery()" required></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-7">
Regex
<info-popover mode="right-normal">
Optional, if you want to extract part of a series name or metric node segment.
</info-popover>
</span>
<input type="text" class="gf-form-input" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
</div>
<div class="gf-form max-width-21">
<span class="gf-form-label width-7">
@ -193,20 +196,6 @@
</div>
</div>
</div>
<div class="gf-form">
<span class="gf-form-label width-7">Query</span>
<input type="text" class="gf-form-input" ng-model='current.query' placeholder="metric name or tags query" ng-model-onblur ng-change="runQuery()"></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-7">
Regex
<info-popover mode="right-normal">
Optional, if you want to extract part of a series name or metric node segment.
</info-popover>
</span>
<input type="text" class="gf-form-input" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
</div>
</div>
<div ng-show="current.type === 'datasource'" class="gf-form-group">
<h5 class="section-heading">Data source options</h5>
@ -233,7 +222,17 @@
</div>
</div>
<div class="section gf-form-group" ng-show="showSelectionOptions()">
<div ng-if="current.type === 'adhoc'" class="gf-form-group">
<h5 class="section-heading">Options</h5>
<div class="gf-form max-width-21">
<span class="gf-form-label width-8">Data source</span>
<div class="gf-form-select-wrapper max-width-14">
<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources" required ng-change="validate()"></select>
</div>
</div>
</div>
<div class="section gf-form-group" ng-show="variableTypes[current.type].supportsMulti">
<h5 class="section-heading">Selection Options</h5>
<div class="section">
<gf-form-switch class="gf-form"
@ -271,7 +270,7 @@
</div>
</div>
<div class="gf-form-group">
<div class="gf-form-group" ng-show="current.options.length">
<h5>Preview of values (shows max 20)</h5>
<div class="gf-form-inline">
<div class="gf-form" ng-repeat="option in current.options | limitTo: 20">
@ -279,12 +278,17 @@
</div>
</div>
</div>
<div class="alert alert-info gf-form-group" ng-if="infoText">
{{infoText}}
</div>
<div class="gf-form-button-row p-y-0">
<button type="button" class="btn btn-success" ng-show="mode === 'edit'" ng-click="update();">Update</button>
<button type="button" class="btn btn-success" ng-show="mode === 'new'" ng-click="add();">Add</button>
<button type="submit" class="btn btn-success" ng-show="mode === 'edit'" ng-click="update();">Update</button>
<button type="submit" class="btn btn-success" ng-show="mode === 'new'" ng-click="add();">Add</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,167 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import {Variable, containsVariable, assignModelProperties, variableTypes} from './variable';
import {VariableSrv} from './variable_srv';
function getNoneOption() {
return { text: 'None', value: '', isNone: true };
}
export class QueryVariable implements Variable {
datasource: any;
query: any;
regex: any;
sort: any;
options: any;
current: any;
refresh: number;
hide: number;
name: string;
multi: boolean;
includeAll: boolean;
defaults = {
type: 'query',
query: '',
regex: '',
sort: 0,
datasource: null,
refresh: 0,
hide: 0,
name: '',
multi: false,
includeAll: false,
allValue: null,
options: [],
current: {},
tagsQuery: null,
tagValuesQuery: null,
};
constructor(private model, private datasourceSrv, private templateSrv, private variableSrv, private $q) {
// copy model properties to this instance
assignModelProperties(this, model, this.defaults);
}
getModel() {
// copy back model properties to model
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
setValue(option){
return this.variableSrv.setOptionAsCurrent(this, option);
}
setValueFromUrl(urlValue) {
return this.variableSrv.setOptionFromUrl(this, urlValue);
}
getValueForUrl() {
if (this.current.text === 'All') {
return 'All';
}
return this.current.value;
}
updateOptions() {
return this.datasourceSrv.get(this.datasource)
.then(this.updateOptionsFromMetricFindQuery.bind(this))
.then(this.variableSrv.validateVariableSelectionState.bind(this.variableSrv, this));
}
updateOptionsFromMetricFindQuery(datasource) {
return datasource.metricFindQuery(this.query).then(results => {
this.options = this.metricNamesToVariableValues(results);
if (this.includeAll) {
this.addAllOption();
}
if (!this.options.length) {
this.options.push(getNoneOption());
}
return datasource;
});
}
addAllOption() {
this.options.unshift({text: 'All', value: "$__all"});
}
metricNamesToVariableValues(metricNames) {
var regex, options, i, matches;
options = [];
if (this.model.regex) {
regex = kbn.stringToJsRegex(this.templateSrv.replace(this.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, this.sort);
}
sortVariableValues(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;
}
dependsOn(variable) {
return containsVariable(this.query, this.datasource, variable.name);
}
}
variableTypes['query'] = {
name: 'Query',
ctor: QueryVariable,
description: 'Variable values are fetched from a datasource query',
supportsMulti: true,
};

View File

@ -0,0 +1,40 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import {AdhocVariable} from '../adhoc_variable';
describe('AdhocVariable', function() {
describe('when serializing to url', function() {
it('should set return key value and op seperated by pipe', function() {
var variable = new AdhocVariable({
filters: [
{key: 'key1', operator: '=', value: 'value1'},
{key: 'key2', operator: '!=', value: 'value2'},
]
});
var urlValue = variable.getValueForUrl();
expect(urlValue).to.eql(["key1|=|value1", "key2|!=|value2"]);
});
});
describe('when deserializing from url', function() {
it('should restore filters', function() {
var variable = new AdhocVariable({});
variable.setValueFromUrl(["key1|=|value1", "key2|!=|value2"]);
expect(variable.filters[0].key).to.be('key1');
expect(variable.filters[0].operator).to.be('=');
expect(variable.filters[0].value).to.be('value1');
expect(variable.filters[1].key).to.be('key2');
expect(variable.filters[1].operator).to.be('!=');
expect(variable.filters[1].value).to.be('value2');
});
});
});

View File

@ -0,0 +1,39 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import {QueryVariable} from '../query_variable';
describe('QueryVariable', function() {
describe('when creating from model', function() {
it('should set defaults', function() {
var variable = new QueryVariable({}, null, null, null, null);
expect(variable.datasource).to.be(null);
expect(variable.refresh).to.be(0);
expect(variable.sort).to.be(0);
expect(variable.name).to.be('');
expect(variable.hide).to.be(0);
expect(variable.options.length).to.be(0);
expect(variable.multi).to.be(false);
expect(variable.includeAll).to.be(false);
});
it('get model should copy changes back to model', () => {
var variable = new QueryVariable({}, null, null, null, null);
variable.options = [{text: 'test'}];
variable.datasource = 'google';
variable.regex = 'asd';
variable.sort = 50;
var model = variable.getModel();
expect(model.options.length).to.be(1);
expect(model.options[0].text).to.be('test');
expect(model.datasource).to.be('google');
expect(model.regex).to.be('asd');
expect(model.sort).to.be(50);
});
});
});

View File

@ -0,0 +1,237 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import '../all';
import {Emitter} from 'app/core/core';
describe('templateSrv', function() {
var _templateSrv, _variableSrv;
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(angularMocks.inject(function(variableSrv, templateSrv) {
_templateSrv = templateSrv;
_variableSrv = variableSrv;
}));
function initTemplateSrv(variables) {
_variableSrv.init({
templating: {list: variables},
events: new Emitter(),
});
}
describe('init', function() {
beforeEach(function() {
initTemplateSrv([{type: 'query', name: 'test', current: {value: 'oogle'}}]);
});
it('should initialize template data', function() {
var target = _templateSrv.replace('this.[[test]].filters');
expect(target).to.be('this.oogle.filters');
});
});
describe('replace can pass scoped vars', function() {
beforeEach(function() {
initTemplateSrv([{type: 'query', name: 'test', current: {value: 'oogle' }}]);
});
it('should replace $test with scoped value', function() {
var target = _templateSrv.replace('this.$test.filters', {'test': {value: 'mupp', text: 'asd'}});
expect(target).to.be('this.mupp.filters');
});
it('should replace $test with scoped text', function() {
var target = _templateSrv.replaceWithText('this.$test.filters', {'test': {value: 'mupp', text: 'asd'}});
expect(target).to.be('this.asd.filters');
});
});
describe('replace can pass multi / all format', function() {
beforeEach(function() {
initTemplateSrv([{type: 'query', name: 'test', current: {value: ['value1', 'value2'] }}]);
});
it('should replace $test with globbed value', function() {
var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).to.be('this.{value1,value2}.filters');
});
it('should replace $test with piped value', function() {
var target = _templateSrv.replace('this=$test', {}, 'pipe');
expect(target).to.be('this=value1|value2');
});
it('should replace $test with piped value', function() {
var target = _templateSrv.replace('this=$test', {}, 'pipe');
expect(target).to.be('this=value1|value2');
});
});
describe('variable with all option', function() {
beforeEach(function() {
initTemplateSrv([{
type: 'query',
name: 'test',
current: {value: '$__all' },
options: [
{value: '$__all'}, {value: 'value1'}, {value: 'value2'}
]
}]);
});
it('should replace $test with formatted all value', function() {
var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).to.be('this.{value1,value2}.filters');
});
});
describe('variable with all option and custom value', function() {
beforeEach(function() {
initTemplateSrv([{
type: 'query',
name: 'test',
current: {value: '$__all' },
allValue: '*',
options: [
{value: 'value1'}, {value: 'value2'}
]
}]);
});
it('should replace $test with formatted all value', function() {
var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).to.be('this.*.filters');
});
it('should not escape custom all value', function() {
var target = _templateSrv.replace('this.$test', {}, 'regex');
expect(target).to.be('this.*');
});
});
describe('lucene format', function() {
it('should properly escape $test with lucene escape sequences', function() {
initTemplateSrv([{type: 'query', name: 'test', current: {value: 'value/4' }}]);
var target = _templateSrv.replace('this:$test', {}, 'lucene');
expect(target).to.be("this:value\\\/4");
});
});
describe('format variable to string values', function() {
it('single value should return value', function() {
var result = _templateSrv.formatValue('test');
expect(result).to.be('test');
});
it('multi value and glob format should render glob string', function() {
var result = _templateSrv.formatValue(['test','test2'], 'glob');
expect(result).to.be('{test,test2}');
});
it('multi value and lucene should render as lucene expr', function() {
var result = _templateSrv.formatValue(['test','test2'], 'lucene');
expect(result).to.be('("test" OR "test2")');
});
it('multi value and regex format should render regex string', function() {
var result = _templateSrv.formatValue(['test.','test2'], 'regex');
expect(result).to.be('(test\\.|test2)');
});
it('multi value and pipe should render pipe string', function() {
var result = _templateSrv.formatValue(['test','test2'], 'pipe');
expect(result).to.be('test|test2');
});
it('slash should be properly escaped in regex format', function() {
var result = _templateSrv.formatValue('Gi3/14', 'regex');
expect(result).to.be('Gi3\\/14');
});
});
describe('can check if variable exists', function() {
beforeEach(function() {
initTemplateSrv([{type: 'query', name: 'test', current: { value: 'oogle' } }]);
});
it('should return true if exists', function() {
var result = _templateSrv.variableExists('$test');
expect(result).to.be(true);
});
});
describe('can hightlight variables in string', function() {
beforeEach(function() {
initTemplateSrv([{type: 'query', name: 'test', current: { value: 'oogle' } }]);
});
it('should insert html', function() {
var result = _templateSrv.highlightVariablesAsHtml('$test');
expect(result).to.be('<span class="template-variable">$test</span>');
});
it('should insert html anywhere in string', function() {
var result = _templateSrv.highlightVariablesAsHtml('this $test ok');
expect(result).to.be('this <span class="template-variable">$test</span> ok');
});
it('should ignore if variables does not exist', function() {
var result = _templateSrv.highlightVariablesAsHtml('this $google ok');
expect(result).to.be('this $google ok');
});
});
describe('updateTemplateData with simple value', function() {
beforeEach(function() {
initTemplateSrv([{type: 'query', name: 'test', current: { value: 'muuuu' } }]);
});
it('should set current value and update template data', function() {
var target = _templateSrv.replace('this.[[test]].filters');
expect(target).to.be('this.muuuu.filters');
});
});
describe('fillVariableValuesForUrl with multi value', function() {
beforeEach(function() {
initTemplateSrv([{type: 'query', name: 'test', current: { value: ['val1', 'val2'] }}]);
});
it('should set multiple url params', function() {
var params = {};
_templateSrv.fillVariableValuesForUrl(params);
expect(params['var-test']).to.eql(['val1', 'val2']);
});
});
describe('fillVariableValuesForUrl with multi value and scopedVars', function() {
beforeEach(function() {
initTemplateSrv([{type: 'query', name: 'test', current: { value: ['val1', 'val2'] }}]);
});
it('should set scoped value as url params', function() {
var params = {};
_templateSrv.fillVariableValuesForUrl(params, {'test': {value: 'val1'}});
expect(params['var-test']).to.eql('val1');
});
});
describe('replaceWithText', function() {
beforeEach(function() {
initTemplateSrv([
{type: 'query', name: 'server', current: { value: '{asd,asd2}', text: 'All' } },
{type: 'interval', name: 'period', current: { value: '$__auto_interval', text: 'auto' } }
]);
_templateSrv.setGrafanaVariable('$__auto_interval', '13m');
_templateSrv.updateTemplateData();
});
it('should replace with text except for grafanaVariables', function() {
var target = _templateSrv.replaceWithText('Server: $server, period: $period');
expect(target).to.be('Server: All, period: 13m');
});
});
});

View File

@ -0,0 +1,59 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import {containsVariable, assignModelProperties} from '../variable';
describe('containsVariable', function() {
describe('when checking if a string contains a variable', function() {
it('should find it with $var syntax', function() {
var contains = containsVariable('this.$test.filters', 'test');
expect(contains).to.be(true);
});
it('should not find it if only part matches with $var syntax', function() {
var contains = containsVariable('this.$ServerDomain.filters', 'Server');
expect(contains).to.be(false);
});
it('should find it with [[var]] syntax', function() {
var contains = containsVariable('this.[[test]].filters', 'test');
expect(contains).to.be(true);
});
it('should find it when part of segment', function() {
var contains = containsVariable('metrics.$env.$group-*', 'group');
expect(contains).to.be(true);
});
it('should find it its the only thing', function() {
var contains = containsVariable('$env', 'env');
expect(contains).to.be(true);
});
it('should be able to pass in multiple test strings', function() {
var contains = containsVariable('asd','asd2.$env', 'env');
expect(contains).to.be(true);
});
});
});
describe('assignModelProperties', function() {
it('only set properties defined in defaults', function() {
var target: any = {test: 'asd'};
assignModelProperties(target, {propA: 1, propB: 2}, {propB: 0});
expect(target.propB).to.be(2);
expect(target.test).to.be('asd');
});
it('use default value if not found on source', function() {
var target: any = {test: 'asd'};
assignModelProperties(target, {propA: 1, propB: 2}, {propC: 10});
expect(target.propC).to.be(10);
});
});

View File

@ -0,0 +1,142 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import '../all';
import _ from 'lodash';
import helpers from 'test/specs/helpers';
import {Emitter} from 'app/core/core';
describe('VariableSrv init', function() {
var ctx = new helpers.ControllerTestContext();
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.controllers'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location']));
beforeEach(angularMocks.inject(($rootScope, $q, $location, $injector) => {
ctx.$q = $q;
ctx.$rootScope = $rootScope;
ctx.$location = $location;
ctx.variableSrv = $injector.get('variableSrv');
ctx.$rootScope.$digest();
}));
function describeInitScenario(desc, fn) {
describe(desc, function() {
var scenario: any = {
urlParams: {},
setup: setupFn => {
scenario.setupFn = setupFn;
}
};
beforeEach(function() {
scenario.setupFn();
ctx.datasource = {};
ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when(scenario.queryResult));
ctx.datasourceSrv.get = sinon.stub().returns(ctx.$q.when(ctx.datasource));
ctx.datasourceSrv.getMetricSources = sinon.stub().returns(scenario.metricSources);
ctx.$location.search = sinon.stub().returns(scenario.urlParams);
ctx.dashboard = {templating: {list: scenario.variables}, events: new Emitter()};
ctx.variableSrv.init(ctx.dashboard);
ctx.$rootScope.$digest();
scenario.variables = ctx.variableSrv.variables;
});
fn(scenario);
});
}
['query', 'interval', 'custom', 'datasource'].forEach(type => {
describeInitScenario('when setting ' + type + ' variable via url', scenario => {
scenario.setup(() => {
scenario.variables = [{
name: 'apps',
type: type,
current: {text: "test", value: "test"},
options: [{text: "test", value: "test"}]
}];
scenario.urlParams["var-apps"] = "new";
});
it('should update current value', () => {
expect(scenario.variables[0].current.value).to.be("new");
expect(scenario.variables[0].current.text).to.be("new");
});
});
});
describe('given dependent variables', () => {
var variableList = [
{
name: 'app',
type: 'query',
query: '',
current: {text: "app1", value: "app1"},
options: [{text: "app1", value: "app1"}]
},
{
name: 'server',
type: 'query',
refresh: 1,
query: '$app.*',
current: {text: "server1", value: "server1"},
options: [{text: "server1", value: "server1"}]
},
];
describeInitScenario('when setting parent var from url', scenario => {
scenario.setup(() => {
scenario.variables = _.cloneDeep(variableList);
scenario.urlParams["var-app"] = "google";
scenario.queryResult = [{text: 'google-server1'}, {text: 'google-server2'}];
});
it('should update child variable', () => {
expect(scenario.variables[1].options.length).to.be(2);
expect(scenario.variables[1].current.text).to.be("google-server1");
});
it('should only update it once', () => {
expect(ctx.datasource.metricFindQuery.callCount).to.be(1);
});
});
});
describeInitScenario('when template variable is present in url multiple times', scenario => {
scenario.setup(() => {
scenario.variables = [{
name: 'apps',
type: 'query',
multi: true,
current: {text: "val1", value: "val1"},
options: [{text: "val1", value: "val1"}, {text: 'val2', value: 'val2'}, {text: 'val3', value: 'val3', selected: true}]
}];
scenario.urlParams["var-apps"] = ["val2", "val1"];
});
it('should update current value', function() {
var variable = ctx.variableSrv.variables[0];
expect(variable.current.value.length).to.be(2);
expect(variable.current.value[0]).to.be("val2");
expect(variable.current.value[1]).to.be("val1");
expect(variable.current.text).to.be("val2 + val1");
expect(variable.options[0].selected).to.be(true);
expect(variable.options[1].selected).to.be(true);
});
it('should set options that are not in value to selected false', function() {
var variable = ctx.variableSrv.variables[0];
expect(variable.options[2].selected).to.be(false);
});
});
});

View File

@ -0,0 +1,395 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import '../all';
import moment from 'moment';
import helpers from 'test/specs/helpers';
import {Emitter} from 'app/core/core';
describe('VariableSrv', function() {
var ctx = new helpers.ControllerTestContext();
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.controllers'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location']));
beforeEach(angularMocks.inject(($rootScope, $q, $location, $injector) => {
ctx.$q = $q;
ctx.$rootScope = $rootScope;
ctx.$location = $location;
ctx.variableSrv = $injector.get('variableSrv');
ctx.variableSrv.init({
templating: {list: []},
events: new Emitter(),
});
ctx.$rootScope.$digest();
}));
function describeUpdateVariable(desc, fn) {
describe(desc, function() {
var scenario: any = {};
scenario.setup = function(setupFn) {
scenario.setupFn = setupFn;
};
beforeEach(function() {
scenario.setupFn();
var ds: any = {};
ds.metricFindQuery = sinon.stub().returns(ctx.$q.when(scenario.queryResult));
ctx.datasourceSrv.get = sinon.stub().returns(ctx.$q.when(ds));
ctx.datasourceSrv.getMetricSources = sinon.stub().returns(scenario.metricSources);
scenario.variable = ctx.variableSrv.addVariable(scenario.variableModel);
ctx.variableSrv.updateOptions(scenario.variable);
ctx.$rootScope.$digest();
});
fn(scenario);
});
}
describeUpdateVariable('interval variable without auto', scenario => {
scenario.setup(() => {
scenario.variableModel = {type: 'interval', query: '1s,2h,5h,1d', name: 'test'};
});
it('should update options array', () => {
expect(scenario.variable.options.length).to.be(4);
expect(scenario.variable.options[0].text).to.be('1s');
expect(scenario.variable.options[0].value).to.be('1s');
});
});
//
// Interval variable update
//
describeUpdateVariable('interval variable with auto', scenario => {
scenario.setup(() => {
scenario.variableModel = {type: 'interval', query: '1s,2h,5h,1d', name: 'test', auto: true, auto_count: 10 };
var range = {
from: moment(new Date()).subtract(7, 'days').toDate(),
to: new Date()
};
ctx.timeSrv.timeRange = sinon.stub().returns(range);
ctx.templateSrv.setGrafanaVariable = sinon.spy();
});
it('should update options array', function() {
expect(scenario.variable.options.length).to.be(5);
expect(scenario.variable.options[0].text).to.be('auto');
expect(scenario.variable.options[0].value).to.be('$__auto_interval');
});
it('should set $__auto_interval', function() {
var call = ctx.templateSrv.setGrafanaVariable.getCall(0);
expect(call.args[0]).to.be('$__auto_interval');
expect(call.args[1]).to.be('12h');
});
});
//
// Query variable update
//
describeUpdateVariable('query variable with empty current object and refresh', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: '', name: 'test', current: {}};
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
});
it('should set current value to first option', function() {
expect(scenario.variable.options.length).to.be(2);
expect(scenario.variable.current.value).to.be('backend1');
});
});
describeUpdateVariable('query variable with multi select and new options does not contain some selected values', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {
type: 'query',
query: '',
name: 'test',
current: {
value: ['val1', 'val2', 'val3'],
text: 'val1 + val2 + val3'
}
};
scenario.queryResult = [{text: 'val2'}, {text: 'val3'}];
});
it('should update current value', function() {
expect(scenario.variable.current.value).to.eql(['val2', 'val3']);
expect(scenario.variable.current.text).to.eql('val2 + val3');
});
});
describeUpdateVariable('query variable with multi select and new options does not contain any selected values', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {
type: 'query',
query: '',
name: 'test',
current: {
value: ['val1', 'val2', 'val3'],
text: 'val1 + val2 + val3'
}
};
scenario.queryResult = [{text: 'val5'}, {text: 'val6'}];
});
it('should update current value with first one', function() {
expect(scenario.variable.current.value).to.eql('val5');
expect(scenario.variable.current.text).to.eql('val5');
});
});
describeUpdateVariable('query variable with multi select and $__all selected', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {
type: 'query',
query: '',
name: 'test',
includeAll: true,
current: {
value: ['$__all'],
text: 'All'
}
};
scenario.queryResult = [{text: 'val5'}, {text: 'val6'}];
});
it('should keep current All value', function() {
expect(scenario.variable.current.value).to.eql(['$__all']);
expect(scenario.variable.current.text).to.eql('All');
});
});
describeUpdateVariable('query variable with numeric results', function(scenario) {
scenario.setup(function() {
scenario.variableModel = { type: 'query', query: '', name: 'test', current: {} };
scenario.queryResult = [{text: 12, value: 12}];
});
it('should set current value to first option', function() {
expect(scenario.variable.current.value).to.be('12');
expect(scenario.variable.options[0].value).to.be('12');
expect(scenario.variable.options[0].text).to.be('12');
});
});
describeUpdateVariable('basic query variable', function(scenario) {
scenario.setup(function() {
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
});
it('should update options array', function() {
expect(scenario.variable.options.length).to.be(2);
expect(scenario.variable.options[0].text).to.be('backend1');
expect(scenario.variable.options[0].value).to.be('backend1');
expect(scenario.variable.options[1].value).to.be('backend2');
});
it('should select first option as value', function() {
expect(scenario.variable.current.value).to.be('backend1');
});
});
describeUpdateVariable('and existing value still exists in options', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test'};
scenario.variableModel.current = { value: 'backend2', text: 'backend2'};
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
});
it('should keep variable value', function() {
expect(scenario.variable.current.text).to.be('backend2');
});
});
describeUpdateVariable('and regex pattern exists', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test'};
scenario.variableModel.regex = '/apps.*(backend_[0-9]+)/';
scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_02.counters.req'}];
});
it('should extract and use match group', function() {
expect(scenario.variable.options[0].value).to.be('backend_01');
});
});
describeUpdateVariable('and regex pattern exists and no match', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test'};
scenario.variableModel.regex = '/apps.*(backendasd[0-9]+)/';
scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_02.counters.req'}];
});
it('should not add non matching items, None option should be added instead', function() {
expect(scenario.variable.options.length).to.be(1);
expect(scenario.variable.options[0].isNone).to.be(true);
});
});
describeUpdateVariable('regex pattern without slashes', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test'};
scenario.variableModel.regex = 'backend_01';
scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_02.counters.req'}];
});
it('should return matches options', function() {
expect(scenario.variable.options.length).to.be(1);
});
});
describeUpdateVariable('regex pattern remove duplicates', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test'};
scenario.variableModel.regex = 'backend_01';
scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_01.counters.req'}];
});
it('should return matches options', function() {
expect(scenario.variable.options.length).to.be(1);
});
});
describeUpdateVariable('with include All', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', includeAll: true};
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}];
});
it('should add All option', function() {
expect(scenario.variable.options[0].text).to.be('All');
expect(scenario.variable.options[0].value).to.be('$__all');
});
});
describeUpdateVariable('with include all and custom value', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', includeAll: true, allValue: '*'};
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}];
});
it('should add All option with custom value', function() {
expect(scenario.variable.options[0].value).to.be('$__all');
});
});
describeUpdateVariable('without sort', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', sort: 0};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options without sort', function() {
expect(scenario.variable.options[0].text).to.be('bbb2');
expect(scenario.variable.options[1].text).to.be('aaa10');
expect(scenario.variable.options[2].text).to.be('ccc3');
});
});
describeUpdateVariable('with alphabetical sort (asc)', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', sort: 1};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options with alphabetical sort', function() {
expect(scenario.variable.options[0].text).to.be('aaa10');
expect(scenario.variable.options[1].text).to.be('bbb2');
expect(scenario.variable.options[2].text).to.be('ccc3');
});
});
describeUpdateVariable('with alphabetical sort (desc)', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', sort: 2};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options with alphabetical sort', function() {
expect(scenario.variable.options[0].text).to.be('ccc3');
expect(scenario.variable.options[1].text).to.be('bbb2');
expect(scenario.variable.options[2].text).to.be('aaa10');
});
});
describeUpdateVariable('with numerical sort (asc)', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', sort: 3};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options with numerical sort', function() {
expect(scenario.variable.options[0].text).to.be('bbb2');
expect(scenario.variable.options[1].text).to.be('ccc3');
expect(scenario.variable.options[2].text).to.be('aaa10');
});
});
describeUpdateVariable('with numerical sort (desc)', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', sort: 4};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options with numerical sort', function() {
expect(scenario.variable.options[0].text).to.be('aaa10');
expect(scenario.variable.options[1].text).to.be('ccc3');
expect(scenario.variable.options[2].text).to.be('bbb2');
});
});
//
// datasource variable update
//
describeUpdateVariable('datasource variable with regex filter', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {
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 set only contain graphite ds and filtered using regex', function() {
expect(scenario.variable.options.length).to.be(2);
expect(scenario.variable.options[0].value).to.be('backend2_pee');
expect(scenario.variable.options[1].value).to.be('backend4_pee');
});
it('should keep current value if available', function() {
expect(scenario.variable.current.value).to.be('backend4_pee');
});
});
//
// Custom variable update
//
describeUpdateVariable('update custom variable', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'custom', query: 'hej, hop, asd', name: 'test'};
});
it('should update options array', function() {
expect(scenario.variable.options.length).to.be(3);
expect(scenario.variable.options[0].text).to.be('hej');
expect(scenario.variable.options[1].value).to.be('hop');
});
});
});

View File

@ -1,10 +1,9 @@
define([
'angular',
'lodash',
'./editorCtrl',
'./templateValuesSrv',
'app/core/utils/kbn',
],
function (angular, _) {
function (angular, _, kbn) {
'use strict';
var module = angular.module('grafana.services');
@ -16,6 +15,7 @@ function (angular, _) {
this._index = {};
this._texts = {};
this._grafanaVariables = {};
this._adhocVariables = {};
this.init = function(variables) {
this.variables = variables;
@ -24,19 +24,32 @@ function (angular, _) {
this.updateTemplateData = function() {
this._index = {};
this._filters = {};
for (var i = 0; i < this.variables.length; i++) {
var variable = this.variables[i];
// add adhoc filters to it's own index
if (variable.type === 'adhoc') {
this._adhocVariables[variable.datasource] = variable;
continue;
}
if (!variable.current || !variable.current.isNone && !variable.current.value) {
continue;
}
this._index[variable.name] = variable;
}
};
function regexEscape(value) {
return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&');
this.getAdhocFilters = function(datasourceName) {
var variable = this._adhocVariables[datasourceName];
if (variable) {
return variable.filters || [];
}
return []
};
function luceneEscape(value) {
return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, "\\$1");
@ -63,10 +76,10 @@ function (angular, _) {
switch(format) {
case "regex": {
if (typeof value === 'string') {
return regexEscape(value);
return kbn.regexEscape(value);
}
var escapedValues = _.map(value, regexEscape);
var escapedValues = _.map(value, kbn.regexEscape);
return '(' + escapedValues.join('|') + ')';
}
case "lucene": {
@ -97,17 +110,6 @@ function (angular, _) {
return match && (self._index[match[1] || match[2]] !== void 0);
};
this.containsVariable = function(str, variableName) {
if (!str) {
return false;
}
variableName = regexEscape(variableName);
var findVarRegex = new RegExp('\\$(' + variableName + ')(?:\\W|$)|\\[\\[(' + variableName + ')\\]\\]', 'g');
var match = findVarRegex.exec(str);
return match !== null;
};
this.highlightVariablesAsHtml = function(str) {
if (!str || !_.isString(str)) { return str; }
@ -196,18 +198,11 @@ function (angular, _) {
this.fillVariableValuesForUrl = function(params, scopedVars) {
_.each(this.variables, function(variable) {
var current = variable.current;
var value = current.value;
if (current.text === 'All') {
value = 'All';
}
if (scopedVars && scopedVars[variable.name] !== void 0) {
value = scopedVars[variable.name].value;
params['var-' + variable.name] = scopedVars[variable.name].value;
} else {
params['var-' + variable.name] = variable.getValueForUrl();
}
params['var-' + variable.name] = value;
});
};

View File

@ -166,8 +166,7 @@ function (angular, _, $, kbn) {
if (otherVariable === updatedVariable) {
return;
}
if ((otherVariable.type === "datasource" &&
templateSrv.containsVariable(otherVariable.regex, updatedVariable.name)) ||
if (templateSrv.containsVariable(otherVariable.regex, updatedVariable.name) ||
templateSrv.containsVariable(otherVariable.query, updatedVariable.name) ||
templateSrv.containsVariable(otherVariable.datasource, updatedVariable.name)) {
return self.updateOptions(otherVariable);
@ -188,6 +187,12 @@ function (angular, _, $, kbn) {
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() };
@ -271,7 +276,7 @@ function (angular, _, $, kbn) {
this.validateVariableSelectionState = function(variable) {
if (!variable.current) {
if (!variable.options.length) { return; }
if (!variable.options.length) { return $q.when(); }
return self.setVariableValue(variable, variable.options[0], false);
}

View File

@ -0,0 +1,40 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
export interface Variable {
setValue(option);
updateOptions();
dependsOn(variable);
setValueFromUrl(urlValue);
getValueForUrl();
getModel();
}
export var variableTypes = {};
export function assignModelProperties(target, source, defaults) {
_.forEach(defaults, function(value, key) {
target[key] = source[key] === undefined ? value : source[key];
});
}
export function containsVariable(...args: any[]) {
var variableName = args[args.length-1];
var str = args[0] || '';
for (var i = 1; i < args.length-1; i++) {
str += args[i] || '';
}
variableName = kbn.regexEscape(variableName);
var findVarRegex = new RegExp('\\$(' + variableName + ')(?:\\W|$)|\\[\\[(' + variableName + ')\\]\\]', 'g');
var match = findVarRegex.exec(str);
return match !== null;
}

View File

@ -0,0 +1,233 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
import coreModule from 'app/core/core_module';
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) {
// update time variant variables
$rootScope.$on('refresh', this.onDashboardRefresh.bind(this), $rootScope);
$rootScope.$on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this), $rootScope);
}
init(dashboard) {
this.variableLock = {};
this.dashboard = dashboard;
// create working class models representing variables
this.variables = dashboard.templating.list.map(this.createVariableFromModel.bind(this));
this.templateSrv.init(this.variables);
// register event to sync back to persisted model
this.dashboard.events.on('prepare-save-model', this.syncToDashboardModel.bind(this));
// init variables
for (let variable of this.variables) {
this.variableLock[variable.name] = this.$q.defer();
}
var queryParams = this.$location.search();
return this.$q.all(this.variables.map(variable => {
return this.processVariable(variable, queryParams);
}));
}
onDashboardRefresh() {
var promises = this.variables
.filter(variable => variable.refresh === 2)
.map(variable => {
var previousOptions = variable.options.slice();
return variable.updateOptions()
.then(this.variableUpdated.bind(this, variable))
.then(() => {
if (angular.toJson(previousOptions) !== angular.toJson(variable.options)) {
this.$rootScope.$emit('template-variable-value-updated');
}
});
});
return this.$q.all(promises);
}
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);
}
}
return this.$q.all(dependencies).then(() => {
var urlValue = queryParams['var-' + variable.name];
if (urlValue !== void 0) {
return variable.setValueFromUrl(urlValue).then(lock.resolve);
}
if (variable.refresh === 1 || variable.refresh === 2) {
return variable.updateOptions().then(lock.resolve);
}
lock.resolve();
}).finally(() => {
delete this.variableLock[variable.name];
});
}
createVariableFromModel(model) {
var ctor = variableTypes[model.type].ctor;
if (!ctor) {
throw "Unable to find variable constructor for " + model.type;
}
var variable = this.$injector.instantiate(ctor, {model: model});
return variable;
}
addVariable(model) {
var variable = this.createVariableFromModel(model);
this.variables.push(this.createVariableFromModel(variable));
return variable;
}
syncToDashboardModel() {
this.dashboard.templating.list = this.variables.map(variable => {
return variable.getModel();
});
}
updateOptions(variable) {
return variable.updateOptions();
}
variableUpdated(variable) {
// if there is a variable lock ignore cascading update because we are in a boot up scenario
if (this.variableLock[variable.name]) {
return this.$q.when();
}
// cascade updates to variables that use this variable
var promises = _.map(this.variables, otherVariable => {
if (otherVariable === variable) {
return;
}
if (otherVariable.dependsOn(variable)) {
return this.updateOptions(otherVariable);
}
});
return this.$q.all(promises);
}
selectOptionsForCurrentValue(variable) {
var i, y, value, option;
var selected: any = [];
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;
}
validateVariableSelectionState(variable) {
if (!variable.current) {
if (!variable.options.length) { return this.$q.when(); }
return variable.setValue(variable.options[0]);
}
if (_.isArray(variable.current.value)) {
var selected = this.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 variable.setValue(selected);
} else {
var currentOption = _.find(variable.options, {text: variable.current.text});
if (currentOption) {
return variable.setValue(currentOption);
} else {
if (!variable.options.length) { return Promise.resolve(); }
return variable.setValue(variable.options[0]);
}
}
}
setOptionFromUrl(variable, urlValue) {
var promise = this.$q.when();
if (variable.refresh) {
promise = variable.updateOptions();
}
return promise.then(() => {
var option = _.find(variable.options, op => {
return op.text === urlValue || op.value === urlValue;
});
option = option || {text: urlValue, value: urlValue};
return variable.setValue(option);
});
}
setOptionAsCurrent(variable, option) {
variable.current = _.cloneDeep(option);
if (_.isArray(variable.current.text)) {
variable.current.text = variable.current.text.join(' + ');
}
this.selectOptionsForCurrentValue(variable);
return this.variableUpdated(variable);
}
updateUrlParamsWithCurrentVariables() {
// update url
var params = this.$location.search();
// remove variable params
_.each(params, function(value, key) {
if (key.indexOf('var-') === 0) {
delete params[key];
}
});
// add new values
this.templateSrv.fillVariableValuesForUrl(params);
// update url
this.$location.search(params);
}
}
coreModule.service('variableSrv', VariableSrv);

View File

@ -1,5 +1,5 @@
<div class="variable-link-wrapper">
<a ng-click="vm.show()" class="variable-value-link">
<a ng-click="vm.show()" class="gf-form-label variable-value-link">
{{vm.linkText}}
<span ng-repeat="tag in vm.selectedTags" bs-tooltip='tag.valuesText' data-placement="bottom">
<span class="label-tag"tag-color-from-name="tag.text">
@ -10,7 +10,7 @@
<i class="fa fa-caret-down"></i>
</a>
<input type="text" class="hidden-input input-small" style="display: none" ng-keydown="vm.keyDown($event)" ng-model="vm.search.query" ng-change="vm.queryChanged()" ></input>
<input type="text" class="hidden-input input-small gf-form-input" style="display: none" ng-keydown="vm.keyDown($event)" ng-model="vm.search.query" ng-change="vm.queryChanged()" ></input>
<div class="variable-value-dropdown" ng-if="vm.dropdownVisible" ng-class="{'multi': vm.variable.multi, 'single': !vm.variable.multi}">
<div class="variable-options-wrapper">

View File

@ -288,6 +288,10 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
esQuery = header + '\n' + esQuery + '\n';
return this._post('_msearch?search_type=count', esQuery).then(function(res) {
if (!res.responses[0].aggregations) {
return [];
}
var buckets = res.responses[0].aggregations["1"].buckets;
return _.map(buckets, function(bucket) {
return {text: bucket.key, value: bucket.key};

View File

@ -7,6 +7,7 @@ import * as dateMath from 'app/core/utils/datemath';
import InfluxSeries from './influx_series';
import InfluxQuery from './influx_query';
import ResponseParser from './response_parser';
import InfluxQueryBuilder from './query_builder';
export default class InfluxDatasource {
type: string;
@ -20,6 +21,7 @@ export default class InfluxDatasource {
interval: any;
supportAnnotations: boolean;
supportMetrics: boolean;
supportAdhocFilters: boolean;
responseParser: any;
/** @ngInject */
@ -38,24 +40,29 @@ export default class InfluxDatasource {
this.interval = (instanceSettings.jsonData || {}).timeInterval;
this.supportAnnotations = true;
this.supportMetrics = true;
this.supportAdhocFilters = true;
this.responseParser = new ResponseParser();
}
query(options) {
var timeFilter = this.getTimeFilter(options);
var scopedVars = options.scopedVars ? _.cloneDeep(options.scopedVars) : {};
var targets = _.cloneDeep(options.targets);
var queryTargets = [];
var queryModel;
var i, y;
var allQueries = _.map(options.targets, (target) => {
var allQueries = _.map(targets, target => {
if (target.hide) { return ""; }
queryTargets.push(target);
// build query
var queryModel = new InfluxQuery(target, this.templateSrv, options.scopedVars);
var query = queryModel.render(true);
query = query.replace(/\$interval/g, (target.interval || options.interval));
return query;
scopedVars.interval = {value: target.interval || options.interval};
queryModel = new InfluxQuery(target, this.templateSrv, scopedVars);
return queryModel.render(true);
}).reduce((acc, current) => {
if (current !== "") {
acc += ";" + current;
@ -63,11 +70,21 @@ export default class InfluxDatasource {
return acc;
});
if (allQueries === '') {
return this.$q.when({data: []});
}
// add global adhoc filters to timeFilter
var adhocFilters = this.templateSrv.getAdhocFilters(this.name);
if (adhocFilters.length > 0 ) {
timeFilter += ' AND ' + queryModel.renderAdhocFilters(adhocFilters);
}
// replace grafana variables
allQueries = allQueries.replace(/\$timeFilter/g, timeFilter);
scopedVars.timeFilter = {value: timeFilter};
// replace templated variables
allQueries = this.templateSrv.replace(allQueries, options.scopedVars);
allQueries = this.templateSrv.replace(allQueries, scopedVars);
return this._seriesQuery(allQueries).then((data): any => {
if (!data || !data.results) {
@ -102,7 +119,7 @@ export default class InfluxDatasource {
}
}
return { data: seriesList };
return {data: seriesList};
});
};
@ -124,16 +141,23 @@ export default class InfluxDatasource {
};
metricFindQuery(query) {
var interpolated;
try {
interpolated = this.templateSrv.replace(query, null, 'regex');
} catch (err) {
return this.$q.reject(err);
}
var interpolated = this.templateSrv.replace(query, null, 'regex');
return this._seriesQuery(interpolated)
.then(_.curry(this.responseParser.parse)(query));
};
}
getTagKeys(options) {
var queryBuilder = new InfluxQueryBuilder({measurement: '', tags: []}, this.database);
var query = queryBuilder.buildExploreQuery('TAG_KEYS');
return this.metricFindQuery(query);
}
getTagValues(options) {
var queryBuilder = new InfluxQueryBuilder({measurement: '', tags: []}, this.database);
var query = queryBuilder.buildExploreQuery('TAG_VALUES', options.key);
return this.metricFindQuery(query);
}
_seriesQuery(query) {
if (!query) { return this.$q.when({results: []}); }
@ -141,7 +165,6 @@ export default class InfluxDatasource {
return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'});
}
serializeParams(params) {
if (!params) { return '';}

View File

@ -2,6 +2,7 @@
import _ from 'lodash';
import queryPart from './query_part';
import kbn from 'app/core/utils/kbn';
export default class InfluxQuery {
target: any;
@ -155,7 +156,7 @@ export default class InfluxQuery {
if (operator !== '>' && operator !== '<') {
value = "'" + value.replace(/\\/g, '\\\\') + "'";
}
} else if (interpolate){
} else if (interpolate) {
value = this.templateSrv.replace(value, this.scopedVars, 'regex');
}
@ -181,12 +182,26 @@ export default class InfluxQuery {
return policy + measurement;
}
interpolateQueryStr(value, variable, defaultFormatFn) {
// if no multi or include all do not regexEscape
if (!variable.multi && !variable.includeAll) {
return value;
}
if (typeof value === 'string') {
return kbn.regexEscape(value);
}
var escapedValues = _.map(value, kbn.regexEscape);
return escapedValues.join('|');
};
render(interpolate?) {
var target = this.target;
if (target.rawQuery) {
if (interpolate) {
return this.templateSrv.replace(target.query, this.scopedVars, 'regex');
return this.templateSrv.replace(target.query, this.scopedVars, this.interpolateQueryStr);
} else {
return target.query;
}
@ -236,4 +251,11 @@ export default class InfluxQuery {
return query;
}
renderAdhocFilters(filters) {
var conditions = _.map(filters, (tag, index) => {
return this.renderTagCondition(tag, index, false);
});
return conditions.join(' ');
}
}

View File

@ -237,6 +237,19 @@ describe('InfluxQuery', function() {
expect(query.target.select[0][2].type).to.be('math');
});
describe('when render adhoc filters', function() {
it('should generate correct query segment', function() {
var query = new InfluxQuery({measurement: 'cpu', }, templateSrv, {});
var queryText = query.renderAdhocFilters([
{key: 'key1', operator: '=', value: 'value1'},
{key: 'key2', operator: '!=', value: 'value2'},
]);
expect(queryText).to.be('"key1" = \'value1\' AND "key2" != \'value2\'');
});
});
});
});

View File

@ -40,7 +40,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
return backendSrv.datasourceRequest(options);
};
function regexEscape(value) {
function prometheusSpecialRegexEscape(value) {
return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
}
@ -51,10 +51,10 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
}
if (typeof value === 'string') {
return regexEscape(value);
return prometheusSpecialRegexEscape(value);
}
var escapedValues = _.map(value, regexEscape);
var escapedValues = _.map(value, prometheusSpecialRegexEscape);
return escapedValues.join('|');
};

View File

@ -48,7 +48,6 @@ $gf-form-margin: 0.25rem;
.gf-form-label {
padding: $input-padding-y $input-padding-x;
margin-right: $gf-form-margin;
line-height: $input-line-height;
flex-shrink: 0;
background-color: $input-label-bg;

View File

@ -1,6 +1,5 @@
.submenu-controls {
margin: 0 $panel-margin ($panel-margin*2) $panel-margin;
font-size: 16px;
}
.annotation-disabled, .annotation-disabled a {
@ -18,22 +17,19 @@
.submenu-item {
margin-right: 20px;
display: inline-block;
border-radius: 3px;
background-color: $panel-bg;
border: $panel-border;
margin-right: 10px;
margin-right: 15px;
display: inline-block;
float: left;
.fa-caret-down {
font-size: 75%;
position: relative;
top: 1px;
top: -1px;
left: 1px;
}
}
.variable-value-link {
font-size: 16px;
padding-right: 10px;
.label-tag {
margin: 0 5px;
@ -42,19 +38,9 @@
padding: 8px 7px;
box-sizing: content-box;
display: inline-block;
font-weight: normal;
display: inline-block;
color: $text-color;
}
.submenu-item-label {
padding: 8px 0px 8px 7px;
box-sizing: content-box;
display: inline-block;
font-weight: normal;
display: inline-block;
}
.variable-link-wrapper {
display: inline-block;
position: relative;

View File

@ -24,6 +24,22 @@ describe("Emitter", () => {
expect(sub2Called).to.be(true);
});
it('when subscribing twice', () => {
var events = new Emitter();
var sub1Called = 0;
function handler() {
sub1Called += 1;
}
events.on('test', handler);
events.on('test', handler);
events.emit('test', null);
expect(sub1Called).to.be(2);
});
it('should handle errors', () => {
var events = new Emitter();
var sub1Called = 0;

View File

@ -1,388 +0,0 @@
define([
'app/features/dashboard/dashboardSrv'
], function() {
'use strict';
describe('dashboardSrv', function() {
var _dashboardSrv;
beforeEach(module('grafana.services'));
beforeEach(module(function($provide) {
$provide.value('contextSrv', {
});
}));
beforeEach(inject(function(dashboardSrv) {
_dashboardSrv = dashboardSrv;
}));
describe('when creating new dashboard with defaults only', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({}, {});
});
it('should have title', function() {
expect(model.title).to.be('No Title');
});
it('should have meta', function() {
expect(model.meta.canSave).to.be(true);
expect(model.meta.canShare).to.be(true);
});
it('should have default properties', function() {
expect(model.rows.length).to.be(0);
});
});
describe('when getting next panel id', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
rows: [{ panels: [{ id: 5 }]}]
});
});
it('should return max id + 1', function() {
expect(model.getNextPanelId()).to.be(6);
});
});
describe('row and panel manipulation', function() {
var dashboard;
beforeEach(function() {
dashboard = _dashboardSrv.create({});
});
it('row span should sum spans', function() {
var spanLeft = dashboard.rowSpan({ panels: [{ span: 2 }, { span: 3 }] });
expect(spanLeft).to.be(5);
});
it('adding default should split span in half', function() {
dashboard.rows = [{ panels: [{ span: 12, id: 7 }] }];
dashboard.addPanel({span: 4}, dashboard.rows[0]);
expect(dashboard.rows[0].panels[0].span).to.be(6);
expect(dashboard.rows[0].panels[1].span).to.be(6);
expect(dashboard.rows[0].panels[1].id).to.be(8);
});
it('duplicate panel should try to add it to same row', function() {
var panel = { span: 4, attr: '123', id: 10 };
dashboard.rows = [{ panels: [panel] }];
dashboard.duplicatePanel(panel, dashboard.rows[0]);
expect(dashboard.rows[0].panels[0].span).to.be(4);
expect(dashboard.rows[0].panels[1].span).to.be(4);
expect(dashboard.rows[0].panels[1].attr).to.be('123');
expect(dashboard.rows[0].panels[1].id).to.be(11);
});
it('duplicate panel should remove repeat data', function() {
var panel = { span: 4, attr: '123', id: 10, repeat: 'asd', scopedVars: { test: 'asd' }};
dashboard.rows = [{ panels: [panel] }];
dashboard.duplicatePanel(panel, dashboard.rows[0]);
expect(dashboard.rows[0].panels[1].repeat).to.be(undefined);
expect(dashboard.rows[0].panels[1].scopedVars).to.be(undefined);
});
});
describe('when creating dashboard with editable false', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
editable: false
});
});
it('should set editable false', function() {
expect(model.editable).to.be(false);
});
});
describe('when creating dashboard with old schema', function() {
var model;
var graph;
var singlestat;
var table;
beforeEach(function() {
model = _dashboardSrv.create({
services: { filter: { time: { from: 'now-1d', to: 'now'}, list: [{}] }},
pulldowns: [
{type: 'filtering', enable: true},
{type: 'annotations', enable: true, annotations: [{name: 'old'}]}
],
rows: [
{
panels: [
{
type: 'graph', legend: true, aliasYAxis: { test: 2 },
y_formats: ['kbyte', 'ms'],
grid: {
min: 1,
max: 10,
rightMin: 5,
rightMax: 15,
leftLogBase: 1,
rightLogBase: 2,
threshold1: 200,
threshold2: 400,
threshold1Color: 'yellow',
threshold2Color: 'red',
},
leftYAxisLabel: 'left label',
targets: [{refId: 'A'}, {}],
},
{
type: 'singlestat', legend: true, thresholds: '10,20,30', aliasYAxis: { test: 2 }, grid: { min: 1, max: 10 },
targets: [{refId: 'A'}, {}],
},
{
type: 'table', legend: true, styles: [{ thresholds: ["10", "20", "30"]}, { thresholds: ["100", "200", "300"]}],
targets: [{refId: 'A'}, {}],
}
]
}
]
});
graph = model.rows[0].panels[0];
singlestat = model.rows[0].panels[1];
table = model.rows[0].panels[2];
});
it('should have title', function() {
expect(model.title).to.be('No Title');
});
it('should have panel id', function() {
expect(graph.id).to.be(1);
});
it('should move time and filtering list', function() {
expect(model.time.from).to.be('now-1d');
expect(model.templating.list[0].allFormat).to.be('glob');
});
it('graphite panel should change name too graph', function() {
expect(graph.type).to.be('graph');
});
it('single stat panel should have two thresholds', function() {
expect(singlestat.thresholds).to.be('20,30');
});
it('queries without refId should get it', function() {
expect(graph.targets[1].refId).to.be('B');
});
it('update legend setting', function() {
expect(graph.legend.show).to.be(true);
});
it('move aliasYAxis to series override', function() {
expect(graph.seriesOverrides[0].alias).to.be("test");
expect(graph.seriesOverrides[0].yaxis).to.be(2);
});
it('should move pulldowns to new schema', function() {
expect(model.annotations.list[0].name).to.be('old');
});
it('table panel should only have two thresholds values', function() {
expect(table.styles[0].thresholds[0]).to.be("20");
expect(table.styles[0].thresholds[1]).to.be("30");
expect(table.styles[1].thresholds[0]).to.be("200");
expect(table.styles[1].thresholds[1]).to.be("300");
});
it('graph grid to yaxes options', function() {
expect(graph.yaxes[0].min).to.be(1);
expect(graph.yaxes[0].max).to.be(10);
expect(graph.yaxes[0].format).to.be('kbyte');
expect(graph.yaxes[0].label).to.be('left label');
expect(graph.yaxes[0].logBase).to.be(1);
expect(graph.yaxes[1].min).to.be(5);
expect(graph.yaxes[1].max).to.be(15);
expect(graph.yaxes[1].format).to.be('ms');
expect(graph.yaxes[1].logBase).to.be(2);
expect(graph.grid.rightMax).to.be(undefined);
expect(graph.grid.rightLogBase).to.be(undefined);
expect(graph.y_formats).to.be(undefined);
});
it('dashboard schema version should be set to latest', function() {
expect(model.schemaVersion).to.be(13);
});
it('graph thresholds should be migrated', function() {
expect(graph.thresholds.length).to.be(2);
expect(graph.thresholds[0].op).to.be('>');
expect(graph.thresholds[0].value).to.be(400);
expect(graph.thresholds[0].fillColor).to.be('red');
expect(graph.thresholds[1].value).to.be(200);
expect(graph.thresholds[1].fillColor).to.be('yellow');
});
});
describe('when creating dashboard model with missing list for annoations or templating', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
annotations: {
enable: true,
},
templating: {
enable: true
}
});
});
it('should add empty list', function() {
expect(model.annotations.list.length).to.be(0);
expect(model.templating.list.length).to.be(0);
});
});
describe('Given editable false dashboard', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
editable: false,
});
});
it('Should set meta canEdit and canSave to false', function() {
expect(model.meta.canSave).to.be(false);
expect(model.meta.canEdit).to.be(false);
});
it('getSaveModelClone should remove meta', function() {
var clone = model.getSaveModelClone();
expect(clone.meta).to.be(undefined);
});
});
describe('when loading dashboard with old influxdb query schema', function() {
var model;
var target;
beforeEach(function() {
model = _dashboardSrv.create({
rows: [{
panels: [{
type: 'graph',
grid: {},
yaxes: [{}, {}],
targets: [{
"alias": "$tag_datacenter $tag_source $col",
"column": "value",
"measurement": "logins.count",
"fields": [
{
"func": "mean",
"name": "value",
"mathExpr": "*2",
"asExpr": "value"
},
{
"name": "one-minute",
"func": "mean",
"mathExpr": "*3",
"asExpr": "one-minute"
}
],
"tags": [],
"fill": "previous",
"function": "mean",
"groupBy": [
{
"interval": "auto",
"type": "time"
},
{
"key": "source",
"type": "tag"
},
{
"type": "tag",
"key": "datacenter"
}
],
}]
}]
}]
});
target = model.rows[0].panels[0].targets[0];
});
it('should update query schema', function() {
expect(target.fields).to.be(undefined);
expect(target.select.length).to.be(2);
expect(target.select[0].length).to.be(4);
expect(target.select[0][0].type).to.be('field');
expect(target.select[0][1].type).to.be('mean');
expect(target.select[0][2].type).to.be('math');
expect(target.select[0][3].type).to.be('alias');
});
});
describe('when creating dashboard model with missing list for annoations or templating', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
annotations: {
enable: true,
},
templating: {
enable: true
}
});
});
it('should add empty list', function() {
expect(model.annotations.list.length).to.be(0);
expect(model.templating.list.length).to.be(0);
});
});
describe('Formatting epoch timestamp when timezone is set as utc', function() {
var dashboard;
beforeEach(function() {
dashboard = _dashboardSrv.create({
timezone: 'utc',
});
});
it('Should format timestamp with second resolution by default', function() {
expect(dashboard.formatDate(1234567890000)).to.be('2009-02-13 23:31:30');
});
it('Should format timestamp with second resolution even if second format is passed as parameter', function() {
expect(dashboard.formatDate(1234567890007,'YYYY-MM-DD HH:mm:ss')).to.be('2009-02-13 23:31:30');
});
it('Should format timestamp with millisecond resolution if format is passed as parameter', function() {
expect(dashboard.formatDate(1234567890007,'YYYY-MM-DD HH:mm:ss.SSS')).to.be('2009-02-13 23:31:30.007');
});
});
});
});

View File

@ -1,267 +0,0 @@
define([
'../mocks/dashboard-mock',
'lodash',
'app/features/templating/templateSrv'
], function(dashboardMock) {
'use strict';
describe('templateSrv', function() {
var _templateSrv;
var _dashboard;
beforeEach(module('grafana.services'));
beforeEach(module(function() {
_dashboard = dashboardMock.create();
}));
beforeEach(inject(function(templateSrv) {
_templateSrv = templateSrv;
}));
describe('init', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]);
});
it('should initialize template data', function() {
var target = _templateSrv.replace('this.[[test]].filters');
expect(target).to.be('this.oogle.filters');
});
});
describe('replace can pass scoped vars', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]);
});
it('should replace $test with scoped value', function() {
var target = _templateSrv.replace('this.$test.filters', {'test': {value: 'mupp', text: 'asd'}});
expect(target).to.be('this.mupp.filters');
});
it('should replace $test with scoped text', function() {
var target = _templateSrv.replaceWithText('this.$test.filters', {'test': {value: 'mupp', text: 'asd'}});
expect(target).to.be('this.asd.filters');
});
});
describe('replace can pass multi / all format', function() {
beforeEach(function() {
_templateSrv.init([{name: 'test', current: {value: ['value1', 'value2'] }}]);
});
it('should replace $test with globbed value', function() {
var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).to.be('this.{value1,value2}.filters');
});
it('should replace $test with piped value', function() {
var target = _templateSrv.replace('this=$test', {}, 'pipe');
expect(target).to.be('this=value1|value2');
});
it('should replace $test with piped value', function() {
var target = _templateSrv.replace('this=$test', {}, 'pipe');
expect(target).to.be('this=value1|value2');
});
});
describe('variable with all option', function() {
beforeEach(function() {
_templateSrv.init([{
name: 'test',
current: {value: '$__all' },
options: [
{value: '$__all'}, {value: 'value1'}, {value: 'value2'}
]
}]);
});
it('should replace $test with formatted all value', function() {
var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).to.be('this.{value1,value2}.filters');
});
});
describe('variable with all option and custom value', function() {
beforeEach(function() {
_templateSrv.init([{
name: 'test',
current: {value: '$__all' },
allValue: '*',
options: [
{value: 'value1'}, {value: 'value2'}
]
}]);
});
it('should replace $test with formatted all value', function() {
var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).to.be('this.*.filters');
});
it('should not escape custom all value', function() {
var target = _templateSrv.replace('this.$test', {}, 'regex');
expect(target).to.be('this.*');
});
});
describe('lucene format', function() {
it('should properly escape $test with lucene escape sequences', function() {
_templateSrv.init([{name: 'test', current: {value: 'value/4' }}]);
var target = _templateSrv.replace('this:$test', {}, 'lucene');
expect(target).to.be("this:value\\\/4");
});
});
describe('format variable to string values', function() {
it('single value should return value', function() {
var result = _templateSrv.formatValue('test');
expect(result).to.be('test');
});
it('multi value and glob format should render glob string', function() {
var result = _templateSrv.formatValue(['test','test2'], 'glob');
expect(result).to.be('{test,test2}');
});
it('multi value and lucene should render as lucene expr', function() {
var result = _templateSrv.formatValue(['test','test2'], 'lucene');
expect(result).to.be('("test" OR "test2")');
});
it('multi value and regex format should render regex string', function() {
var result = _templateSrv.formatValue(['test.','test2'], 'regex');
expect(result).to.be('(test\\.|test2)');
});
it('multi value and pipe should render pipe string', function() {
var result = _templateSrv.formatValue(['test','test2'], 'pipe');
expect(result).to.be('test|test2');
});
it('slash should be properly escaped in regex format', function() {
var result = _templateSrv.formatValue('Gi3/14', 'regex');
expect(result).to.be('Gi3\\/14');
});
});
describe('can check if variable exists', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]);
});
it('should return true if exists', function() {
var result = _templateSrv.variableExists('$test');
expect(result).to.be(true);
});
});
describe('can hightlight variables in string', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]);
});
it('should insert html', function() {
var result = _templateSrv.highlightVariablesAsHtml('$test');
expect(result).to.be('<span class="template-variable">$test</span>');
});
it('should insert html anywhere in string', function() {
var result = _templateSrv.highlightVariablesAsHtml('this $test ok');
expect(result).to.be('this <span class="template-variable">$test</span> ok');
});
it('should ignore if variables does not exist', function() {
var result = _templateSrv.highlightVariablesAsHtml('this $google ok');
expect(result).to.be('this $google ok');
});
});
describe('when checking if a string contains a variable', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: 'muuuu' } }]);
});
it('should find it with $var syntax', function() {
var contains = _templateSrv.containsVariable('this.$test.filters', 'test');
expect(contains).to.be(true);
});
it('should not find it if only part matches with $var syntax', function() {
var contains = _templateSrv.containsVariable('this.$ServerDomain.filters', 'Server');
expect(contains).to.be(false);
});
it('should find it with [[var]] syntax', function() {
var contains = _templateSrv.containsVariable('this.[[test]].filters', 'test');
expect(contains).to.be(true);
});
it('should find it when part of segment', function() {
var contains = _templateSrv.containsVariable('metrics.$env.$group-*', 'group');
expect(contains).to.be(true);
});
it('should find it its the only thing', function() {
var contains = _templateSrv.containsVariable('$env', 'env');
expect(contains).to.be(true);
});
});
describe('updateTemplateData with simple value', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: 'muuuu' } }]);
});
it('should set current value and update template data', function() {
var target = _templateSrv.replace('this.[[test]].filters');
expect(target).to.be('this.muuuu.filters');
});
});
describe('fillVariableValuesForUrl with multi value', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: ['val1', 'val2'] }}]);
});
it('should set multiple url params', function() {
var params = {};
_templateSrv.fillVariableValuesForUrl(params);
expect(params['var-test']).to.eql(['val1', 'val2']);
});
});
describe('fillVariableValuesForUrl with multi value and scopedVars', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: ['val1', 'val2'] }}]);
});
it('should set multiple url params', function() {
var params = {};
_templateSrv.fillVariableValuesForUrl(params, {'test': {value: 'val1'}});
expect(params['var-test']).to.eql('val1');
});
});
describe('replaceWithText', function() {
beforeEach(function() {
_templateSrv.init([
{ name: 'server', current: { value: '{asd,asd2}', text: 'All' } },
{ name: 'period', current: { value: '$__auto_interval', text: 'auto' } }
]);
_templateSrv.setGrafanaVariable('$__auto_interval', '13m');
_templateSrv.updateTemplateData();
});
it('should replace with text except for grafanaVariables', function() {
var target = _templateSrv.replaceWithText('Server: $server, period: $period');
expect(target).to.be('Server: All, period: 13m');
});
});
});
});

View File

@ -1,9 +1,8 @@
define([
'../mocks/dashboard-mock',
'./helpers',
'moment',
'app/features/templating/templateValuesSrv'
], function(dashboardMock, helpers, moment) {
], function(dashboardMock, helpers) {
'use strict';
describe('templateValuesSrv', function() {
@ -13,21 +12,8 @@ define([
beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location']));
beforeEach(ctx.createService('templateValuesSrv'));
describe('update interval variable options', function() {
var variable = { type: 'interval', query: 'auto,1s,2h,5h,1d', name: 'test' };
beforeEach(function() {
ctx.service.updateOptions(variable);
});
it('should update options array', function() {
expect(variable.options.length).to.be(5);
expect(variable.options[1].text).to.be('1s');
expect(variable.options[1].value).to.be('1s');
});
});
describe('when template variable is present in url', function() {
describe('and setting simple variable', function() {
var variable = {
name: 'apps',
current: {text: "test", value: "test"},
@ -49,406 +35,24 @@ define([
});
});
describe('when template variable is present in url multiple times', function() {
var variable = {
name: 'apps',
multi: true,
current: {text: "val1", value: "val1"},
options: [{text: "val1", value: "val1"}, {text: 'val2', value: 'val2'}, {text: 'val3', value: 'val3', selected: true}]
};
beforeEach(function(done) {
var dashboard = { templating: { list: [variable] } };
var urlParams = {};
urlParams["var-apps"] = ["val2", "val1"];
ctx.$location.search = sinon.stub().returns(urlParams);
ctx.service.init(dashboard).then(function() { done(); });
ctx.$rootScope.$digest();
// 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'});
// });
// });
});
it('should update current value', function() {
expect(variable.current.value.length).to.be(2);
expect(variable.current.value[0]).to.be("val2");
expect(variable.current.value[1]).to.be("val1");
expect(variable.current.text).to.be("val2 + val1");
expect(variable.options[0].selected).to.be(true);
expect(variable.options[1].selected).to.be(true);
});
it('should set options that are not in value to selected false', function() {
expect(variable.options[2].selected).to.be(false);
});
});
function describeUpdateVariable(desc, fn) {
describe(desc, function() {
var scenario = {};
scenario.setup = function(setupFn) {
scenario.setupFn = setupFn;
};
beforeEach(function() {
scenario.setupFn();
var ds = {};
ds.metricFindQuery = sinon.stub().returns(ctx.$q.when(scenario.queryResult));
ctx.datasourceSrv.get = sinon.stub().returns(ctx.$q.when(ds));
ctx.datasourceSrv.getMetricSources = sinon.stub().returns(scenario.metricSources);
ctx.service.updateOptions(scenario.variable);
ctx.$rootScope.$digest();
});
fn(scenario);
});
}
describeUpdateVariable('interval variable without auto', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'interval', query: '1s,2h,5h,1d', name: 'test' };
});
it('should update options array', function() {
expect(scenario.variable.options.length).to.be(4);
expect(scenario.variable.options[0].text).to.be('1s');
expect(scenario.variable.options[0].value).to.be('1s');
});
});
describeUpdateVariable('query variable with empty current object and refresh', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: '', name: 'test', current: {} };
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
});
it('should set current value to first option', function() {
expect(scenario.variable.options.length).to.be(2);
expect(scenario.variable.current.value).to.be('backend1');
});
});
describeUpdateVariable('query variable with multi select and new options does not contain some selected values', function(scenario) {
scenario.setup(function() {
scenario.variable = {
type: 'query',
query: '',
name: 'test',
current: {
value: ['val1', 'val2', 'val3'],
text: 'val1 + val2 + val3'
}
};
scenario.queryResult = [{text: 'val2'}, {text: 'val3'}];
});
it('should update current value', function() {
expect(scenario.variable.current.value).to.eql(['val2', 'val3']);
expect(scenario.variable.current.text).to.eql('val2 + val3');
});
});
describeUpdateVariable('query variable with multi select and new options does not contain any selected values', function(scenario) {
scenario.setup(function() {
scenario.variable = {
type: 'query',
query: '',
name: 'test',
current: {
value: ['val1', 'val2', 'val3'],
text: 'val1 + val2 + val3'
}
};
scenario.queryResult = [{text: 'val5'}, {text: 'val6'}];
});
it('should update current value with first one', function() {
expect(scenario.variable.current.value).to.eql('val5');
expect(scenario.variable.current.text).to.eql('val5');
});
});
describeUpdateVariable('query variable with multi select and $__all selected', function(scenario) {
scenario.setup(function() {
scenario.variable = {
type: 'query',
query: '',
name: 'test',
includeAll: true,
current: {
value: ['$__all'],
text: 'All'
}
};
scenario.queryResult = [{text: 'val5'}, {text: 'val6'}];
});
it('should keep current All value', function() {
expect(scenario.variable.current.value).to.eql(['$__all']);
expect(scenario.variable.current.text).to.eql('All');
});
});
describeUpdateVariable('query variable with numeric results', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: '', name: 'test', current: {} };
scenario.queryResult = [{text: 12, value: 12}];
});
it('should set current value to first option', function() {
expect(scenario.variable.current.value).to.be('12');
expect(scenario.variable.options[0].value).to.be('12');
expect(scenario.variable.options[0].text).to.be('12');
});
});
describeUpdateVariable('interval variable without auto', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'interval', query: '1s,2h,5h,1d', name: 'test' };
});
it('should update options array', function() {
expect(scenario.variable.options.length).to.be(4);
expect(scenario.variable.options[0].text).to.be('1s');
expect(scenario.variable.options[0].value).to.be('1s');
});
});
describeUpdateVariable('interval variable with auto', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'interval', query: '1s,2h,5h,1d', name: 'test', auto: true, auto_count: 10 };
var range = {
from: moment(new Date()).subtract(7, 'days').toDate(),
to: new Date()
};
ctx.timeSrv.timeRange = sinon.stub().returns(range);
ctx.templateSrv.setGrafanaVariable = sinon.spy();
});
it('should update options array', function() {
expect(scenario.variable.options.length).to.be(5);
expect(scenario.variable.options[0].text).to.be('auto');
expect(scenario.variable.options[0].value).to.be('$__auto_interval');
});
it('should set $__auto_interval', function() {
var call = ctx.templateSrv.setGrafanaVariable.getCall(0);
expect(call.args[0]).to.be('$__auto_interval');
expect(call.args[1]).to.be('12h');
});
});
describeUpdateVariable('update custom variable', function(scenario) {
scenario.setup(function() {
scenario.variable = {type: 'custom', query: 'hej, hop, asd', name: 'test'};
});
it('should update options array', function() {
expect(scenario.variable.options.length).to.be(3);
expect(scenario.variable.options[0].text).to.be('hej');
expect(scenario.variable.options[1].value).to.be('hop');
});
it('should set $__auto_interval', function() {
var call = ctx.templateSrv.setGrafanaVariable.getCall(0);
expect(call.args[0]).to.be('$__auto_interval');
expect(call.args[1]).to.be('12h');
});
});
describeUpdateVariable('basic query variable', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
});
it('should update options array', function() {
expect(scenario.variable.options.length).to.be(2);
expect(scenario.variable.options[0].text).to.be('backend1');
expect(scenario.variable.options[0].value).to.be('backend1');
expect(scenario.variable.options[1].value).to.be('backend2');
});
it('should select first option as value', function() {
expect(scenario.variable.current.value).to.be('backend1');
});
});
describeUpdateVariable('and existing value still exists in options', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variable.current = { value: 'backend2', text: 'backend2'};
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
});
it('should keep variable value', function() {
expect(scenario.variable.current.text).to.be('backend2');
});
});
describeUpdateVariable('and regex pattern exists', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variable.regex = '/apps.*(backend_[0-9]+)/';
scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_02.counters.req'}];
});
it('should extract and use match group', function() {
expect(scenario.variable.options[0].value).to.be('backend_01');
});
});
describeUpdateVariable('and regex pattern exists and no match', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variable.regex = '/apps.*(backendasd[0-9]+)/';
scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_02.counters.req'}];
});
it('should not add non matching items, None option should be added instead', function() {
expect(scenario.variable.options.length).to.be(1);
expect(scenario.variable.options[0].isNone).to.be(true);
});
});
describeUpdateVariable('regex pattern without slashes', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variable.regex = 'backend_01';
scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_02.counters.req'}];
});
it('should return matches options', function() {
expect(scenario.variable.options.length).to.be(1);
});
});
describeUpdateVariable('regex pattern remove duplicates', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variable.regex = 'backend_01';
scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_01.counters.req'}];
});
it('should return matches options', function() {
expect(scenario.variable.options.length).to.be(1);
});
});
describeUpdateVariable('with include All', function(scenario) {
scenario.setup(function() {
scenario.variable = {type: 'query', query: 'apps.*', name: 'test', includeAll: true};
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}];
});
it('should add All option', function() {
expect(scenario.variable.options[0].text).to.be('All');
expect(scenario.variable.options[0].value).to.be('$__all');
});
});
describeUpdateVariable('with include all and custom value', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: 'apps.*', name: 'test', includeAll: true, allValue: '*' };
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}];
});
it('should add All option with custom value', function() {
expect(scenario.variable.options[0].value).to.be('$__all');
});
});
describeUpdateVariable('datasource variable with regex filter', function(scenario) {
scenario.setup(function() {
scenario.variable = {
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 set only contain graphite ds and filtered using regex', function() {
expect(scenario.variable.options.length).to.be(2);
expect(scenario.variable.options[0].value).to.be('backend2_pee');
expect(scenario.variable.options[1].value).to.be('backend4_pee');
});
it('should keep current value if available', function() {
expect(scenario.variable.current.value).to.be('backend4_pee');
});
});
describeUpdateVariable('without sort', function(scenario) {
scenario.setup(function() {
scenario.variable = {type: 'query', query: 'apps.*', name: 'test', sort: 0};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options without sort', function() {
expect(scenario.variable.options[0].text).to.be('bbb2');
expect(scenario.variable.options[1].text).to.be('aaa10');
expect(scenario.variable.options[2].text).to.be('ccc3');
});
});
describeUpdateVariable('with alphabetical sort (asc)', function(scenario) {
scenario.setup(function() {
scenario.variable = {type: 'query', query: 'apps.*', name: 'test', sort: 1};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options with alphabetical sort', function() {
expect(scenario.variable.options[0].text).to.be('aaa10');
expect(scenario.variable.options[1].text).to.be('bbb2');
expect(scenario.variable.options[2].text).to.be('ccc3');
});
});
describeUpdateVariable('with alphabetical sort (desc)', function(scenario) {
scenario.setup(function() {
scenario.variable = {type: 'query', query: 'apps.*', name: 'test', sort: 2};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options with alphabetical sort', function() {
expect(scenario.variable.options[0].text).to.be('ccc3');
expect(scenario.variable.options[1].text).to.be('bbb2');
expect(scenario.variable.options[2].text).to.be('aaa10');
});
});
describeUpdateVariable('with numerical sort (asc)', function(scenario) {
scenario.setup(function() {
scenario.variable = {type: 'query', query: 'apps.*', name: 'test', sort: 3};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options with numerical sort', function() {
expect(scenario.variable.options[0].text).to.be('bbb2');
expect(scenario.variable.options[1].text).to.be('ccc3');
expect(scenario.variable.options[2].text).to.be('aaa10');
});
});
describeUpdateVariable('with numerical sort (desc)', function(scenario) {
scenario.setup(function() {
scenario.variable = {type: 'query', query: 'apps.*', name: 'test', sort: 4};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options with numerical sort', function() {
expect(scenario.variable.options[0].text).to.be('aaa10');
expect(scenario.variable.options[1].text).to.be('ccc3');
expect(scenario.variable.options[2].text).to.be('bbb2');
});
});
});
});

View File

@ -1,6 +1,6 @@
define([
'app/features/dashboard/unsavedChangesSrv',
'app/features/dashboard/dashboardSrv'
'app/features/dashboard/dashboard_srv'
], function() {
'use strict';
@ -14,6 +14,7 @@ define([
var dash;
var scope;
beforeEach(module('grafana.core'));
beforeEach(module('grafana.services'));
beforeEach(module(function($provide) {
$provide.value('contextSrv', _contextSrvStub);