From c2e4e19004aa856cf7287690fa2e151a8f41f89a Mon Sep 17 00:00:00 2001 From: Arik Fraimovich Date: Thu, 10 Apr 2014 12:29:07 +0300 Subject: [PATCH] Events reporting from client side. --- .../scripts/controllers/admin_controllers.js | 35 +- rd_ui/app/scripts/controllers/controllers.js | 6 +- rd_ui/app/scripts/controllers/dashboard.js | 16 +- rd_ui/app/scripts/controllers/query_source.js | 9 +- rd_ui/app/scripts/controllers/query_view.js | 85 ++--- .../directives/dashboard_directives.js | 12 +- rd_ui/app/scripts/services/notifications.js | 5 +- rd_ui/app/scripts/services/services.js | 30 +- rd_ui/app/scripts/visualizations/base.js | 324 +++++++++--------- 9 files changed, 285 insertions(+), 237 deletions(-) diff --git a/rd_ui/app/scripts/controllers/admin_controllers.js b/rd_ui/app/scripts/controllers/admin_controllers.js index f05c67caa..170c5ed38 100644 --- a/rd_ui/app/scripts/controllers/admin_controllers.js +++ b/rd_ui/app/scripts/controllers/admin_controllers.js @@ -1,23 +1,24 @@ (function () { - var AdminStatusCtrl = function ($scope, $http, $timeout) { - $scope.$parent.pageTitle = "System Status"; + var AdminStatusCtrl = function ($scope, Events, $http, $timeout) { + Events.record(currentUser, "view", "page", "admin/status"); + $scope.$parent.pageTitle = "System Status"; - var refresh = function () { - $scope.refresh_time = moment().add('minutes', 1); - $http.get('/status.json').success(function (data) { - $scope.workers = data.workers; - delete data.workers; - $scope.manager = data.manager; - delete data.manager; - $scope.status = data; - }); + var refresh = function () { + $scope.refresh_time = moment().add('minutes', 1); + $http.get('/status.json').success(function (data) { + $scope.workers = data.workers; + delete data.workers; + $scope.manager = data.manager; + delete data.manager; + $scope.status = data; + }); - $timeout(refresh, 59 * 1000); - }; + $timeout(refresh, 59 * 1000); + }; - refresh(); - } + refresh(); + } - angular.module('redash.admin_controllers', []) - .controller('AdminStatusCtrl', ['$scope', '$http', '$timeout', AdminStatusCtrl]) + angular.module('redash.admin_controllers', []) + .controller('AdminStatusCtrl', ['$scope', 'Events', '$http', '$timeout', AdminStatusCtrl]) })(); diff --git a/rd_ui/app/scripts/controllers/controllers.js b/rd_ui/app/scripts/controllers/controllers.js index d68dfe92c..dcb4672b7 100644 --- a/rd_ui/app/scripts/controllers/controllers.js +++ b/rd_ui/app/scripts/controllers/controllers.js @@ -136,11 +136,13 @@ }); } - var IndexCtrl = function($scope, Dashboard) { + var IndexCtrl = function($scope, Events, Dashboard) { + Events.record(currentUser, "view", "page", "homepage"); $scope.$parent.pageTitle = "Home"; $scope.archiveDashboard = function(dashboard) { if (confirm('Are you sure you want to delete "' + dashboard.name + '" dashboard?')) { + Events.record(currentUser, "archive", "dashboard", dashboard.id); dashboard.$delete(function() { $scope.$parent.reloadDashboards(); }); @@ -150,6 +152,6 @@ angular.module('redash.controllers', []) .controller('QueriesCtrl', ['$scope', '$http', '$location', '$filter', 'Query', QueriesCtrl]) - .controller('IndexCtrl', ['$scope', 'Dashboard', IndexCtrl]) + .controller('IndexCtrl', ['$scope', 'Events', 'Dashboard', IndexCtrl]) .controller('MainCtrl', ['$scope', 'Dashboard', 'notifications', MainCtrl]); })(); diff --git a/rd_ui/app/scripts/controllers/dashboard.js b/rd_ui/app/scripts/controllers/dashboard.js index e427ed954..34b46a3c7 100644 --- a/rd_ui/app/scripts/controllers/dashboard.js +++ b/rd_ui/app/scripts/controllers/dashboard.js @@ -1,5 +1,7 @@ (function() { - var DashboardCtrl = function($scope, $routeParams, $http, $timeout, Dashboard) { + var DashboardCtrl = function($scope, Events, $routeParams, $http, $timeout, Dashboard) { + Events.record(currentUser, "view", "dashboard", dashboard.id); + $scope.refreshEnabled = false; $scope.refreshRate = 60; $scope.dashboard = Dashboard.get({ @@ -35,6 +37,8 @@ $scope.triggerRefresh = function() { $scope.refreshEnabled = !$scope.refreshEnabled; + Events.record(currentUser, "autorefresh", "dashboard", dashboard.id, {'enable': $scope.refreshEnabled}); + if ($scope.refreshEnabled) { var refreshRate = _.min(_.flatten($scope.dashboard.widgets), function(widget) { return widget.visualization.query.ttl; @@ -47,12 +51,14 @@ }; }; - var WidgetCtrl = function($scope, $http, $location, Query) { + var WidgetCtrl = function($scope, Events, $http, $location, Query) { $scope.deleteWidget = function() { if (!confirm('Are you sure you want to remove "' + $scope.widget.visualization.name + '" from the dashboard?')) { return; } + Events.record(currentUser, "delete", "widget", $scope.widget.id); + $http.delete('/api/widgets/' + $scope.widget.id).success(function() { $scope.dashboard.widgets = _.map($scope.dashboard.widgets, function(row) { return _.filter(row, function(widget) { @@ -62,6 +68,8 @@ }); }; + // TODO: fire event for query view for each query + $scope.query = new Query($scope.widget.visualization.query); $scope.queryResult = $scope.query.getQueryResult(); @@ -72,7 +80,7 @@ }; angular.module('redash.controllers') - .controller('DashboardCtrl', ['$scope', '$routeParams', '$http', '$timeout', 'Dashboard', DashboardCtrl]) - .controller('WidgetCtrl', ['$scope', '$http', '$location', 'Query', WidgetCtrl]) + .controller('DashboardCtrl', ['$scope', 'Events', '$routeParams', '$http', '$timeout', 'Dashboard', DashboardCtrl]) + .controller('WidgetCtrl', ['$scope', 'Events', '$http', '$location', 'Query', WidgetCtrl]) })(); \ No newline at end of file diff --git a/rd_ui/app/scripts/controllers/query_source.js b/rd_ui/app/scripts/controllers/query_source.js index f973ee84b..075b9dee9 100644 --- a/rd_ui/app/scripts/controllers/query_source.js +++ b/rd_ui/app/scripts/controllers/query_source.js @@ -1,7 +1,7 @@ (function() { 'use strict'; - function QuerySourceCtrl($controller, $scope, $location, Query, Visualization, KeyboardShortcuts) { + function QuerySourceCtrl(Events, $controller, $scope, $location, Query, Visualization, KeyboardShortcuts) { // extends QueryViewCtrl $controller('QueryViewCtrl', {$scope: $scope}); // TODO: @@ -9,6 +9,8 @@ // Obviously it shouldn't be repeated, but we got bigger fish to fry. var DEFAULT_TAB = 'table'; + Events.record(currentUser, 'view_source', 'query', $scope.query.id); + var isNewQuery = !$scope.query.id, queryText = $scope.query.query, // ref to QueryViewCtrl.saveQuery @@ -47,6 +49,7 @@ }; $scope.duplicateQuery = function() { + Events.record(currentUser, 'fork', 'query', $scope.query.id); $scope.query.id = null; $scope.query.ttl = -1; @@ -62,6 +65,8 @@ $scope.deleteVisualization = function($e, vis) { $e.preventDefault(); if (confirm('Are you sure you want to delete ' + vis.name + ' ?')) { + Events.record(currentUser, 'delete', 'visualization', vis.id); + Visualization.delete(vis); if ($scope.selectedTab == vis.id) { $scope.selectedTab = DEFAULT_TAB; @@ -94,7 +99,7 @@ } angular.module('redash.controllers').controller('QuerySourceCtrl', [ - '$controller', '$scope', '$location', 'Query', + 'Events', '$controller', '$scope', '$location', 'Query', 'Visualization', 'KeyboardShortcuts', QuerySourceCtrl ]); })(); \ No newline at end of file diff --git a/rd_ui/app/scripts/controllers/query_view.js b/rd_ui/app/scripts/controllers/query_view.js index 464e42fb6..46e0fd902 100644 --- a/rd_ui/app/scripts/controllers/query_view.js +++ b/rd_ui/app/scripts/controllers/query_view.js @@ -1,9 +1,11 @@ (function() { 'use strict'; - function QueryViewCtrl($scope, $route, $location, notifications, growl, Query, DataSource) { + function QueryViewCtrl($scope, Events, $route, $location, notifications, growl, Query, DataSource) { var DEFAULT_TAB = 'table'; + Events.record(currentUser, 'view', 'query', $scope.query.id); + $scope.query = $route.current.locals.query; $scope.queryResult = $scope.query.getQueryResult(); $scope.queryExecuting = false; @@ -37,15 +39,16 @@ growl.addSuccessMessage(options.successMessage); }, function(httpResponse) { growl.addErrorMessage(options.errorMessage); - }) - .$promise; + }).$promise; } $scope.saveDescription = function() { + Events.record(currentUser, 'edit_description', 'query', $scope.query.id); $scope.saveQuery(undefined, {'description': $scope.query.description}); }; $scope.saveName = function() { + Events.record(currentUser, 'edit_name', 'query', $scope.query.id); $scope.saveQuery(undefined, {'name': $scope.query.name}); }; @@ -53,23 +56,26 @@ $scope.queryResult = $scope.query.getQueryResult(0); $scope.lockButton(true); $scope.cancelling = false; + Events.record(currentUser, 'execute', 'query', $scope.query.id); }; $scope.cancelExecution = function() { $scope.cancelling = true; $scope.queryResult.cancelExecution(); + Events.record(currentUser, 'cancel_execute', 'query', $scope.query.id); }; $scope.updateDataSource = function() { - $scope.query.latest_query_data = null; - $scope.query.latest_query_data_id = null; - Query.save({ - 'id': $scope.query.id, - 'data_source_id': $scope.query.data_source_id, - 'latest_query_data_id': null - }); + Events.record(currentUser, 'update_data_source', 'query', $scope.query.id); + $scope.query.latest_query_data = null; + $scope.query.latest_query_data_id = null; + Query.save({ + 'id': $scope.query.id, + 'data_source_id': $scope.query.data_source_id, + 'latest_query_data_id': null + }); - $scope.executeQuery(); + $scope.executeQuery(); }; $scope.setVisualizationTab = function (visualization) { @@ -81,38 +87,36 @@ $scope.$parent.pageTitle = $scope.query.name; }); - $scope.$watch('queryResult && queryResult.getError()', - function(newError, oldError) { - if (newError == undefined) { - return; - } + $scope.$watch('queryResult && queryResult.getError()', function(newError, oldError) { + if (newError == undefined) { + return; + } - if (oldError == undefined && newError != undefined) { - $scope.lockButton(false); - } - }); + if (oldError == undefined && newError != undefined) { + $scope.lockButton(false); + } + }); - $scope.$watch('queryResult && queryResult.getData()', - function(data, oldData) { - if (!data) { - return; - } + $scope.$watch('queryResult && queryResult.getData()', function(data, oldData) { + if (!data) { + return; + } - $scope.filters = $scope.queryResult.getFilters(); + $scope.filters = $scope.queryResult.getFilters(); - if ($scope.queryResult.getId() == null) { - $scope.dataUri = ""; - } else { - $scope.dataUri = - '/api/queries/' + $scope.query.id + '/results/' + - $scope.queryResult.getId() + '.csv'; + if ($scope.queryResult.getId() == null) { + $scope.dataUri = ""; + } else { + $scope.dataUri = + '/api/queries/' + $scope.query.id + '/results/' + + $scope.queryResult.getId() + '.csv'; - $scope.dataFilename = - $scope.query.name.replace(" ", "_") + - moment($scope.queryResult.getUpdatedAt()).format("_YYYY_MM_DD") + - ".csv"; - } - }); + $scope.dataFilename = + $scope.query.name.replace(" ", "_") + + moment($scope.queryResult.getUpdatedAt()).format("_YYYY_MM_DD") + + ".csv"; + } + }); $scope.$watch("queryResult && queryResult.getStatus()", function(status) { if (!status) { @@ -139,11 +143,14 @@ $scope.$watch(function() { return $location.hash() }, function(hash) { + if (hash == 'pivot') { + Events.record(currentUser, 'pivot', 'query', $scope.query && $scope.query.id); + } $scope.selectedTab = hash || DEFAULT_TAB; }); }; angular.module('redash.controllers') .controller('QueryViewCtrl', - ['$scope', '$route', '$location', 'notifications', 'growl', 'Query', 'DataSource', QueryViewCtrl]); + ['$scope', 'Events', '$route', '$location', 'notifications', 'growl', 'Query', 'DataSource', QueryViewCtrl]); })(); \ No newline at end of file diff --git a/rd_ui/app/scripts/directives/dashboard_directives.js b/rd_ui/app/scripts/directives/dashboard_directives.js index d39ae2c37..c1bc2dde3 100644 --- a/rd_ui/app/scripts/directives/dashboard_directives.js +++ b/rd_ui/app/scripts/directives/dashboard_directives.js @@ -3,8 +3,8 @@ var directives = angular.module('redash.directives'); - directives.directive('editDashboardForm', ['$http', '$location', '$timeout', 'Dashboard', - function($http, $location, $timeout, Dashboard) { + directives.directive('editDashboardForm', ['Events', '$http', '$location', '$timeout', 'Dashboard', + function(Events, $http, $location, $timeout, Dashboard) { return { restrict: 'E', scope: { @@ -54,7 +54,6 @@ _.each(layout, function(item) { var el = gsItemTemplate.replace('{id}', item.id).replace('{name}', item.name); gridster.add_widget(el, item.xSize, item.ySize, item.col, item.row); - }); } }); @@ -89,14 +88,17 @@ $scope.dashboard = new Dashboard(response); $scope.saveInProgress = false; $(element).modal('hide'); - }) + }); + Events.record(currentUser, 'edit', 'dashboard', $scope.dashboard.id); } else { + $http.post('/api/dashboards', { 'name': $scope.dashboard.name }).success(function(response) { $(element).modal('hide'); $location.path('/dashboard/' + response.slug).replace(); - }) + }); + Events.record(currentUser, 'create', 'dashboard'); } } diff --git a/rd_ui/app/scripts/services/notifications.js b/rd_ui/app/scripts/services/notifications.js index 455569955..dfb5ae422 100644 --- a/rd_ui/app/scripts/services/notifications.js +++ b/rd_ui/app/scripts/services/notifications.js @@ -1,5 +1,5 @@ (function () { - var notifications = function () { + var notifications = function (Events) { var notificationService = {}; var lastNotification = null; @@ -40,6 +40,7 @@ notification.onclick = function () { window.focus(); this.cancel(); + Events.record(currentUser, 'click', 'notification'); }; notification.show() @@ -49,5 +50,5 @@ } angular.module('redash.services') - .factory('notifications', notifications); + .factory('notifications', ['Events', notifications]); })(); diff --git a/rd_ui/app/scripts/services/services.js b/rd_ui/app/scripts/services/services.js index feb2189df..a8dd79bb1 100644 --- a/rd_ui/app/scripts/services/services.js +++ b/rd_ui/app/scripts/services/services.js @@ -1,24 +1,40 @@ -(function() { +(function () { 'use strict' function KeyboardShortcuts() { this.bind = function bind(keymap) { - _.forEach(keymap, function(fn, key) { - Mousetrap.bindGlobal(key, function(e) { - e.preventDefault(); - fn(); + _.forEach(keymap, function (fn, key) { + Mousetrap.bindGlobal(key, function (e) { + e.preventDefault(); + fn(); }); }); } this.unbind = function unbind(keymap) { - _.forEach(keymap, function(fn, key) { + _.forEach(keymap, function (fn, key) { Mousetrap.unbind(key); }); } } + function Events($http) { + this.record = function (user, action, object_type, object_id, additional_properties) { + + var event = { + "user_id": user.id, + "action": action, + "object_type": object_type, + "object_id": object_id + }; + _.extend(event, additional_properties); + + $http.post('/api/events', [event]); + }; + } + angular.module('redash.services', []) - .service('KeyboardShortcuts', [KeyboardShortcuts]) + .service('KeyboardShortcuts', [KeyboardShortcuts]) + .service('Events', ['$http', Events]) })(); \ No newline at end of file diff --git a/rd_ui/app/scripts/visualizations/base.js b/rd_ui/app/scripts/visualizations/base.js index 2bc770b2f..f733b1f6e 100644 --- a/rd_ui/app/scripts/visualizations/base.js +++ b/rd_ui/app/scripts/visualizations/base.js @@ -1,170 +1,176 @@ (function () { - var VisualizationProvider = function() { - this.visualizations = {}; - this.visualizationTypes = {}; - var defaultConfig = { - defaultOptions: {}, - skipTypes: false, - editorTemplate: null - } - - this.registerVisualization = function(config) { - var visualization = _.extend({}, defaultConfig, config); - - // TODO: this is prone to errors; better refactor. - if (_.isEmpty(this.visualizations)) { - this.defaultVisualization = visualization; - } - - this.visualizations[config.type] = visualization; - - if (!config.skipTypes) { - this.visualizationTypes[config.name] = config.type; - }; - }; - - this.getSwitchTemplate = function(property) { - var pattern = /(<[a-zA-Z0-9-]*?)( |>)/ - - var mergedTemplates = _.reduce(this.visualizations, function(templates, visualization) { - if (visualization[property]) { - var ngSwitch = '$1 ng-switch-when="' + visualization.type + '" $2'; - var template = visualization[property].replace(pattern, ngSwitch); - - return templates + "\n" + template; - } - - return templates; - }, ""); - - mergedTemplates = '
'+ mergedTemplates + "
"; - - return mergedTemplates; - } - - this.$get = ['$resource', function($resource) { - var Visualization = $resource('/api/visualizations/:id', {id: '@id'}); - Visualization.visualizations = this.visualizations; - Visualization.visualizationTypes = this.visualizationTypes; - Visualization.renderVisualizationsTemplate = this.getSwitchTemplate('renderTemplate'); - Visualization.editorTemplate = this.getSwitchTemplate('editorTemplate'); - Visualization.defaultVisualization = this.defaultVisualization; - - return Visualization; - }]; - }; - - var VisualizationRenderer = function(Visualization) { - return { - restrict: 'E', - scope: { - visualization: '=', - queryResult: '=' - }, - // TODO: using switch here (and in the options editor) might introduce errors and bad - // performance wise. It's better to eventually show the correct template based on the - // visualization type and not make the browser render all of them. - template: '\n' + Visualization.renderVisualizationsTemplate, - replace: false, - link: function(scope) { - scope.$watch('queryResult && queryResult.getFilters()', function(filters) { - if (filters) { - scope.filters = filters; - } - }); - } - } - }; - - var VisualizationOptionsEditor = function(Visualization) { - return { - restrict: 'E', - template: Visualization.editorTemplate, - replace: false - } - }; - - var Filters = function() { - return { - restrict: 'E', - templateUrl: '/views/visualizations/filters.html' - } + var VisualizationProvider = function () { + this.visualizations = {}; + this.visualizationTypes = {}; + var defaultConfig = { + defaultOptions: {}, + skipTypes: false, + editorTemplate: null } - var EditVisualizationForm = function(Visualization, growl) { - return { - restrict: 'E', - templateUrl: '/views/visualizations/edit_visualization.html', - replace: true, - scope: { - query: '=', - queryResult: '=', - visualization: '=?', - onNewSuccess: '=?' - }, - link: function (scope, element, attrs) { - scope.editRawOptions = currentUser.hasPermission('edit_raw_chart'); - scope.visTypes = Visualization.visualizationTypes; + this.registerVisualization = function (config) { + var visualization = _.extend({}, defaultConfig, config); - scope.newVisualization = function(q) { - return { - 'query_id': q.id, - 'type': Visualization.defaultVisualization.type, - 'name': Visualization.defaultVisualization.name, - 'description': q.description || '', - 'options': Visualization.defaultVisualization.defaultOptions - }; - } + // TODO: this is prone to errors; better refactor. + if (_.isEmpty(this.visualizations)) { + this.defaultVisualization = visualization; + } - if (!scope.visualization) { - // create new visualization - // wait for query to load to populate with defaults - var unwatch = scope.$watch('query', function (q) { - if (q && q.id) { - unwatch(); + this.visualizations[config.type] = visualization; - scope.visualization = scope.newVisualization(q); - } - }, true); - } - - scope.$watch('visualization.type', function (type, oldType) { - // if not edited by user, set name to match type - if (type && oldType != type && scope.visualization && !scope.visForm.name.$dirty) { - // poor man's titlecase - scope.visualization.name = scope.visualization.type[0] + scope.visualization.type.slice(1).toLowerCase(); - } - }); - - scope.submit = function () { - Visualization.save(scope.visualization, function success(result) { - growl.addSuccessMessage("Visualization saved"); - - scope.visualization = scope.newVisualization(scope.query); - - var visIds = _.pluck(scope.query.visualizations, 'id'); - var index = visIds.indexOf(result.id); - if (index > -1) { - scope.query.visualizations[index] = result; - } else { - // new visualization - scope.query.visualizations.push(result); - scope.onNewSuccess && scope.onNewSuccess(result); - } - }, function error() { - growl.addErrorMessage("Visualization could not be saved"); - }); - }; - } - } + if (!config.skipTypes) { + this.visualizationTypes[config.name] = config.type; + } + ; }; + this.getSwitchTemplate = function (property) { + var pattern = /(<[a-zA-Z0-9-]*?)( |>)/ + + var mergedTemplates = _.reduce(this.visualizations, function (templates, visualization) { + if (visualization[property]) { + var ngSwitch = '$1 ng-switch-when="' + visualization.type + '" $2'; + var template = visualization[property].replace(pattern, ngSwitch); + + return templates + "\n" + template; + } + + return templates; + }, ""); + + mergedTemplates = '
' + mergedTemplates + "
"; + + return mergedTemplates; + } + + this.$get = ['$resource', function ($resource) { + var Visualization = $resource('/api/visualizations/:id', {id: '@id'}); + Visualization.visualizations = this.visualizations; + Visualization.visualizationTypes = this.visualizationTypes; + Visualization.renderVisualizationsTemplate = this.getSwitchTemplate('renderTemplate'); + Visualization.editorTemplate = this.getSwitchTemplate('editorTemplate'); + Visualization.defaultVisualization = this.defaultVisualization; + + return Visualization; + }]; + }; + + var VisualizationRenderer = function (Visualization) { + return { + restrict: 'E', + scope: { + visualization: '=', + queryResult: '=' + }, + // TODO: using switch here (and in the options editor) might introduce errors and bad + // performance wise. It's better to eventually show the correct template based on the + // visualization type and not make the browser render all of them. + template: '\n' + Visualization.renderVisualizationsTemplate, + replace: false, + link: function (scope) { + scope.$watch('queryResult && queryResult.getFilters()', function (filters) { + if (filters) { + scope.filters = filters; + } + }); + } + } + }; + + var VisualizationOptionsEditor = function (Visualization) { + return { + restrict: 'E', + template: Visualization.editorTemplate, + replace: false + } + }; + + var Filters = function () { + return { + restrict: 'E', + templateUrl: '/views/visualizations/filters.html' + } + } + + var EditVisualizationForm = function (Events, Visualization, growl) { + return { + restrict: 'E', + templateUrl: '/views/visualizations/edit_visualization.html', + replace: true, + scope: { + query: '=', + queryResult: '=', + visualization: '=?', + onNewSuccess: '=?' + }, + link: function (scope, element, attrs) { + scope.editRawOptions = currentUser.hasPermission('edit_raw_chart'); + scope.visTypes = Visualization.visualizationTypes; + + scope.newVisualization = function (q) { + return { + 'query_id': q.id, + 'type': Visualization.defaultVisualization.type, + 'name': Visualization.defaultVisualization.name, + 'description': q.description || '', + 'options': Visualization.defaultVisualization.defaultOptions + }; + } + + if (!scope.visualization) { + // create new visualization + // wait for query to load to populate with defaults + var unwatch = scope.$watch('query', function (q) { + if (q && q.id) { + unwatch(); + + scope.visualization = scope.newVisualization(q); + } + }, true); + } + + scope.$watch('visualization.type', function (type, oldType) { + // if not edited by user, set name to match type + if (type && oldType != type && scope.visualization && !scope.visForm.name.$dirty) { + // poor man's titlecase + scope.visualization.name = scope.visualization.type[0] + scope.visualization.type.slice(1).toLowerCase(); + } + }); + + scope.submit = function () { + if (scope.visualization.id) { + Events.record(currentUser, "update", "visualization", scope.visualization.id, {'type': scope.visualization.type}); + } else { + Events.record(currentUser, "create", "visualization", null, {'type': scope.visualization.type}); + } + + Visualization.save(scope.visualization, function success(result) { + growl.addSuccessMessage("Visualization saved"); + + scope.visualization = scope.newVisualization(scope.query); + + var visIds = _.pluck(scope.query.visualizations, 'id'); + var index = visIds.indexOf(result.id); + if (index > -1) { + scope.query.visualizations[index] = result; + } else { + // new visualization + scope.query.visualizations.push(result); + scope.onNewSuccess && scope.onNewSuccess(result); + } + }, function error() { + growl.addErrorMessage("Visualization could not be saved"); + }); + }; + } + } + }; - angular.module('redash.visualization', []) - .provider('Visualization', VisualizationProvider) - .directive('visualizationRenderer', ['Visualization', VisualizationRenderer]) - .directive('visualizationOptionsEditor', ['Visualization', VisualizationOptionsEditor]) - .directive('filters', Filters) - .directive('editVisulatizationForm', ['Visualization', 'growl', EditVisualizationForm]) + angular.module('redash.visualization', []) + .provider('Visualization', VisualizationProvider) + .directive('visualizationRenderer', ['Visualization', VisualizationRenderer]) + .directive('visualizationOptionsEditor', ['Visualization', VisualizationOptionsEditor]) + .directive('filters', Filters) + .directive('editVisulatizationForm', ['Events', 'Visualization', 'growl', EditVisualizationForm]) })();