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);