Events reporting from client side.

This commit is contained in:
Arik Fraimovich
2014-04-10 12:29:07 +03:00
parent fcda122107
commit c2e4e19004
9 changed files with 285 additions and 237 deletions

View File

@@ -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])
})();

View File

@@ -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]);
})();

View File

@@ -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])
})();

View File

@@ -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
]);
})();

View File

@@ -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]);
})();

View File

@@ -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');
}
}

View File

@@ -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]);
})();

View File

@@ -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])
})();

View File

@@ -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 = '<div ng-switch on="visualization.type">'+ mergedTemplates + "</div>";
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: '<filters></filters>\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 = '<div ng-switch on="visualization.type">' + mergedTemplates + "</div>";
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: '<filters></filters>\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])
})();