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 @@ -