From 9b63a817564d3f3f711b47a4bab0e973b0592005 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 21 Dec 2017 13:22:20 +0100 Subject: [PATCH 01/14] migrated files to ts + fixed specfile --- public/app/features/dashboard/all.ts | 6 +- .../features/dashboard/dashboardLoaderSrv.js | 109 -------- .../dashboard/dashboard_loader_srv.ts | 158 ++++++++++++ ...SnapshotCtrl.js => share_snapshot_ctrl.ts} | 107 ++++---- .../specs/unsaved_changes_srv_specs.ts | 26 +- .../features/dashboard/unsavedChangesSrv.js | 189 -------------- .../features/dashboard/unsaved_changes_srv.ts | 236 ++++++++++++++++++ 7 files changed, 473 insertions(+), 358 deletions(-) delete mode 100644 public/app/features/dashboard/dashboardLoaderSrv.js create mode 100644 public/app/features/dashboard/dashboard_loader_srv.ts rename public/app/features/dashboard/{shareSnapshotCtrl.js => share_snapshot_ctrl.ts} (61%) delete mode 100644 public/app/features/dashboard/unsavedChangesSrv.js create mode 100644 public/app/features/dashboard/unsaved_changes_srv.ts diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index a3535c8fb35..fd79b7b1f03 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -1,18 +1,18 @@ import './dashboard_ctrl'; import './alerting_srv'; import './history/history'; -import './dashboardLoaderSrv'; +import './dashboard_loader_srv'; import './dashnav/dashnav'; import './submenu/submenu'; import './save_as_modal'; import './save_modal'; import './shareModalCtrl'; -import './shareSnapshotCtrl'; +import './share_snapshot_ctrl'; import './dashboard_srv'; import './view_state_srv'; import './validation_srv'; import './time_srv'; -import './unsavedChangesSrv'; +import './unsaved_changes_srv'; import './unsaved_changes_modal'; import './timepicker/timepicker'; import './upload'; diff --git a/public/app/features/dashboard/dashboardLoaderSrv.js b/public/app/features/dashboard/dashboardLoaderSrv.js deleted file mode 100644 index d5e257de665..00000000000 --- a/public/app/features/dashboard/dashboardLoaderSrv.js +++ /dev/null @@ -1,109 +0,0 @@ -define([ - 'angular', - 'moment', - 'lodash', - 'jquery', - 'app/core/utils/kbn', - 'app/core/utils/datemath', - 'app/core/services/impression_srv' -], -function (angular, moment, _, $, kbn, dateMath, impressionSrv) { - 'use strict'; - - kbn = kbn.default; - impressionSrv = impressionSrv.default; - - var module = angular.module('grafana.services'); - - module.service('dashboardLoaderSrv', function(backendSrv, - dashboardSrv, - datasourceSrv, - $http, $q, $timeout, - contextSrv, $routeParams, - $rootScope) { - var self = this; - - this._dashboardLoadFailed = function(title, snapshot) { - snapshot = snapshot || false; - return { - meta: { canStar: false, isSnapshot: snapshot, canDelete: false, canSave: false, canEdit: false, dashboardNotFound: true }, - dashboard: {title: title } - }; - }; - - this.loadDashboard = function(type, slug) { - var promise; - - if (type === 'script') { - promise = this._loadScriptedDashboard(slug); - } else if (type === 'snapshot') { - promise = backendSrv.get('/api/snapshots/' + $routeParams.slug) - .catch(function() { - return self._dashboardLoadFailed("Snapshot not found", true); - }); - } else { - promise = backendSrv.getDashboard($routeParams.type, $routeParams.slug) - .then(function(result) { - if (result.meta.isFolder) { - $rootScope.appEvent("alert-error", ['Dashboard not found']); - throw new Error("Dashboard not found"); - } - return result; - }) - .catch(function() { - return self._dashboardLoadFailed("Not found"); - }); - } - - promise.then(function(result) { - - if (result.meta.dashboardNotFound !== true) { - impressionSrv.addDashboardImpression(result.dashboard.id); - } - - return result; - }); - - return promise; - }; - - this._loadScriptedDashboard = function(file) { - var url = 'public/dashboards/'+file.replace(/\.(?!js)/,"/") + '?' + new Date().getTime(); - - return $http({ url: url, method: "GET" }) - .then(this._executeScript).then(function(result) { - return { meta: { fromScript: true, canDelete: false, canSave: false, canStar: false}, dashboard: result.data }; - }, function(err) { - console.log('Script dashboard error '+ err); - $rootScope.appEvent('alert-error', ["Script Error", "Please make sure it exists and returns a valid dashboard"]); - return self._dashboardLoadFailed('Scripted dashboard'); - }); - }; - - this._executeScript = function(result) { - var services = { - dashboardSrv: dashboardSrv, - datasourceSrv: datasourceSrv, - $q: $q, - }; - - /*jshint -W054 */ - var script_func = new Function('ARGS','kbn','dateMath','_','moment','window','document','$','jQuery', 'services', result.data); - var script_result = script_func($routeParams, kbn, dateMath, _ , moment, window, document, $, $, services); - - // Handle async dashboard scripts - if (_.isFunction(script_result)) { - var deferred = $q.defer(); - script_result(function(dashboard) { - $timeout(function() { - deferred.resolve({ data: dashboard }); - }); - }); - return deferred.promise; - } - - return { data: script_result }; - }; - - }); -}); diff --git a/public/app/features/dashboard/dashboard_loader_srv.ts b/public/app/features/dashboard/dashboard_loader_srv.ts new file mode 100644 index 00000000000..323b5e39177 --- /dev/null +++ b/public/app/features/dashboard/dashboard_loader_srv.ts @@ -0,0 +1,158 @@ +import angular from 'angular'; +import moment from 'moment'; +import _ from 'lodash'; +import $ from 'jquery'; +import kbn from 'app/core/utils/kbn'; +import * as dateMath from 'app/core/utils/datemath'; +import impressionSrv from 'app/core/services/impression_srv'; + +export class DashboardLoaderSrv { + /** @ngInject */ + constructor( + private backendSrv, + private dashboardSrv, + private datasourceSrv, + private $http, + private $q, + private $timeout, + contextSrv, + private $routeParams, + private $rootScope + ) {} + + _dashboardLoadFailed(title, snapshot) { + snapshot = snapshot || false; + return { + meta: { + canStar: false, + isSnapshot: snapshot, + canDelete: false, + canSave: false, + canEdit: false, + dashboardNotFound: true, + }, + dashboard: { title: title }, + }; + } + + loadDashboard(type, slug) { + var promise; + + if (type === 'script') { + promise = this._loadScriptedDashboard(slug); + } else if (type === 'snapshot') { + promise = this.backendSrv + .get('/api/snapshots/' + this.$routeParams.slug) + .catch(() => { + return this._dashboardLoadFailed('Snapshot not found', true); + }); + } else { + promise = this.backendSrv + .getDashboard(this.$routeParams.type, this.$routeParams.slug) + .then(result => { + if (result.meta.isFolder) { + this.$rootScope.appEvent('alert-error', ['Dashboard not found']); + throw new Error('Dashboard not found'); + } + return result; + }) + .catch(() => { + return this._dashboardLoadFailed('Not found', true); + }); + } + + promise.then(function(result) { + if (result.meta.dashboardNotFound !== true) { + impressionSrv.addDashboardImpression(result.dashboard.id); + } + + return result; + }); + + return promise; + } + + _loadScriptedDashboard(file) { + var url = + 'public/dashboards/' + + file.replace(/\.(?!js)/, '/') + + '?' + + new Date().getTime(); + + return this.$http({ url: url, method: 'GET' }) + .then(this._executeScript) + .then( + function(result) { + return { + meta: { + fromScript: true, + canDelete: false, + canSave: false, + canStar: false, + }, + dashboard: result.data, + }; + }, + function(err) { + console.log('Script dashboard error ' + err); + this.$rootScope.appEvent('alert-error', [ + 'Script Error', + 'Please make sure it exists and returns a valid dashboard', + ]); + return this._dashboardLoadFailed('Scripted dashboard'); + } + ); + } + + _executeScript(result) { + var services = { + dashboardSrv: this.dashboardSrv, + datasourceSrv: this.datasourceSrv, + $q: this.$q, + }; + + /*jshint -W054 */ + var script_func = new Function( + 'ARGS', + 'kbn', + 'dateMath', + '_', + 'moment', + 'window', + 'document', + '$', + 'jQuery', + 'services', + result.data + ); + var script_result = script_func( + this.$routeParams, + kbn, + dateMath, + _, + moment, + window, + document, + $, + $, + services + ); + + // Handle async dashboard scripts + if (_.isFunction(script_result)) { + var deferred = this.$q.defer(); + script_result(dashboard => { + this.$timeout(() => { + deferred.resolve({ data: dashboard }); + }); + }); + return deferred.promise; + } + + return { data: script_result }; + } +} + +angular + .module('grafana.services') + .service('dashboardLoaderSrv', DashboardLoaderSrv); diff --git a/public/app/features/dashboard/shareSnapshotCtrl.js b/public/app/features/dashboard/share_snapshot_ctrl.ts similarity index 61% rename from public/app/features/dashboard/shareSnapshotCtrl.js rename to public/app/features/dashboard/share_snapshot_ctrl.ts index 617e54db59c..7ab583e815a 100644 --- a/public/app/features/dashboard/shareSnapshotCtrl.js +++ b/public/app/features/dashboard/share_snapshot_ctrl.ts @@ -1,14 +1,8 @@ -define([ - 'angular', - 'lodash', -], -function (angular, _) { - 'use strict'; - - var module = angular.module('grafana.controllers'); - - module.controller('ShareSnapshotCtrl', function($scope, $rootScope, $location, backendSrv, $timeout, timeSrv) { +import angular from 'angular'; +import _ from 'lodash'; +export class ShareSnapshotCtrl { + constructor($scope, $rootScope, $location, backendSrv, $timeout, timeSrv) { $scope.snapshot = { name: $scope.dashboard.title, expires: 0, @@ -18,16 +12,16 @@ function (angular, _) { $scope.step = 1; $scope.expireOptions = [ - {text: '1 Hour', value: 60*60}, - {text: '1 Day', value: 60*60*24}, - {text: '7 Days', value: 60*60*24*7}, - {text: 'Never', value: 0}, + { text: '1 Hour', value: 60 * 60 }, + { text: '1 Day', value: 60 * 60 * 24 }, + { text: '7 Days', value: 60 * 60 * 24 * 7 }, + { text: 'Never', value: 0 }, ]; $scope.accessOptions = [ - {text: 'Anyone with the link', value: 1}, - {text: 'Organization users', value: 2}, - {text: 'Public on the web', value: 3}, + { text: 'Anyone with the link', value: 1 }, + { text: 'Organization users', value: 2 }, + { text: 'Public on the web', value: 3 }, ]; $scope.init = function() { @@ -42,7 +36,7 @@ function (angular, _) { $scope.createSnapshot = function(external) { $scope.dashboard.snapshot = { - timestamp: new Date() + timestamp: new Date(), }; if (!external) { @@ -69,31 +63,37 @@ function (angular, _) { expires: $scope.snapshot.expires, }; - var postUrl = external ? $scope.externalUrl + $scope.apiUrl : $scope.apiUrl; + var postUrl = external + ? $scope.externalUrl + $scope.apiUrl + : $scope.apiUrl; - backendSrv.post(postUrl, cmdData).then(function(results) { - $scope.loading = false; + backendSrv.post(postUrl, cmdData).then( + function(results) { + $scope.loading = false; - if (external) { - $scope.deleteUrl = results.deleteUrl; - $scope.snapshotUrl = results.url; - $scope.saveExternalSnapshotRef(cmdData, results); - } else { - var url = $location.url(); - var baseUrl = $location.absUrl(); + if (external) { + $scope.deleteUrl = results.deleteUrl; + $scope.snapshotUrl = results.url; + $scope.saveExternalSnapshotRef(cmdData, results); + } else { + var url = $location.url(); + var baseUrl = $location.absUrl(); - if (url !== '/') { - baseUrl = baseUrl.replace(url, '') + '/'; + if (url !== '/') { + baseUrl = baseUrl.replace(url, '') + '/'; + } + + $scope.snapshotUrl = baseUrl + 'dashboard/snapshot/' + results.key; + $scope.deleteUrl = + baseUrl + 'api/snapshots-delete/' + results.deleteKey; } - $scope.snapshotUrl = baseUrl + 'dashboard/snapshot/' + results.key; - $scope.deleteUrl = baseUrl + 'api/snapshots-delete/' + results.deleteKey; + $scope.step = 2; + }, + function() { + $scope.loading = false; } - - $scope.step = 2; - }, function() { - $scope.loading = false; - }); + ); }; $scope.getSnapshotUrl = function() { @@ -116,21 +116,22 @@ function (angular, _) { // remove annotation queries dash.annotations.list = _.chain(dash.annotations.list) - .filter(function(annotation) { - return annotation.enable; - }) - .map(function(annotation) { - return { - name: annotation.name, - enable: annotation.enable, - iconColor: annotation.iconColor, - snapshotData: annotation.snapshotData - }; - }).value(); + .filter(function(annotation) { + return annotation.enable; + }) + .map(function(annotation) { + return { + name: annotation.name, + enable: annotation.enable, + iconColor: annotation.iconColor, + snapshotData: annotation.snapshotData, + }; + }) + .value(); // remove template queries _.each(dash.templating.list, function(variable) { - variable.query = ""; + variable.query = ''; variable.options = variable.current; variable.refresh = false; }); @@ -168,7 +169,9 @@ function (angular, _) { cmdData.deleteKey = results.deleteKey; backendSrv.post('/api/snapshots/', cmdData); }; + } +} - }); - -}); +angular + .module('grafana.controllers') + .controller('ShareSnapshotCtrl', ShareSnapshotCtrl); diff --git a/public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts b/public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts index 79a0a237ca1..b510f76f114 100644 --- a/public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts +++ b/public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts @@ -6,14 +6,17 @@ import { sinon, angularMocks, } from 'test/lib/common'; -import 'app/features/dashboard/unsavedChangesSrv'; +import { Tracker } from 'app/features/dashboard/unsaved_changes_srv'; import 'app/features/dashboard/dashboard_srv'; +import { contextSrv } from 'app/core/core'; describe('unsavedChangesSrv', function() { - var _unsavedChangesSrv; var _dashboardSrv; var _contextSrvStub = { isEditor: true }; var _rootScope; + var _location; + var _timeout; + var _window; var tracker; var dash; var scope; @@ -32,11 +35,15 @@ describe('unsavedChangesSrv', function() { unsavedChangesSrv, $location, $rootScope, - dashboardSrv + dashboardSrv, + $timeout, + $window ) { - _unsavedChangesSrv = unsavedChangesSrv; _dashboardSrv = dashboardSrv; _rootScope = $rootScope; + _location = $location; + _timeout = $timeout; + _window = $window; }) ); @@ -54,7 +61,16 @@ describe('unsavedChangesSrv', function() { scope.appEvent = sinon.spy(); scope.onAppEvent = sinon.spy(); - tracker = new _unsavedChangesSrv.Tracker(dash, scope); + tracker = new Tracker( + dash, + scope, + undefined, + _location, + _window, + _timeout, + contextSrv, + _rootScope + ); }); it('No changes should not have changes', function() { diff --git a/public/app/features/dashboard/unsavedChangesSrv.js b/public/app/features/dashboard/unsavedChangesSrv.js deleted file mode 100644 index 7ffdb36952e..00000000000 --- a/public/app/features/dashboard/unsavedChangesSrv.js +++ /dev/null @@ -1,189 +0,0 @@ -define([ - 'angular', - 'lodash', -], -function(angular, _) { - 'use strict'; - - var module = angular.module('grafana.services'); - - module.service('unsavedChangesSrv', function($rootScope, $q, $location, $timeout, contextSrv, dashboardSrv, $window) { - - function Tracker(dashboard, scope, originalCopyDelay) { - var self = this; - - this.current = dashboard; - this.originalPath = $location.path(); - this.scope = scope; - - // register events - scope.onAppEvent('dashboard-saved', function() { - this.original = this.current.getSaveModelClone(); - this.originalPath = $location.path(); - }.bind(this)); - - $window.onbeforeunload = function() { - if (self.ignoreChanges()) { return; } - if (self.hasChanges()) { - return "There are unsaved changes to this dashboard"; - } - }; - - scope.$on("$locationChangeStart", function(event, next) { - // check if we should look for changes - if (self.originalPath === $location.path()) { return true; } - if (self.ignoreChanges()) { return true; } - - if (self.hasChanges()) { - event.preventDefault(); - self.next = next; - - $timeout(function() { - self.open_modal(); - }); - } - }); - - if (originalCopyDelay) { - $timeout(function() { - // wait for different services to patch the dashboard (missing properties) - self.original = dashboard.getSaveModelClone(); - }, originalCopyDelay); - } else { - self.original = dashboard.getSaveModelClone(); - } - } - - var p = Tracker.prototype; - - // for some dashboards and users - // changes should be ignored - p.ignoreChanges = function() { - if (!this.original) { return true; } - if (!contextSrv.isEditor) { return true; } - if (!this.current || !this.current.meta) { return true; } - - var meta = this.current.meta; - return !meta.canSave || meta.fromScript || meta.fromFile; - }; - - // remove stuff that should not count in diff - p.cleanDashboardFromIgnoredChanges = function(dash) { - // ignore time and refresh - dash.time = 0; - dash.refresh = 0; - dash.schemaVersion = 0; - - // filter row and panels properties that should be ignored - dash.rows = _.filter(dash.rows, function(row) { - if (row.repeatRowId) { - return false; - } - - row.panels = _.filter(row.panels, function(panel) { - if (panel.repeatPanelId) { - return false; - } - - // remove scopedVars - panel.scopedVars = null; - - // ignore span changes - panel.span = null; - - // ignore panel legend sort - if (panel.legend) { - delete panel.legend.sort; - delete panel.legend.sortDesc; - } - - return true; - }); - - // ignore collapse state - row.collapse = false; - return true; - }); - - dash.panels = _.filter(dash.panels, function(panel) { - if (panel.repeatPanelId) { - return false; - } - - // remove scopedVars - panel.scopedVars = null; - - // ignore panel legend sort - if (panel.legend) { - delete panel.legend.sort; - delete panel.legend.sortDesc; - } - - return true; - }); - - // ignore template variable values - _.each(dash.templating.list, function(value) { - value.current = null; - value.options = null; - value.filters = null; - }); - }; - - p.hasChanges = function() { - var current = this.current.getSaveModelClone(); - var original = this.original; - - this.cleanDashboardFromIgnoredChanges(current); - this.cleanDashboardFromIgnoredChanges(original); - - var currentTimepicker = _.find(current.nav, { type: 'timepicker' }); - var originalTimepicker = _.find(original.nav, { type: 'timepicker' }); - - if (currentTimepicker && originalTimepicker) { - currentTimepicker.now = originalTimepicker.now; - } - - var currentJson = angular.toJson(current); - var originalJson = angular.toJson(original); - - return currentJson !== originalJson; - }; - - p.discardChanges = function() { - this.original = null; - this.gotoNext(); - }; - - p.open_modal = function() { - $rootScope.appEvent('show-modal', { - templateHtml: '', - modalClass: 'modal--narrow confirm-modal' - }); - }; - - p.saveChanges = function() { - var self = this; - var cancel = $rootScope.$on('dashboard-saved', function() { - cancel(); - $timeout(function() { - self.gotoNext(); - }); - }); - - $rootScope.appEvent('save-dashboard'); - }; - - p.gotoNext = function() { - var baseLen = $location.absUrl().length - $location.url().length; - var nextUrl = this.next.substring(baseLen); - $location.url(nextUrl); - }; - - this.Tracker = Tracker; - this.init = function(dashboard, scope) { - this.tracker = new Tracker(dashboard, scope, 1000); - return this.tracker; - }; - }); -}); diff --git a/public/app/features/dashboard/unsaved_changes_srv.ts b/public/app/features/dashboard/unsaved_changes_srv.ts new file mode 100644 index 00000000000..e02466a91e0 --- /dev/null +++ b/public/app/features/dashboard/unsaved_changes_srv.ts @@ -0,0 +1,236 @@ +import angular from 'angular'; +import _ from 'lodash'; + +export class Tracker { + current: any; + originalPath: any; + scope: any; + original: any; + next: any; + $window: any; + + /** @ngInject */ + constructor( + dashboard, + scope, + originalCopyDelay, + private $location, + $window, + private $timeout, + private contextSrv, + private $rootScope + ) { + this.$location = $location; + this.$window = $window; + + this.current = dashboard; + this.originalPath = $location.path(); + this.scope = scope; + + // register events + scope.onAppEvent('dashboard-saved', () => { + this.original = this.current.getSaveModelClone(); + this.originalPath = $location.path(); + }); + + $window.onbeforeunload = () => { + if (this.ignoreChanges()) { + return ''; + } + if (this.hasChanges()) { + return 'There are unsaved changes to this dashboard'; + } + return ''; + }; + + scope.$on('$locationChangeStart', (event, next) => { + // check if we should look for changes + if (this.originalPath === $location.path()) { + return true; + } + if (this.ignoreChanges()) { + return true; + } + + if (this.hasChanges()) { + event.preventDefault(); + this.next = next; + + this.$timeout(() => { + this.open_modal(); + }); + } + return false; + }); + + if (originalCopyDelay) { + this.$timeout(() => { + // wait for different services to patch the dashboard (missing properties) + this.original = dashboard.getSaveModelClone(); + }, originalCopyDelay); + } else { + this.original = dashboard.getSaveModelClone(); + } + } + + // for some dashboards and users + // changes should be ignored + ignoreChanges() { + if (!this.original) { + return true; + } + if (!this.contextSrv.isEditor) { + return true; + } + if (!this.current || !this.current.meta) { + return true; + } + + var meta = this.current.meta; + return !meta.canSave || meta.fromScript || meta.fromFile; + } + + // remove stuff that should not count in diff + cleanDashboardFromIgnoredChanges(dash) { + // ignore time and refresh + dash.time = 0; + dash.refresh = 0; + dash.schemaVersion = 0; + + // filter row and panels properties that should be ignored + dash.rows = _.filter(dash.rows, function(row) { + if (row.repeatRowId) { + return false; + } + + row.panels = _.filter(row.panels, function(panel) { + if (panel.repeatPanelId) { + return false; + } + + // remove scopedVars + panel.scopedVars = null; + + // ignore span changes + panel.span = null; + + // ignore panel legend sort + if (panel.legend) { + delete panel.legend.sort; + delete panel.legend.sortDesc; + } + + return true; + }); + + // ignore collapse state + row.collapse = false; + return true; + }); + + dash.panels = _.filter(dash.panels, panel => { + if (panel.repeatPanelId) { + return false; + } + + // remove scopedVars + panel.scopedVars = null; + + // ignore panel legend sort + if (panel.legend) { + delete panel.legend.sort; + delete panel.legend.sortDesc; + } + + return true; + }); + + // ignore template variable values + _.each(dash.templating.list, function(value) { + value.current = null; + value.options = null; + value.filters = null; + }); + } + + hasChanges() { + var current = this.current.getSaveModelClone(); + var original = this.original; + + this.cleanDashboardFromIgnoredChanges(current); + this.cleanDashboardFromIgnoredChanges(original); + + var currentTimepicker = _.find(current.nav, { type: 'timepicker' }); + var originalTimepicker = _.find(original.nav, { type: 'timepicker' }); + + if (currentTimepicker && originalTimepicker) { + currentTimepicker.now = originalTimepicker.now; + } + + var currentJson = angular.toJson(current); + var originalJson = angular.toJson(original); + + return currentJson !== originalJson; + } + + discardChanges() { + this.original = null; + this.gotoNext(); + } + + open_modal() { + this.$rootScope.appEvent('show-modal', { + templateHtml: + '', + modalClass: 'modal--narrow confirm-modal', + }); + } + + saveChanges() { + var self = this; + var cancel = this.$rootScope.$on('dashboard-saved', () => { + cancel(); + this.$timeout(() => { + self.gotoNext(); + }); + }); + + this.$rootScope.appEvent('save-dashboard'); + } + + gotoNext() { + var baseLen = this.$location.absUrl().length - this.$location.url().length; + var nextUrl = this.next.substring(baseLen); + this.$location.url(nextUrl); + } +} + +/** @ngInject */ +export function unsavedChangesSrv( + $rootScope, + $q, + $location, + $timeout, + contextSrv, + dashboardSrv, + $window +) { + this.Tracker = Tracker; + this.init = function(dashboard, scope) { + this.tracker = new Tracker( + dashboard, + scope, + 1000, + $location, + $window, + $timeout, + contextSrv, + $rootScope + ); + return this.tracker; + }; +} + +angular + .module('grafana.services') + .service('unsavedChangesSrv', unsavedChangesSrv); From 972c3bc6352ef276304e4b2d05bcc0916f5694a9 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 21 Dec 2017 13:44:34 +0100 Subject: [PATCH 02/14] code formatting fix --- public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts b/public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts index b510f76f114..f9e7fb0db18 100644 --- a/public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts +++ b/public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts @@ -32,7 +32,6 @@ describe('unsavedChangesSrv', function() { beforeEach( angularMocks.inject(function( - unsavedChangesSrv, $location, $rootScope, dashboardSrv, From e480a38dc13bb98331f875db5d28b2fe9c3f56b3 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 22 Dec 2017 09:58:18 +0100 Subject: [PATCH 03/14] pagerduty: adds test for reading auto resolve setting --- pkg/services/alerting/notifiers/pagerduty_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/alerting/notifiers/pagerduty_test.go b/pkg/services/alerting/notifiers/pagerduty_test.go index 522bb133d77..ba8d229df13 100644 --- a/pkg/services/alerting/notifiers/pagerduty_test.go +++ b/pkg/services/alerting/notifiers/pagerduty_test.go @@ -10,7 +10,6 @@ import ( func TestPagerdutyNotifier(t *testing.T) { Convey("Pagerduty notifier tests", t, func() { - Convey("Parsing alert notification from settings", func() { Convey("empty settings should return error", func() { json := `{ }` @@ -29,7 +28,8 @@ func TestPagerdutyNotifier(t *testing.T) { Convey("settings should trigger incident", func() { json := ` { - "integrationKey": "abcdefgh0123456789" + "integrationKey": "abcdefgh0123456789", + "autoResolve": false }` settingsJSON, _ := simplejson.NewJson([]byte(json)) @@ -46,8 +46,8 @@ func TestPagerdutyNotifier(t *testing.T) { So(pagerdutyNotifier.Name, ShouldEqual, "pagerduty_testing") So(pagerdutyNotifier.Type, ShouldEqual, "pagerduty") So(pagerdutyNotifier.Key, ShouldEqual, "abcdefgh0123456789") + So(pagerdutyNotifier.AutoResolve, ShouldBeFalse) }) - }) }) } From 68457f56360542988f91f73e37c43c76987d1e26 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 21 Dec 2017 18:10:24 +0100 Subject: [PATCH 04/14] dashboard: copy panel to clipboard Adds a new menu item to panels, Copy to Clipboard, that will both copy the panel json to the clipboard and temporarily store the panel object in the browsers window object. The temporarily stored panel object are available in Add Panel from any dashboard for as long you don't refresh the browser. Fixes #10248, #1004 --- public/app/features/dashboard/all.ts | 1 + .../app/features/dashboard/dashboard_ctrl.ts | 7 ++- .../dashboard/dashgrid/AddPanelPanel.tsx | 51 +++++++++++++++---- .../dashboard/dashgrid/PanelContainer.ts | 1 + .../features/dashboard/panel_clipboard_srv.ts | 21 ++++++++ public/app/features/panel/panel_ctrl.ts | 20 ++++++++ public/app/features/panel/panel_header.ts | 6 +++ public/sass/base/font-awesome/_larger.scss | 2 +- public/sass/components/_panel_add_panel.scss | 4 ++ 9 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 public/app/features/dashboard/panel_clipboard_srv.ts diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index a3535c8fb35..267de52c609 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -27,6 +27,7 @@ import './acl/acl'; import './folder_picker/folder_picker'; import './move_to_folder_modal/move_to_folder'; import './settings/settings'; +import './panel_clipboard_srv'; import coreModule from 'app/core/core_module'; import { DashboardListCtrl } from './dashboard_list_ctrl'; diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 8b1c69ef7fe..3012f86fe31 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -22,7 +22,8 @@ export class DashboardCtrl implements PanelContainer { private unsavedChangesSrv, private dashboardViewStateSrv, public playlistSrv, - private panelLoader + private panelLoader, + private panelClipboardSrv ) { // temp hack due to way dashboards are loaded // can't use controllerAs on route yet @@ -122,6 +123,10 @@ export class DashboardCtrl implements PanelContainer { return this.panelLoader; } + getClipboardPanel() { + return this.panelClipboardSrv.getPanel(); + } + timezoneChanged() { this.$rootScope.$broadcast('refresh'); } diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index 75c8cbf36bc..78def4d47bc 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -2,8 +2,8 @@ import React from 'react'; import _ from 'lodash'; import config from 'app/core/config'; -import {PanelModel} from '../panel_model'; -import {PanelContainer} from './PanelContainer'; +import { PanelModel } from '../panel_model'; +import { PanelContainer } from './PanelContainer'; import ScrollBar from 'app/core/components/ScrollBar/ScrollBar'; export interface AddPanelPanelProps { @@ -14,6 +14,7 @@ export interface AddPanelPanelProps { export interface AddPanelPanelState { filter: string; panelPlugins: any[]; + clipboardPanel: any; } export class AddPanelPanel extends React.Component { @@ -22,45 +23,77 @@ export class AddPanelPanel extends React.Component item) .value(); // add special row type - panels.push({id: 'row', name: 'Row', sort: 8, info: {logos: {small: 'public/img/icn-row.svg'}}}); + panels.push({ id: 'row', name: 'Row', sort: 8, info: { logos: { small: 'public/img/icn-row.svg' } } }); // add sort by sort property return _.sortBy(panels, 'sort'); } + getClipboardPanel() { + return this.props.getPanelContainer().getClipboardPanel(); + } + onPanelSelected(panelPluginInfo) { const panelContainer = this.props.getPanelContainer(); const dashboard = panelContainer.getDashboard(); - const {gridPos} = this.props.panel; + const { gridPos } = this.props.panel; var newPanel: any = { type: panelPluginInfo.id, title: 'Panel Title', - gridPos: {x: gridPos.x, y: gridPos.y, w: gridPos.w, h: gridPos.h} + gridPos: { x: gridPos.x, y: gridPos.y, w: gridPos.w, h: gridPos.h }, }; if (panelPluginInfo.id === 'row') { newPanel.title = 'Row title'; - newPanel.gridPos = {x: 0, y: 0}; + newPanel.gridPos = { x: 0, y: 0 }; } dashboard.addPanel(newPanel); dashboard.removePanel(this.props.panel); } + onClipboardPanelSelected(panel) { + const panelContainer = this.props.getPanelContainer(); + const dashboard = panelContainer.getDashboard(); + + const { gridPos } = this.props.panel; + panel.gridPos.x = gridPos.x; + panel.gridPos.y = gridPos.y; + + dashboard.addPanel(panel); + dashboard.removePanel(this.props.panel); + } + + renderClipboardPanel(copiedPanel) { + const panel = copiedPanel.panel; + const title = `Paste copied panel '${panel.title}' from '${copiedPanel.dashboard}'`; + + return ( +
this.onClipboardPanelSelected(panel)} title={title}> +
+ +
+
Paste copied panel
+
+ ); + } + renderPanelItem(panel) { return (
this.onPanelSelected(panel)} title={panel.name}> @@ -75,11 +108,12 @@ export class AddPanelPanel extends React.Component
- + New Panel Select a visualization
+ {this.state.clipboardPanel && this.renderClipboardPanel(this.state.clipboardPanel)} {this.state.panelPlugins.map(this.renderPanelItem.bind(this))}
@@ -87,4 +121,3 @@ export class AddPanelPanel extends React.Component { diff --git a/public/app/features/panel/panel_header.ts b/public/app/features/panel/panel_header.ts index ca6ed68b648..de8c93d5038 100644 --- a/public/app/features/panel/panel_header.ts +++ b/public/app/features/panel/panel_header.ts @@ -51,6 +51,12 @@ function renderMenuItem(item, ctrl) { html += ` href="${item.href}"`; } + if (item.directives) { + for (let directive of item.directives) { + html += ` ${directive}`; + } + } + html += `>`; html += `${item.text}`; diff --git a/public/sass/base/font-awesome/_larger.scss b/public/sass/base/font-awesome/_larger.scss index 1efeef30392..5d7bebcda14 100644 --- a/public/sass/base/font-awesome/_larger.scss +++ b/public/sass/base/font-awesome/_larger.scss @@ -8,7 +8,7 @@ vertical-align: -15%; } .#{$fa-css-prefix}-2x { - font-size: 2em; + font-size: 2em !important; } .#{$fa-css-prefix}-3x { font-size: 3em; diff --git a/public/sass/components/_panel_add_panel.scss b/public/sass/components/_panel_add_panel.scss index a6b5aebd107..548d677ef47 100644 --- a/public/sass/components/_panel_add_panel.scss +++ b/public/sass/components/_panel_add_panel.scss @@ -65,3 +65,7 @@ .add-panel__item-img { height: calc(100% - 15px); } + +.add-panel__item-icon { + padding: 2px; +} From 281e519fab16a66da50b09ba48954995d5e234eb Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 22 Dec 2017 10:26:01 +0100 Subject: [PATCH 05/14] fix: remove unused code --- public/app/features/panel/panel_ctrl.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 47d48af0fa2..1cf4681515c 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -271,7 +271,6 @@ export class PanelCtrl { let editScope = this.$scope.$root.$new(); editScope.object = this.panel.getSaveModel(); editScope.updateHandler = this.replacePanel.bind(this); - editScope.enableCopy = true; this.publishAppEvent('show-modal', { src: 'public/app/partials/edit_json.html', From 7917efb31a90bcf695b3756f284ca2a0728ebad5 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 22 Dec 2017 10:54:48 +0100 Subject: [PATCH 06/14] pagerduty: fixes invalid default value autoResolve incident checkbox was set to disabled by default but the backend used enabled as default. This commit makes both use disabled by defualt fixes #10222 --- pkg/services/alerting/notifiers/pagerduty.go | 2 +- .../alerting/notifiers/pagerduty_test.go | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index 35f90a9e4b7..c4067abec3b 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -42,7 +42,7 @@ var ( ) func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) { - autoResolve := model.Settings.Get("autoResolve").MustBool(true) + autoResolve := model.Settings.Get("autoResolve").MustBool(false) key := model.Settings.Get("integrationKey").MustString() if key == "" { return nil, alerting.ValidationError{Reason: "Could not find integration key property in settings"} diff --git a/pkg/services/alerting/notifiers/pagerduty_test.go b/pkg/services/alerting/notifiers/pagerduty_test.go index ba8d229df13..1d2eeec4a52 100644 --- a/pkg/services/alerting/notifiers/pagerduty_test.go +++ b/pkg/services/alerting/notifiers/pagerduty_test.go @@ -25,6 +25,26 @@ func TestPagerdutyNotifier(t *testing.T) { So(err, ShouldNotBeNil) }) + Convey("auto resolve should default to false", func() { + json := `{ "integrationKey": "abcdefgh0123456789" }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "pagerduty_testing", + Type: "pagerduty", + Settings: settingsJSON, + } + + not, err := NewPagerdutyNotifier(model) + pagerdutyNotifier := not.(*PagerdutyNotifier) + + So(err, ShouldBeNil) + So(pagerdutyNotifier.Name, ShouldEqual, "pagerduty_testing") + So(pagerdutyNotifier.Type, ShouldEqual, "pagerduty") + So(pagerdutyNotifier.Key, ShouldEqual, "abcdefgh0123456789") + So(pagerdutyNotifier.AutoResolve, ShouldBeFalse) + }) + Convey("settings should trigger incident", func() { json := ` { From cd92d219cd935f3f438e26371ac694cff155ca87 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 22 Dec 2017 11:00:18 +0100 Subject: [PATCH 07/14] changelog: adds note about closing #10222 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb0d2e026b9..c76ab12984c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ Dashboard panels and rows are positioned using a gridPos object `{x: 0, y: 0, w: Config files for provisioning datasources as configuration have changed from `/conf/datasources` to `/conf/provisioning/datasources`. From `/etc/grafana/datasources` to `/etc/grafana/provisioning/datasources` when installed with deb/rpm packages. +The pagerduty notifier now defaults to not auto resolve incidents. More details at [#10222](https://github.com/grafana/grafana/issues/10222) + ## New Features * **Data Source Proxy**: Add support for whitelisting specified cookies that will be passed through to the data source when proxying data source requests [#5457](https://github.com/grafana/grafana/issues/5457), thanks [@robingustafsson](https://github.com/robingustafsson) * **Postgres/MySQL**: add __timeGroup macro for mysql [#9596](https://github.com/grafana/grafana/pull/9596), thanks [@svenklemm](https://github.com/svenklemm) @@ -55,6 +57,7 @@ From `/etc/grafana/datasources` to `/etc/grafana/provisioning/datasources` when * **Sensu**: Send alert message to sensu output [#9551](https://github.com/grafana/grafana/issues/9551), thx [@cjchand](https://github.com/cjchand) * **Singlestat**: suppress error when result contains no datapoints [#9636](https://github.com/grafana/grafana/issues/9636), thx [@utkarshcmu](https://github.com/utkarshcmu) * **Postgres/MySQL**: Control quoting in SQL-queries when using template variables [#9030](https://github.com/grafana/grafana/issues/9030), thanks [@svenklemm](https://github.com/svenklemm) +* **Pagerduty**: Pagerduty dont auto resolve incidents by default anymore. [#10222](https://github.com/grafana/grafana/issues/10222) # 4.6.3 (2017-12-14) From e234cf5b18f9577108b7b519ac8b00f8f94327c6 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 22 Dec 2017 12:22:42 +0100 Subject: [PATCH 08/14] prom: removes limitation of one query per tsdb call --- pkg/tsdb/prometheus/prometheus.go | 131 +++++++++++++------------ pkg/tsdb/prometheus/prometheus_test.go | 21 ++-- pkg/tsdb/prometheus/types.go | 1 + 3 files changed, 86 insertions(+), 67 deletions(-) diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index e798b92c6fe..1186fccbbf9 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -83,41 +83,48 @@ func (e *PrometheusExecutor) getClient(dsInfo *models.DataSource) (apiv1.API, er } func (e *PrometheusExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { - result := &tsdb.Response{} + result := &tsdb.Response{ + Results: map[string]*tsdb.QueryResult{}, + } client, err := e.getClient(dsInfo) if err != nil { return nil, err } - query, err := parseQuery(dsInfo, tsdbQuery.Queries, tsdbQuery) + querys, err := parseQuery(dsInfo, tsdbQuery.Queries, tsdbQuery) if err != nil { return nil, err } - timeRange := apiv1.Range{ - Start: query.Start, - End: query.End, - Step: query.Step, + for _, query := range querys { + timeRange := apiv1.Range{ + Start: query.Start, + End: query.End, + Step: query.Step, + } + + plog.Debug("Sending query", "start", timeRange.Start, "end", timeRange.End, "step", timeRange.Step, "query", query.Expr) + + span, ctx := opentracing.StartSpanFromContext(ctx, "alerting.prometheus") + span.SetTag("expr", query.Expr) + span.SetTag("start_unixnano", int64(query.Start.UnixNano())) + span.SetTag("stop_unixnano", int64(query.End.UnixNano())) + defer span.Finish() + + value, err := client.QueryRange(ctx, query.Expr, timeRange) + + if err != nil { + return nil, err + } + + queryResult, err := parseResponse(value, query) + if err != nil { + return nil, err + } + result.Results[query.RefId] = queryResult } - span, ctx := opentracing.StartSpanFromContext(ctx, "alerting.prometheus") - span.SetTag("expr", query.Expr) - span.SetTag("start_unixnano", int64(query.Start.UnixNano())) - span.SetTag("stop_unixnano", int64(query.End.UnixNano())) - defer span.Finish() - - value, err := client.QueryRange(ctx, query.Expr, timeRange) - - if err != nil { - return nil, err - } - - queryResult, err := parseResponse(value, query) - if err != nil { - return nil, err - } - result.Results = queryResult return result, nil } @@ -140,51 +147,54 @@ func formatLegend(metric model.Metric, query *PrometheusQuery) string { return string(result) } -func parseQuery(dsInfo *models.DataSource, queries []*tsdb.Query, queryContext *tsdb.TsdbQuery) (*PrometheusQuery, error) { - queryModel := queries[0] +func parseQuery(dsInfo *models.DataSource, queries []*tsdb.Query, queryContext *tsdb.TsdbQuery) ([]*PrometheusQuery, error) { + qs := []*PrometheusQuery{} + for _, queryModel := range queries { + expr, err := queryModel.Model.Get("expr").String() + if err != nil { + return nil, err + } - expr, err := queryModel.Model.Get("expr").String() - if err != nil { - return nil, err + format := queryModel.Model.Get("legendFormat").MustString("") + + start, err := queryContext.TimeRange.ParseFrom() + if err != nil { + return nil, err + } + + end, err := queryContext.TimeRange.ParseTo() + if err != nil { + return nil, err + } + + dsInterval, err := tsdb.GetIntervalFrom(dsInfo, queryModel.Model, time.Second*15) + if err != nil { + return nil, err + } + + intervalFactor := queryModel.Model.Get("intervalFactor").MustInt64(1) + interval := intervalCalculator.Calculate(queryContext.TimeRange, dsInterval) + step := time.Duration(int64(interval.Value) * intervalFactor) + + qs = append(qs, &PrometheusQuery{ + Expr: expr, + Step: step, + LegendFormat: format, + Start: start, + End: end, + RefId: queryModel.RefId, + }) } - format := queryModel.Model.Get("legendFormat").MustString("") - - start, err := queryContext.TimeRange.ParseFrom() - if err != nil { - return nil, err - } - - end, err := queryContext.TimeRange.ParseTo() - if err != nil { - return nil, err - } - - dsInterval, err := tsdb.GetIntervalFrom(dsInfo, queryModel.Model, time.Second*15) - if err != nil { - return nil, err - } - - intervalFactor := queryModel.Model.Get("intervalFactor").MustInt64(1) - interval := intervalCalculator.Calculate(queryContext.TimeRange, dsInterval) - step := time.Duration(int64(interval.Value) * intervalFactor) - - return &PrometheusQuery{ - Expr: expr, - Step: step, - LegendFormat: format, - Start: start, - End: end, - }, nil + return qs, nil } -func parseResponse(value model.Value, query *PrometheusQuery) (map[string]*tsdb.QueryResult, error) { - queryResults := make(map[string]*tsdb.QueryResult) +func parseResponse(value model.Value, query *PrometheusQuery) (*tsdb.QueryResult, error) { queryRes := tsdb.NewQueryResult() data, ok := value.(model.Matrix) if !ok { - return queryResults, fmt.Errorf("Unsupported result format: %s", value.Type().String()) + return queryRes, fmt.Errorf("Unsupported result format: %s", value.Type().String()) } for _, v := range data { @@ -204,6 +214,5 @@ func parseResponse(value model.Value, query *PrometheusQuery) (map[string]*tsdb. queryRes.Series = append(queryRes.Series, &series) } - queryResults["A"] = queryRes - return queryResults, nil + return queryRes, nil } diff --git a/pkg/tsdb/prometheus/prometheus_test.go b/pkg/tsdb/prometheus/prometheus_test.go index c551ab98112..efb42318214 100644 --- a/pkg/tsdb/prometheus/prometheus_test.go +++ b/pkg/tsdb/prometheus/prometheus_test.go @@ -60,9 +60,10 @@ func TestPrometheus(t *testing.T) { Convey("with 48h time range", func() { queryContext.TimeRange = tsdb.NewTimeRange("12h", "now") - model, err := parseQuery(dsInfo, queryModels, queryContext) - + models, err := parseQuery(dsInfo, queryModels, queryContext) So(err, ShouldBeNil) + + model := models[0] So(model.Step, ShouldEqual, time.Second*30) }) }) @@ -83,18 +84,22 @@ func TestPrometheus(t *testing.T) { Convey("with 48h time range", func() { queryContext.TimeRange = tsdb.NewTimeRange("48h", "now") - model, err := parseQuery(dsInfo, queryModels, queryContext) + models, err := parseQuery(dsInfo, queryModels, queryContext) So(err, ShouldBeNil) + + model := models[0] So(model.Step, ShouldEqual, time.Minute*2) }) Convey("with 1h time range", func() { queryContext.TimeRange = tsdb.NewTimeRange("1h", "now") - model, err := parseQuery(dsInfo, queryModels, queryContext) + models, err := parseQuery(dsInfo, queryModels, queryContext) So(err, ShouldBeNil) + + model := models[0] So(model.Step, ShouldEqual, time.Second*15) }) }) @@ -116,9 +121,11 @@ func TestPrometheus(t *testing.T) { Convey("with 48h time range", func() { queryContext.TimeRange = tsdb.NewTimeRange("48h", "now") - model, err := parseQuery(dsInfo, queryModels, queryContext) + models, err := parseQuery(dsInfo, queryModels, queryContext) So(err, ShouldBeNil) + + model := models[0] So(model.Step, ShouldEqual, time.Minute*20) }) }) @@ -139,9 +146,11 @@ func TestPrometheus(t *testing.T) { Convey("with 48h time range", func() { queryContext.TimeRange = tsdb.NewTimeRange("48h", "now") - model, err := parseQuery(dsInfo, queryModels, queryContext) + models, err := parseQuery(dsInfo, queryModels, queryContext) So(err, ShouldBeNil) + + model := models[0] So(model.Step, ShouldEqual, time.Minute*2) }) }) diff --git a/pkg/tsdb/prometheus/types.go b/pkg/tsdb/prometheus/types.go index 8ed665d0123..cf8c16682e8 100644 --- a/pkg/tsdb/prometheus/types.go +++ b/pkg/tsdb/prometheus/types.go @@ -8,4 +8,5 @@ type PrometheusQuery struct { LegendFormat string Start time.Time End time.Time + RefId string } From 52f30f6f00a837c30ee15f15e182bec29df44fea Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 22 Dec 2017 20:52:57 +0100 Subject: [PATCH 09/14] migrated file to ts (#10328) --- public/app/features/plugins/datasource_srv.js | 153 ------------------ public/app/features/plugins/datasource_srv.ts | 152 +++++++++++++++++ 2 files changed, 152 insertions(+), 153 deletions(-) delete mode 100644 public/app/features/plugins/datasource_srv.js create mode 100644 public/app/features/plugins/datasource_srv.ts diff --git a/public/app/features/plugins/datasource_srv.js b/public/app/features/plugins/datasource_srv.js deleted file mode 100644 index bdd9a921c3d..00000000000 --- a/public/app/features/plugins/datasource_srv.js +++ /dev/null @@ -1,153 +0,0 @@ -define([ - 'angular', - 'lodash', - 'app/core/core_module', - 'app/core/config', - './plugin_loader', -], -function (angular, _, coreModule, config, pluginLoader) { - 'use strict'; - - config = config.default; - - coreModule.default.service('datasourceSrv', function($q, $injector, $rootScope, templateSrv) { - var self = this; - - this.init = function() { - this.datasources = {}; - }; - - this.get = function(name) { - if (!name) { - return this.get(config.defaultDatasource); - } - - name = templateSrv.replace(name); - - if (name === 'default') { - return this.get(config.defaultDatasource); - } - - if (this.datasources[name]) { - return $q.when(this.datasources[name]); - } - - return this.loadDatasource(name); - }; - - this.loadDatasource = function(name) { - var dsConfig = config.datasources[name]; - if (!dsConfig) { - return $q.reject({message: "Datasource named " + name + " was not found"}); - } - - var deferred = $q.defer(); - var pluginDef = dsConfig.meta; - - pluginLoader.importPluginModule(pluginDef.module).then(function(plugin) { - // check if its in cache now - if (self.datasources[name]) { - deferred.resolve(self.datasources[name]); - return; - } - - // plugin module needs to export a constructor function named Datasource - if (!plugin.Datasource) { - throw "Plugin module is missing Datasource constructor"; - } - - var instance = $injector.instantiate(plugin.Datasource, {instanceSettings: dsConfig}); - instance.meta = pluginDef; - instance.name = name; - self.datasources[name] = instance; - deferred.resolve(instance); - }).catch(function(err) { - $rootScope.appEvent('alert-error', [dsConfig.name + ' plugin failed', err.toString()]); - }); - - return deferred.promise; - }; - - this.getAll = function() { - return config.datasources; - }; - - this.getAnnotationSources = function() { - var sources = []; - - this.addDataSourceVariables(sources); - - _.each(config.datasources, function(value) { - if (value.meta && value.meta.annotations) { - sources.push(value); - } - }); - - return sources; - }; - - this.getMetricSources = function(options) { - var metricSources = []; - - _.each(config.datasources, function(value, key) { - if (value.meta && value.meta.metrics) { - metricSources.push({value: key, name: key, meta: value.meta}); - - if (key === config.defaultDatasource) { - metricSources.push({value: null, name: 'default', meta: value.meta}); - } - } - }); - - if (!options || !options.skipVariables) { - this.addDataSourceVariables(metricSources); - } - - metricSources.sort(function(a, b) { - // these two should always be at the bottom - if (a.meta.id === "mixed" || a.meta.id === "grafana") { - return 1; - } - if (b.meta.id === "mixed" || b.meta.id === "grafana") { - return -1; - } - if (a.name.toLowerCase() > b.name.toLowerCase()) { - return 1; - } - if (a.name.toLowerCase() < b.name.toLowerCase()) { - return -1; - } - return 0; - }); - - return metricSources; - }; - - this.addDataSourceVariables = function(list) { - // look for data source variables - for (var i = 0; i < templateSrv.variables.length; i++) { - var variable = templateSrv.variables[i]; - if (variable.type !== 'datasource') { - continue; - } - - var first = variable.current.value; - if (first === 'default') { - first = config.defaultDatasource; - } - - var ds = config.datasources[first]; - - if (ds) { - list.push({ - name: '$' + variable.name, - value: '$' + variable.name, - meta: ds.meta, - }); - } - } - }; - - this.init(); - }); -}); diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts new file mode 100644 index 00000000000..423845a7cc0 --- /dev/null +++ b/public/app/features/plugins/datasource_srv.ts @@ -0,0 +1,152 @@ +import _ from 'lodash'; +import coreModule from 'app/core/core_module'; +import config from 'app/core/config'; +// import pluginLoader from './plugin_loader'; +import { importPluginModule } from './plugin_loader'; + +export class DatasourceSrv { + datasources: any; + + constructor(private $q, private $injector, $rootScope, private templateSrv) { + this.init(); + } + + init() { + this.datasources = {}; + } + + get(name) { + if (!name) { + return this.get(config.defaultDatasource); + } + + name = this.templateSrv.replace(name); + + if (name === 'default') { + return this.get(config.defaultDatasource); + } + + if (this.datasources[name]) { + return this.$q.when(this.datasources[name]); + } + + return this.loadDatasource(name); + } + + loadDatasource(name) { + var dsConfig = config.datasources[name]; + if (!dsConfig) { + return this.$q.reject({ message: 'Datasource named ' + name + ' was not found' }); + } + + var deferred = this.$q.defer(); + var pluginDef = dsConfig.meta; + + importPluginModule(pluginDef.module) + .then(plugin => { + // check if its in cache now + if (this.datasources[name]) { + deferred.resolve(this.datasources[name]); + return; + } + + // plugin module needs to export a constructor function named Datasource + if (!plugin.Datasource) { + throw new Error('Plugin module is missing Datasource constructor'); + } + + var instance = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig }); + instance.meta = pluginDef; + instance.name = name; + this.datasources[name] = instance; + deferred.resolve(instance); + }) + .catch(function(err) { + this.$rootScope.appEvent('alert-error', [dsConfig.name + ' plugin failed', err.toString()]); + }); + + return deferred.promise; + } + + getAll() { + return config.datasources; + } + + getAnnotationSources() { + var sources = []; + + this.addDataSourceVariables(sources); + + _.each(config.datasources, function(value) { + if (value.meta && value.meta.annotations) { + sources.push(value); + } + }); + + return sources; + } + + getMetricSources(options) { + var metricSources = []; + + _.each(config.datasources, function(value, key) { + if (value.meta && value.meta.metrics) { + metricSources.push({ value: key, name: key, meta: value.meta }); + + if (key === config.defaultDatasource) { + metricSources.push({ value: null, name: 'default', meta: value.meta }); + } + } + }); + + if (!options || !options.skipVariables) { + this.addDataSourceVariables(metricSources); + } + + metricSources.sort(function(a, b) { + // these two should always be at the bottom + if (a.meta.id === 'mixed' || a.meta.id === 'grafana') { + return 1; + } + if (b.meta.id === 'mixed' || b.meta.id === 'grafana') { + return -1; + } + if (a.name.toLowerCase() > b.name.toLowerCase()) { + return 1; + } + if (a.name.toLowerCase() < b.name.toLowerCase()) { + return -1; + } + return 0; + }); + + return metricSources; + } + + addDataSourceVariables(list) { + // look for data source variables + for (var i = 0; i < this.templateSrv.variables.length; i++) { + var variable = this.templateSrv.variables[i]; + if (variable.type !== 'datasource') { + continue; + } + + var first = variable.current.value; + if (first === 'default') { + first = config.defaultDatasource; + } + + var ds = config.datasources[first]; + + if (ds) { + list.push({ + name: '$' + variable.name, + value: '$' + variable.name, + meta: ds.meta, + }); + } + } + } +} + +coreModule.service('datasourceSrv', DatasourceSrv); From 24723cdb3c02dbc814687ac17081e2998ee81bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 23 Dec 2017 08:34:48 +0100 Subject: [PATCH 10/14] fix: fixed issue with optimized build, fixes #10333 --- public/app/features/plugins/datasource_srv.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index 423845a7cc0..fb7a9ece37a 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -1,12 +1,12 @@ import _ from 'lodash'; import coreModule from 'app/core/core_module'; import config from 'app/core/config'; -// import pluginLoader from './plugin_loader'; import { importPluginModule } from './plugin_loader'; export class DatasourceSrv { datasources: any; + /** @ngInject */ constructor(private $q, private $injector, $rootScope, private templateSrv) { this.init(); } From 6a6633ab86b4d70c240261712466b494b3c2a011 Mon Sep 17 00:00:00 2001 From: Julien Pivotto Date: Sat, 23 Dec 2017 14:57:37 +0100 Subject: [PATCH 11/14] Fix small singlestat value display This fix improves the rendering of singlestats in small boxes in grafana 5. This allows the user to get boxes oh height=1 and still see the value of the stat entirely. Signed-off-by: Julien Pivotto --- public/sass/components/_panel_singlestat.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/public/sass/components/_panel_singlestat.scss b/public/sass/components/_panel_singlestat.scss index 0510c46cd47..33a956a0244 100644 --- a/public/sass/components/_panel_singlestat.scss +++ b/public/sass/components/_panel_singlestat.scss @@ -6,6 +6,7 @@ } .singlestat-panel-value-container { + line-height: 1; display: table-cell; vertical-align: middle; text-align: center; From 6d5628f2eaa7032b7b6d65a7a6e53fd0e42e1f77 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 26 Dec 2017 19:59:03 +0900 Subject: [PATCH 12/14] ignore trailing whitespace (#10344) --- .../app/plugins/datasource/prometheus/metric_find_query.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.ts b/public/app/plugins/datasource/prometheus/metric_find_query.ts index 9c834b7157d..b27f1cd50af 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.ts @@ -12,9 +12,9 @@ export default class PrometheusMetricFindQuery { } process() { - var label_values_regex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]+)\)$/; - var metric_names_regex = /^metrics\((.+)\)$/; - var query_result_regex = /^query_result\((.+)\)$/; + var label_values_regex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]+)\)\s*$/; + var metric_names_regex = /^metrics\((.+)\)\s*$/; + var query_result_regex = /^query_result\((.+)\)\s*$/; var label_values_query = this.query.match(label_values_regex); if (label_values_query) { From c11cf188794ee96b40078cb1ec1adb0ab27a2da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 26 Dec 2017 13:20:45 +0100 Subject: [PATCH 13/14] refactor: tried to simplify and also minimize scope a bit for #10323 --- public/app/core/constants.ts | 2 + public/app/features/dashboard/all.ts | 1 - .../app/features/dashboard/dashboard_ctrl.ts | 7 +- .../dashboard/dashgrid/AddPanelPanel.tsx | 69 ++++++++----------- .../dashboard/dashgrid/PanelContainer.ts | 1 - .../features/dashboard/panel_clipboard_srv.ts | 21 ------ public/app/features/panel/panel_ctrl.ts | 29 ++++---- public/app/features/panel/panel_header.ts | 6 -- 8 files changed, 44 insertions(+), 92 deletions(-) delete mode 100644 public/app/features/dashboard/panel_clipboard_srv.ts diff --git a/public/app/core/constants.ts b/public/app/core/constants.ts index a45a14f1ea2..2642c5e400a 100644 --- a/public/app/core/constants.ts +++ b/public/app/core/constants.ts @@ -6,3 +6,5 @@ export const REPEAT_DIR_VERTICAL = 'v'; export const DEFAULT_PANEL_SPAN = 4; export const DEFAULT_ROW_HEIGHT = 250; export const MIN_PANEL_HEIGHT = GRID_CELL_HEIGHT * 3; + +export const LS_PANEL_COPY_KEY = 'panel-copy'; diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index 72d1a696651..fd79b7b1f03 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -27,7 +27,6 @@ import './acl/acl'; import './folder_picker/folder_picker'; import './move_to_folder_modal/move_to_folder'; import './settings/settings'; -import './panel_clipboard_srv'; import coreModule from 'app/core/core_module'; import { DashboardListCtrl } from './dashboard_list_ctrl'; diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 3012f86fe31..8b1c69ef7fe 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -22,8 +22,7 @@ export class DashboardCtrl implements PanelContainer { private unsavedChangesSrv, private dashboardViewStateSrv, public playlistSrv, - private panelLoader, - private panelClipboardSrv + private panelLoader ) { // temp hack due to way dashboards are loaded // can't use controllerAs on route yet @@ -123,10 +122,6 @@ export class DashboardCtrl implements PanelContainer { return this.panelLoader; } - getClipboardPanel() { - return this.panelClipboardSrv.getPanel(); - } - timezoneChanged() { this.$rootScope.$broadcast('refresh'); } diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index 78def4d47bc..1f143f3d7f7 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -5,6 +5,8 @@ import config from 'app/core/config'; import { PanelModel } from '../panel_model'; import { PanelContainer } from './PanelContainer'; import ScrollBar from 'app/core/components/ScrollBar/ScrollBar'; +import store from 'app/core/store'; +import { LS_PANEL_COPY_KEY } from 'app/core/constants'; export interface AddPanelPanelProps { panel: PanelModel; @@ -14,7 +16,6 @@ export interface AddPanelPanelProps { export interface AddPanelPanelState { filter: string; panelPlugins: any[]; - clipboardPanel: any; } export class AddPanelPanel extends React.Component { @@ -23,12 +24,8 @@ export class AddPanelPanel extends React.Component { const panelContainer = this.props.getPanelContainer(); const dashboard = panelContainer.getDashboard(); const { gridPos } = this.props.panel; @@ -64,39 +70,23 @@ export class AddPanelPanel extends React.Component this.onClipboardPanelSelected(panel)} title={title}> -
- -
-
Paste copied panel
-
- ); - } - - renderPanelItem(panel) { - return ( -
this.onPanelSelected(panel)} title={panel.name}> +
this.onAddPanel(panel)} title={panel.name}>
{panel.name}
@@ -113,7 +103,6 @@ export class AddPanelPanel extends React.ComponentSelect a visualization
- {this.state.clipboardPanel && this.renderClipboardPanel(this.state.clipboardPanel)} {this.state.panelPlugins.map(this.renderPanelItem.bind(this))} diff --git a/public/app/features/dashboard/dashgrid/PanelContainer.ts b/public/app/features/dashboard/dashgrid/PanelContainer.ts index f56fab6ef4f..87f3235a176 100644 --- a/public/app/features/dashboard/dashgrid/PanelContainer.ts +++ b/public/app/features/dashboard/dashgrid/PanelContainer.ts @@ -4,5 +4,4 @@ import { PanelLoader } from './PanelLoader'; export interface PanelContainer { getPanelLoader(): PanelLoader; getDashboard(): DashboardModel; - getClipboardPanel(): any; } diff --git a/public/app/features/dashboard/panel_clipboard_srv.ts b/public/app/features/dashboard/panel_clipboard_srv.ts deleted file mode 100644 index 78411562097..00000000000 --- a/public/app/features/dashboard/panel_clipboard_srv.ts +++ /dev/null @@ -1,21 +0,0 @@ -import coreModule from 'app/core/core_module'; -import { appEvents } from 'app/core/core'; - -class PanelClipboardSrv { - key = 'GrafanaDashboardClipboardPanel'; - - /** @ngInject **/ - constructor(private $window) { - appEvents.on('copy-dashboard-panel', this.copyDashboardPanel.bind(this)); - } - - getPanel() { - return this.$window[this.key]; - } - - private copyDashboardPanel(payload) { - this.$window[this.key] = payload; - } -} - -coreModule.service('panelClipboardSrv', PanelClipboardSrv); diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 1cf4681515c..17eae2cbf19 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -4,7 +4,8 @@ import $ from 'jquery'; import { appEvents, profiler } from 'app/core/core'; import { PanelModel } from 'app/features/dashboard/panel_model'; import Remarkable from 'remarkable'; -import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; +import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, LS_PANEL_COPY_KEY } from 'app/core/constants'; +import store from 'app/core/store'; const TITLE_HEIGHT = 27; const PANEL_BORDER = 2; @@ -190,19 +191,19 @@ export class PanelCtrl { click: 'ctrl.duplicate()', role: 'Editor', }); + + menu.push({ + text: 'Add to Panel List', + click: 'ctrl.addToPanelList()', + role: 'Editor', + }); } + menu.push({ text: 'Panel JSON', click: 'ctrl.editPanelJson(); dismiss();', }); - menu.push({ - text: 'Copy to Clipboard', - click: 'ctrl.copyPanelToClipboard()', - role: 'Editor', - directives: ['clipboard-button="ctrl.getPanelJson()"'], - }); - this.events.emit('init-panel-actions', menu); return menu; } @@ -278,15 +279,9 @@ export class PanelCtrl { }); } - copyPanelToClipboard() { - appEvents.emit('copy-dashboard-panel', { - dashboard: this.dashboard.title, - panel: this.panel.getSaveModel(), - }); - } - - getPanelJson() { - return JSON.stringify(this.panel.getSaveModel(), null, 2); + addToPanelList() { + store.set(LS_PANEL_COPY_KEY, JSON.stringify(this.panel.getSaveModel())); + appEvents.emit('alert-success', ['Panel temporarily added to panel list']); } replacePanel(newPanel, oldPanel) { diff --git a/public/app/features/panel/panel_header.ts b/public/app/features/panel/panel_header.ts index de8c93d5038..ca6ed68b648 100644 --- a/public/app/features/panel/panel_header.ts +++ b/public/app/features/panel/panel_header.ts @@ -51,12 +51,6 @@ function renderMenuItem(item, ctrl) { html += ` href="${item.href}"`; } - if (item.directives) { - for (let directive of item.directives) { - html += ` ${directive}`; - } - } - html += `>`; html += `${item.text}`; From 39eb8f9eba7c2ed24ece39b659195fc2418d9f4b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 26 Dec 2017 13:52:19 +0100 Subject: [PATCH 14/14] Dashboard: View JSON improvements (#10327) * dashboard: enable copy to clipboard for view json and panel json * dashboard: use code editor for view json under settings --- public/app/core/controllers/json_editor_ctrl.ts | 3 +++ public/app/core/directives/misc.ts | 5 +++++ public/app/features/dashboard/export/export_modal.ts | 1 + public/app/features/dashboard/settings/settings.html | 2 +- public/app/features/panel/panel_ctrl.ts | 1 + public/app/partials/edit_json.html | 3 +++ 6 files changed, 14 insertions(+), 1 deletion(-) diff --git a/public/app/core/controllers/json_editor_ctrl.ts b/public/app/core/controllers/json_editor_ctrl.ts index 08491acc682..d369fe8b3c0 100644 --- a/public/app/core/controllers/json_editor_ctrl.ts +++ b/public/app/core/controllers/json_editor_ctrl.ts @@ -6,11 +6,14 @@ export class JsonEditorCtrl { constructor($scope) { $scope.json = angular.toJson($scope.object, true); $scope.canUpdate = $scope.updateHandler !== void 0 && $scope.contextSrv.isEditor; + $scope.canCopy = $scope.enableCopy; $scope.update = function() { var newObject = angular.fromJson($scope.json); $scope.updateHandler(newObject, $scope.object); }; + + $scope.getContentForClipboard = () => $scope.json; } } diff --git a/public/app/core/directives/misc.ts b/public/app/core/directives/misc.ts index 0d913c32349..299de05f112 100644 --- a/public/app/core/directives/misc.ts +++ b/public/app/core/directives/misc.ts @@ -2,6 +2,7 @@ import angular from 'angular'; import Clipboard from 'clipboard'; import coreModule from '../core_module'; import kbn from 'app/core/utils/kbn'; +import { appEvents } from 'app/core/core'; /** @ngInject */ function tip($compile) { @@ -32,6 +33,10 @@ function clipboardButton() { }, }); + scope.clipboard.on('success', () => { + appEvents.emit('alert-success', ['Content copied to clipboard']); + }); + scope.$on('$destroy', function() { if (scope.clipboard) { scope.clipboard.destroy(); diff --git a/public/app/features/dashboard/export/export_modal.ts b/public/app/features/dashboard/export/export_modal.ts index 929071c23a2..2e61ce9f8a8 100644 --- a/public/app/features/dashboard/export/export_modal.ts +++ b/public/app/features/dashboard/export/export_modal.ts @@ -31,6 +31,7 @@ export class DashExportCtrl { var clone = this.dash; let editScope = this.$rootScope.$new(); editScope.object = clone; + editScope.enableCopy = true; this.$rootScope.appEvent('show-modal', { src: 'public/app/partials/edit_json.html', diff --git a/public/app/features/dashboard/settings/settings.html b/public/app/features/dashboard/settings/settings.html index 0411850c543..18dd055ef05 100644 --- a/public/app/features/dashboard/settings/settings.html +++ b/public/app/features/dashboard/settings/settings.html @@ -89,7 +89,7 @@

View JSON

- +
diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 17eae2cbf19..d8757f49be6 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -272,6 +272,7 @@ export class PanelCtrl { let editScope = this.$scope.$root.$new(); editScope.object = this.panel.getSaveModel(); editScope.updateHandler = this.replacePanel.bind(this); + editScope.enableCopy = true; this.publishAppEvent('show-modal', { src: 'public/app/partials/edit_json.html', diff --git a/public/app/partials/edit_json.html b/public/app/partials/edit_json.html index b87bb0ce261..f3237193954 100644 --- a/public/app/partials/edit_json.html +++ b/public/app/partials/edit_json.html @@ -16,6 +16,9 @@
+