diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts
index 41d99c3198f..ac00528db20 100644
--- a/public/app/core/services/context_srv.ts
+++ b/public/app/core/services/context_srv.ts
@@ -9,6 +9,7 @@ export class User {
isGrafanaAdmin: any;
isSignedIn: any;
orgRole: any;
+ timezone: string;
constructor() {
if (config.bootData.user) {
diff --git a/public/app/core/utils/kbn.js b/public/app/core/utils/kbn.js
index c0d162b0ece..cf80d671d71 100644
--- a/public/app/core/utils/kbn.js
+++ b/public/app/core/utils/kbn.js
@@ -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) {
diff --git a/public/app/features/all.js b/public/app/features/all.js
index 43d9f33f190..cd7adb49de6 100644
--- a/public/app/features/all.js
+++ b/public/app/features/all.js
@@ -2,7 +2,7 @@ define([
'./panellinks/module',
'./dashlinks/module',
'./annotations/annotations_srv',
- './templating/templateSrv',
+ './templating/all',
'./dashboard/all',
'./playlist/all',
'./snapshot/all',
diff --git a/public/app/features/dashboard/ad_hoc_filters.ts b/public/app/features/dashboard/ad_hoc_filters.ts
new file mode 100644
index 00000000000..f962f8ca2f4
--- /dev/null
+++ b/public/app/features/dashboard/ad_hoc_filters.ts
@@ -0,0 +1,171 @@
+///
+
+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 = `
+
+`;
+
+export function adHocFiltersComponent() {
+ return {
+ restrict: 'E',
+ template: template,
+ controller: AdHocFiltersCtrl,
+ bindToController: true,
+ controllerAs: 'ctrl',
+ scope: {
+ variable: "="
+ }
+ };
+}
+
+coreModule.directive('adHocFilters', adHocFiltersComponent);
diff --git a/public/app/features/dashboard/all.js b/public/app/features/dashboard/all.js
index 6aea2efa9f1..b49b910f95f 100644
--- a/public/app/features/dashboard/all.js
+++ b/public/app/features/dashboard/all.js
@@ -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 () {});
diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js
deleted file mode 100644
index 66801b64f00..00000000000
--- a/public/app/features/dashboard/dashboardSrv.js
+++ /dev/null
@@ -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;
- },
- };
- });
-});
diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts
index 162331c4a98..4daf8ef6a68 100644
--- a/public/app/features/dashboard/dashboard_ctrl.ts
+++ b/public/app/features/dashboard/dashboard_ctrl.ts
@@ -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);
};
diff --git a/public/app/features/dashboard/dashboard_srv.ts b/public/app/features/dashboard/dashboard_srv.ts
new file mode 100644
index 00000000000..289e3a841f5
--- /dev/null
+++ b/public/app/features/dashboard/dashboard_srv.ts
@@ -0,0 +1,590 @@
+///
+
+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);
+
diff --git a/public/app/features/dashboard/specs/dashboard_srv_specs.ts b/public/app/features/dashboard/specs/dashboard_srv_specs.ts
new file mode 100644
index 00000000000..56ce355881a
--- /dev/null
+++ b/public/app/features/dashboard/specs/dashboard_srv_specs.ts
@@ -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');
+ });
+ });
+});
diff --git a/public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts b/public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts
index fbc1913ca30..0173c6569a5 100644
--- a/public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts
+++ b/public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts
@@ -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', {
diff --git a/public/app/features/dashboard/submenu/submenu.html b/public/app/features/dashboard/submenu/submenu.html
index 464d8c4cecf..1e1513d4021 100644
--- a/public/app/features/dashboard/submenu/submenu.html
+++ b/public/app/features/dashboard/submenu/submenu.html
@@ -1,10 +1,13 @@
-