diff --git a/rd_ui/app/scripts/controllers/query_view.js b/rd_ui/app/scripts/controllers/query_view.js
index df8a8d4d..14c35d00 100644
--- a/rd_ui/app/scripts/controllers/query_view.js
+++ b/rd_ui/app/scripts/controllers/query_view.js
@@ -107,19 +107,6 @@
}
$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';
-
- $scope.dataFilename =
- $scope.query.name.replace(" ", "_") +
- moment($scope.queryResult.getUpdatedAt()).format("_YYYY_MM_DD") +
- ".csv";
- }
});
$scope.$watch("queryResult && queryResult.getStatus()", function(status) {
diff --git a/rd_ui/app/scripts/directives/directives.js b/rd_ui/app/scripts/directives/directives.js
index 342f53cb..9bef5f1a 100644
--- a/rd_ui/app/scripts/directives/directives.js
+++ b/rd_ui/app/scripts/directives/directives.js
@@ -1,219 +1,227 @@
-(function() {
- 'use strict';
+(function () {
+ 'use strict';
- var directives = angular.module('redash.directives', []);
+ var directives = angular.module('redash.directives', []);
- directives.directive('alertUnsavedChanges', ['$window', function($window) {
- return {
- restrict: 'E',
- replace: true,
- scope: {
- 'isDirty': '='
- },
- link: function($scope) {
- var
+ directives.directive('alertUnsavedChanges', ['$window', function ($window) {
+ return {
+ restrict: 'E',
+ replace: true,
+ scope: {
+ 'isDirty': '='
+ },
+ link: function ($scope) {
+ var
- unloadMessage = "You will lose your changes if you leave",
- confirmMessage = unloadMessage + "\n\nAre you sure you want to leave this page?",
+ unloadMessage = "You will lose your changes if you leave",
+ confirmMessage = unloadMessage + "\n\nAre you sure you want to leave this page?",
- // store original handler (if any)
- _onbeforeunload = $window.onbeforeunload;
+ // store original handler (if any)
+ _onbeforeunload = $window.onbeforeunload;
- $window.onbeforeunload = function() {
- return $scope.isDirty ? unloadMessage : null;
- }
-
- $scope.$on('$locationChangeStart', function(event, next, current) {
- if (next.split("#")[0] == current.split("#")[0]) {
- return;
- }
-
- if ($scope.isDirty && !confirm(confirmMessage)) {
- event.preventDefault();
- }
- });
-
- $scope.$on('$destroy', function() {
- $window.onbeforeunload = _onbeforeunload;
- });
- }
+ $window.onbeforeunload = function () {
+ return $scope.isDirty ? unloadMessage : null;
}
- }]);
- directives.directive('rdTab', function() {
- return {
- restrict: 'E',
- scope: {
- 'tabId': '@',
- 'name': '@'
- },
- transclude: true,
- template: '
{{name}}',
- replace: true,
- link: function(scope) {
- scope.$watch(function(){return scope.$parent.selectedTab}, function(tab) {
- scope.selectedTab = tab;
- });
- }
+ $scope.$on('$locationChangeStart', function (event, next, current) {
+ if (next.split("#")[0] == current.split("#")[0]) {
+ return;
+ }
+
+ if ($scope.isDirty && !confirm(confirmMessage)) {
+ event.preventDefault();
+ }
+ });
+
+ $scope.$on('$destroy', function () {
+ $window.onbeforeunload = _onbeforeunload;
+ });
+ }
+ }
+ }]);
+
+ directives.directive('rdTab', function () {
+ return {
+ restrict: 'E',
+ scope: {
+ 'tabId': '@',
+ 'name': '@'
+ },
+ transclude: true,
+ template: '{{name}}',
+ replace: true,
+ link: function (scope) {
+ scope.$watch(function () {
+ return scope.$parent.selectedTab
+ }, function (tab) {
+ scope.selectedTab = tab;
+ });
+ }
+ }
+ });
+
+ directives.directive('rdTabs', ['$location', function ($location) {
+ return {
+ restrict: 'E',
+ scope: {
+ tabsCollection: '=',
+ selectedTab: '='
+ },
+ template: '',
+ replace: true,
+ link: function ($scope, element, attrs) {
+ $scope.selectTab = function (tabKey) {
+ $scope.selectedTab = _.find($scope.tabsCollection, function (tab) {
+ return tab.key == tabKey;
+ });
}
- });
- directives.directive('rdTabs', ['$location', function($location) {
- return {
- restrict: 'E',
- scope: {
- tabsCollection: '=',
- selectedTab: '='
- },
- template: '',
- replace: true,
- link: function($scope, element, attrs) {
- $scope.selectTab = function(tabKey) {
- $scope.selectedTab = _.find($scope.tabsCollection, function(tab) { return tab.key == tabKey; });
- }
+ $scope.$watch(function () {
+ return $location.hash()
+ }, function (hash) {
+ if (hash) {
+ $scope.selectTab($location.hash());
+ } else {
+ $scope.selectTab($scope.tabsCollection[0].key);
+ }
+ });
+ }
+ }
+ }]);
- $scope.$watch(function() { return $location.hash()}, function(hash) {
- if (hash) {
- $scope.selectTab($location.hash());
- } else {
- $scope.selectTab($scope.tabsCollection[0].key);
- }
- });
- }
- }
- }]);
+ // From: http://jsfiddle.net/joshdmiller/NDFHg/
+ directives.directive('editInPlace', function () {
+ return {
+ restrict: 'E',
+ scope: {
+ value: '=',
+ ignoreBlanks: '=',
+ editable: '=',
+ done: '='
+ },
+ template: function (tElement, tAttrs) {
+ var elType = tAttrs.editor || 'input';
+ var placeholder = tAttrs.placeholder || 'Click to edit';
+ return '' +
+ '' + placeholder + '' +
+ '<{elType} ng-model="value" class="rd-form-control">{elType}>'.replace('{elType}', elType);
+ },
+ link: function ($scope, element, attrs) {
+ // Let's get a reference to the input element, as we'll want to reference it.
+ var inputElement = angular.element(element.children()[2]);
- // From: http://jsfiddle.net/joshdmiller/NDFHg/
- directives.directive('editInPlace', function () {
- return {
- restrict: 'E',
- scope: {
- value: '=',
- ignoreBlanks: '=',
- editable: '=',
- done: '='
- },
- template: function(tElement, tAttrs) {
- var elType = tAttrs.editor || 'input';
- var placeholder = tAttrs.placeholder || 'Click to edit';
- return '' +
- '' + placeholder + '' +
- '<{elType} ng-model="value" class="rd-form-control">{elType}>'.replace('{elType}', elType);
- },
- link: function ($scope, element, attrs) {
- // Let's get a reference to the input element, as we'll want to reference it.
- var inputElement = angular.element(element.children()[2]);
+ // This directive should have a set class so we can style it.
+ element.addClass('edit-in-place');
- // This directive should have a set class so we can style it.
- element.addClass('edit-in-place');
+ // Initially, we're not editing.
+ $scope.editing = false;
- // Initially, we're not editing.
- $scope.editing = false;
+ // ng-click handler to activate edit-in-place
+ $scope.edit = function () {
+ $scope.oldValue = $scope.value;
- // ng-click handler to activate edit-in-place
- $scope.edit = function () {
- $scope.oldValue = $scope.value;
+ $scope.editing = true;
- $scope.editing = true;
+ // We control display through a class on the directive itself. See the CSS.
+ element.addClass('active');
- // We control display through a class on the directive itself. See the CSS.
- element.addClass('active');
-
- // And we must focus the element.
- // `angular.element()` provides a chainable array, like jQuery so to access a native DOM function,
- // we have to reference the first element in the array.
- inputElement[0].focus();
- };
-
- function save() {
- if ($scope.editing) {
- if ($scope.ignoreBlanks && _.isEmpty($scope.value)) {
- $scope.value = $scope.oldValue;
- }
- $scope.editing = false;
- element.removeClass('active');
-
- if ($scope.value !== $scope.oldValue) {
- $scope.done && $scope.done();
- }
- }
- }
-
- $(inputElement).keydown(function(e) {
- // 'return' or 'enter' key pressed
- // allow 'shift' to break lines
- if (e.which === 13 && !e.shiftKey) {
- save();
- } else if (e.which === 27) {
- $scope.value = $scope.oldValue;
- $scope.$apply(function() {
- $(inputElement[0]).blur();
- });
- }
- }).blur(function() {
- save();
- });
- }
+ // And we must focus the element.
+ // `angular.element()` provides a chainable array, like jQuery so to access a native DOM function,
+ // we have to reference the first element in the array.
+ inputElement[0].focus();
};
- });
- // http://stackoverflow.com/a/17904092/1559840
- directives.directive('jsonText', function() {
- return {
- restrict: 'A',
- require: 'ngModel',
- link: function(scope, element, attr, ngModel) {
- function into(input) {
- return JSON.parse(input);
- }
- function out(data) {
- return JSON.stringify(data, undefined, 2);
- }
- ngModel.$parsers.push(into);
- ngModel.$formatters.push(out);
-
- scope.$watch(attr.ngModel, function(newValue) {
- element[0].value = out(newValue);
- }, true);
+ function save() {
+ if ($scope.editing) {
+ if ($scope.ignoreBlanks && _.isEmpty($scope.value)) {
+ $scope.value = $scope.oldValue;
}
- };
- });
+ $scope.editing = false;
+ element.removeClass('active');
- directives.directive('rdTimer', [function () {
- return {
- restrict: 'E',
- scope: { timestamp: '=' },
- template: '{{currentTime}}',
- controller: ['$scope' ,function ($scope) {
- $scope.currentTime = "00:00:00";
-
- // We're using setInterval directly instead of $timeout, to avoid using $apply, to
- // prevent the digest loop being run every second.
- var currentTimer = setInterval(function() {
- $scope.currentTime = moment(moment() - moment($scope.timestamp)).utc().format("HH:mm:ss");
- $scope.$digest();
- }, 1000);
-
- $scope.$on('$destroy', function () {
- if (currentTimer) {
- clearInterval(currentTimer);
- currentTimer = null;
- }
- });
- }]
- };
- }]);
-
- directives.directive('rdTimeAgo', function() {
- return {
- restrict: 'E',
- scope: {
- value: '='
- },
- template: '' +
- '' +
- '-' +
- ''
+ if ($scope.value !== $scope.oldValue) {
+ $scope.done && $scope.done();
+ }
+ }
}
- });
+
+ $(inputElement).keydown(function (e) {
+ // 'return' or 'enter' key pressed
+ // allow 'shift' to break lines
+ if (e.which === 13 && !e.shiftKey) {
+ save();
+ } else if (e.which === 27) {
+ $scope.value = $scope.oldValue;
+ $scope.$apply(function () {
+ $(inputElement[0]).blur();
+ });
+ }
+ }).blur(function () {
+ save();
+ });
+ }
+ };
+ });
+
+ // http://stackoverflow.com/a/17904092/1559840
+ directives.directive('jsonText', function () {
+ return {
+ restrict: 'A',
+ require: 'ngModel',
+ link: function (scope, element, attr, ngModel) {
+ function into(input) {
+ return JSON.parse(input);
+ }
+
+ function out(data) {
+ return JSON.stringify(data, undefined, 2);
+ }
+
+ ngModel.$parsers.push(into);
+ ngModel.$formatters.push(out);
+
+ scope.$watch(attr.ngModel, function (newValue) {
+ element[0].value = out(newValue);
+ }, true);
+ }
+ };
+ });
+
+ directives.directive('rdTimer', [function () {
+ return {
+ restrict: 'E',
+ scope: { timestamp: '=' },
+ template: '{{currentTime}}',
+ controller: ['$scope' , function ($scope) {
+ $scope.currentTime = "00:00:00";
+
+ // We're using setInterval directly instead of $timeout, to avoid using $apply, to
+ // prevent the digest loop being run every second.
+ var currentTimer = setInterval(function () {
+ $scope.currentTime = moment(moment() - moment($scope.timestamp)).utc().format("HH:mm:ss");
+ $scope.$digest();
+ }, 1000);
+
+ $scope.$on('$destroy', function () {
+ if (currentTimer) {
+ clearInterval(currentTimer);
+ currentTimer = null;
+ }
+ });
+ }]
+ };
+ }]);
+
+ directives.directive('rdTimeAgo', function () {
+ return {
+ restrict: 'E',
+ scope: {
+ value: '='
+ },
+ template: '' +
+ '' +
+ '-' +
+ ''
+ }
+ });
})();
diff --git a/rd_ui/app/scripts/directives/query_directives.js b/rd_ui/app/scripts/directives/query_directives.js
index b8f29eec..af376ff2 100644
--- a/rd_ui/app/scripts/directives/query_directives.js
+++ b/rd_ui/app/scripts/directives/query_directives.js
@@ -38,6 +38,26 @@
}
}
+ function queryResultCSVLink() {
+ return {
+ restrict: 'A',
+ link: function (scope, element) {
+ scope.$watch('queryResult && queryResult.getData()', function(data) {
+ if (!data) {
+ return;
+ }
+
+ if (scope.queryResult.getId() == null) {
+ element.attr('href', '');
+ } else {
+ element.attr('href', '/api/queries/' + scope.query.id + '/results/' + scope.queryResult.getId() + '.csv');
+ element.attr('download', scope.query.name.replace(" ", "_") + moment(scope.queryResult.getUpdatedAt()).format("_YYYY_MM_DD") + ".csv");
+ }
+ });
+ }
+ }
+ }
+
function queryEditor() {
return {
restrict: 'E',
@@ -135,6 +155,7 @@
angular.module('redash.directives')
.directive('queryLink', queryLink)
.directive('querySourceLink', querySourceLink)
+ .directive('queryResultLink', queryResultCSVLink)
.directive('queryEditor', queryEditor)
.directive('queryRefreshSelect', queryRefreshSelect)
.directive('queryFormatter', ['$http', queryFormatter]);
diff --git a/rd_ui/app/views/dashboard.html b/rd_ui/app/views/dashboard.html
index a2c0f7c0..4bd67b90 100644
--- a/rd_ui/app/views/dashboard.html
+++ b/rd_ui/app/views/dashboard.html
@@ -44,6 +44,12 @@
+
+
+
+
+
+
diff --git a/rd_ui/app/views/query.html b/rd_ui/app/views/query.html
index 5142b708..63b03f11 100644
--- a/rd_ui/app/views/query.html
+++ b/rd_ui/app/views/query.html
@@ -122,7 +122,7 @@
-
+
Download Dataset