diff --git a/migrations/0024_add_options_to_query.py b/migrations/0024_add_options_to_query.py
new file mode 100644
index 0000000000..05e37f5c50
--- /dev/null
+++ b/migrations/0024_add_options_to_query.py
@@ -0,0 +1,10 @@
+from redash.models import db, Query
+from playhouse.migrate import PostgresqlMigrator, migrate
+
+if __name__ == '__main__':
+ migrator = PostgresqlMigrator(db.database)
+
+ with db.database.transaction():
+ migrate(
+ migrator.add_column('queries', 'options', Query.options),
+ )
diff --git a/rd_ui/app/scripts/controllers/dashboard.js b/rd_ui/app/scripts/controllers/dashboard.js
index 50a37fc9a9..7ebdf805e9 100644
--- a/rd_ui/app/scripts/controllers/dashboard.js
+++ b/rd_ui/app/scripts/controllers/dashboard.js
@@ -211,14 +211,20 @@
Events.record(currentUser, "view", "widget", $scope.widget.id);
+ $scope.reload = function(force) {
+ var maxAge = $location.search()['maxAge'];
+ if (force) {
+ maxAge = 0;
+ }
+ $scope.queryResult = $scope.query.getQueryResult(maxAge);
+ };
+
if ($scope.widget.visualization) {
Events.record(currentUser, "view", "query", $scope.widget.visualization.query.id);
Events.record(currentUser, "view", "visualization", $scope.widget.visualization.id);
$scope.query = $scope.widget.getQuery();
- var parameters = Query.collectParamsFromQueryString($location, $scope.query);
- var maxAge = $location.search()['maxAge'];
- $scope.queryResult = $scope.query.getQueryResult(maxAge, parameters);
+ $scope.reload(false);
$scope.type = 'visualization';
} else if ($scope.widget.restricted) {
diff --git a/rd_ui/app/scripts/controllers/query_view.js b/rd_ui/app/scripts/controllers/query_view.js
index f6e36cc713..4b7cbbccdb 100644
--- a/rd_ui/app/scripts/controllers/query_view.js
+++ b/rd_ui/app/scripts/controllers/query_view.js
@@ -5,8 +5,6 @@
var DEFAULT_TAB = 'table';
var getQueryResult = function(maxAge) {
- // Collect params, and getQueryResult with params; getQueryResult merges it into the query
- var parameters = Query.collectParamsFromQueryString($location, $scope.query);
if (maxAge === undefined) {
maxAge = $location.search()['maxAge'];
}
@@ -16,7 +14,7 @@
}
$scope.showLog = false;
- $scope.queryResult = $scope.query.getQueryResult(maxAge, parameters);
+ $scope.queryResult = $scope.query.getQueryResult(maxAge);
};
var getDataSourceId = function() {
@@ -127,7 +125,10 @@
if (data) {
data.id = $scope.query.id;
} else {
- data = _.clone($scope.query);
+ data = _.pick($scope.query, ["schedule", "query", "id", "description", "name", "data_source_id", "options"]);
+ if ($scope.query.isNew()) {
+ data['latest_query_data_id'] = $scope.query.latest_query_data_id;
+ }
}
options = _.extend({}, {
@@ -135,9 +136,6 @@
errorMessage: 'Query could not be saved'
}, options);
- delete data.latest_query_data;
- delete data.queryResult;
-
return Query.save(data, function() {
growl.addSuccessMessage(options.successMessage);
}, function(httpResponse) {
diff --git a/rd_ui/app/scripts/directives/directives.js b/rd_ui/app/scripts/directives/directives.js
index fef6a423f5..63326724f3 100644
--- a/rd_ui/app/scripts/directives/directives.js
+++ b/rd_ui/app/scripts/directives/directives.js
@@ -92,13 +92,14 @@
restrict: 'E',
scope: {
'tabId': '@',
- 'name': '@'
+ 'name': '@',
+ 'basePath': '=?'
},
transclude: true,
template: '
{{name}}',
replace: true,
link: function (scope) {
- scope.basePath = $location.path().substring(1);
+ scope.basePath = scope.basePath || $location.path().substring(1);
scope.$watch(function () {
return scope.$parent.selectedTab
}, function (tab) {
@@ -496,4 +497,41 @@
}
}]);
+ directives.directive('parameters', ['$location', '$modal', function($location, $modal) {
+ return {
+ restrict: 'E',
+ transclude: true,
+ scope: {
+ 'parameters': '=',
+ 'syncValues': '=?',
+ 'editable': '=?'
+ },
+ templateUrl: '/views/directives/parameters.html',
+ link: function(scope, elem, attrs) {
+ // is this the correct location for this logic?
+ if (scope.syncValues !== false) {
+ scope.$watch('parameters', function() {
+ _.each(scope.parameters, function(param) {
+ if (param.value !== null || param.value !== '') {
+ $location.search('p_' + param.name, param.value);
+ }
+ })
+ }, true);
+ }
+
+ scope.showParameterSettings = function(param) {
+ $modal.open({
+ templateUrl: '/views/dialogs/parameter_settings.html',
+ controller: ['$scope', '$modalInstance', function($scope, $modalInstance) {
+ $scope.close = function() {
+ $modalInstance.close();
+ };
+ $scope.parameter = param;
+ }]
+ })
+ }
+ }
+ }
+ }]);
+
})();
diff --git a/rd_ui/app/scripts/directives/query_directives.js b/rd_ui/app/scripts/directives/query_directives.js
index 7a73b9da17..2dfa39a929 100644
--- a/rd_ui/app/scripts/directives/query_directives.js
+++ b/rd_ui/app/scripts/directives/query_directives.js
@@ -10,29 +10,29 @@
},
template: '{{query.name}}',
link: function(scope, element) {
- scope.link = 'queries/' + scope.query.id;
+ var hash = null;
if (scope.visualization) {
if (scope.visualization.type === 'TABLE') {
// link to hard-coded table tab instead of the (hidden) visualization tab
- scope.link += '#table';
+ hash = 'table';
} else {
- scope.link += '#' + scope.visualization.id;
+ hash = scope.visualization.id;
}
}
- // element.find('a').attr('href', link);
+ scope.link = scope.query.getUrl(false, hash);
}
}
}
- function querySourceLink() {
+ function querySourceLink($location) {
return {
restrict: 'E',
template: '\
Show Source\
+ ng-href="{{query.getUrl(true, selectedTab)}}" class="btn btn-default">Show Source\
\
Hide Source\
+ ng-href="{{query.getUrl(false, selectedTab)}}" class="btn btn-default">Hide Source\
\
'
}
@@ -285,7 +285,7 @@
angular.module('redash.directives')
.directive('queryLink', queryLink)
- .directive('querySourceLink', querySourceLink)
+ .directive('querySourceLink', ['$location', querySourceLink])
.directive('queryResultLink', queryResultLink)
.directive('queryEditor', queryEditor)
.directive('queryRefreshSelect', queryRefreshSelect)
diff --git a/rd_ui/app/scripts/filters.js b/rd_ui/app/scripts/filters.js
index 461f1aaa0a..6a42fa594f 100644
--- a/rd_ui/app/scripts/filters.js
+++ b/rd_ui/app/scripts/filters.js
@@ -120,4 +120,10 @@ angular.module('redash.filters', []).
filtered.push(items[i])
return filtered;
};
+ })
+
+ .filter('notEmpty', function() {
+ return function(collection) {
+ return !_.isEmpty(collection);
+ }
});
diff --git a/rd_ui/app/scripts/services/resources.js b/rd_ui/app/scripts/services/resources.js
index 8a4ca34362..3d160c999d 100644
--- a/rd_ui/app/scripts/services/resources.js
+++ b/rd_ui/app/scripts/services/resources.js
@@ -417,7 +417,7 @@
return QueryResult;
};
- var Query = function ($resource, QueryResult, DataSource) {
+ var Query = function ($resource, $location, QueryResult) {
var Query = $resource('api/queries/:id', {id: '@id'},
{
search: {
@@ -429,32 +429,19 @@
method: 'get',
isArray: true,
url: "api/queries/recent"
- }});
+ }
+ });
Query.newQuery = function () {
return new Query({
query: "",
name: "New Query",
schedule: null,
- user: currentUser
+ user: currentUser,
+ options: {}
});
};
- Query.collectParamsFromQueryString = function($location, query) {
- var parameterNames = query.getParameters();
- var parameters = {};
-
- var queryString = $location.search();
- _.each(parameterNames, function(param, i) {
- var qsName = "p_" + param;
- if (qsName in queryString) {
- parameters[param] = queryString[qsName];
- }
- });
-
- return parameters;
- };
-
Query.prototype.getSourceLink = function () {
return '/queries/' + this.id + '/source';
};
@@ -477,32 +464,31 @@
};
Query.prototype.paramsRequired = function() {
- var queryParameters = this.getParameters();
- return !_.isEmpty(queryParameters);
+ return this.getParameters().isRequired();
};
- Query.prototype.getQueryResult = function (maxAge, parameters) {
+ Query.prototype.getQueryResult = function (maxAge) {
if (!this.query) {
return;
}
var queryText = this.query;
- var queryParameters = this.getParameters();
- var paramsRequired = !_.isEmpty(queryParameters);
-
- var missingParams = parameters === undefined ? queryParameters : _.difference(queryParameters, _.keys(parameters));
+ var parameters = this.getParameters();
+ var missingParams = parameters.getMissing();
- if (paramsRequired && missingParams.length > 0) {
+ if (missingParams.length > 0) {
var paramsWord = "parameter";
+ var valuesWord = "value";
if (missingParams.length > 1) {
paramsWord = "parameters";
+ valuesWord = "values";
}
- return new QueryResult({job: {error: "Missing values for " + missingParams.join(', ') + " "+paramsWord+".", status: 4}});
+ return new QueryResult({job: {error: "missing " + valuesWord + " for " + missingParams.join(', ') + " "+paramsWord+".", status: 4}});
}
- if (paramsRequired) {
- queryText = Mustache.render(queryText, parameters);
+ if (parameters.isRequired()) {
+ queryText = Mustache.render(queryText, parameters.getValues());
// Need to clear latest results, to make sure we don't use results for different params.
this.latest_query_data = null;
@@ -526,34 +512,142 @@
return this.queryResult;
};
+ Query.prototype.getUrl = function(source, hash) {
+ var url = "queries/" + this.id;
+
+ if (source) {
+ url += '/source';
+ }
+
+ var params = "";
+ if (this.getParameters().isRequired()) {
+ _.each(this.getParameters().getValues(), function(value, name) {
+ if (value === null) {
+ return;
+ }
+
+ if (params !== "") {
+ params += "&";
+ }
+
+ params += 'p_' + encodeURIComponent(name) + "=" + encodeURIComponent(value);
+ });
+ }
+
+ if (params !== "") {
+ url += "?" + params;
+ }
+
+ if (hash) {
+ url += "#" + hash;
+ }
+
+ return url;
+ }
+
Query.prototype.getQueryResultPromise = function() {
return this.getQueryResult().toPromise();
};
- Query.prototype.getParameters = function() {
- var parts = Mustache.parse(this.query);
- var parameters = [];
- var collectParams = function(parts) {
- parameters = [];
- _.each(parts, function(part) {
- if (part[0] == 'name' || part[0] == '&') {
- parameters.push(part[1]);
- } else if (part[0] == '#') {
- parameters = _.union(parameters, collectParams(part[4]));
+
+ var Parameters = function(query) {
+ this.query = query;
+
+ this.parseQuery = function() {
+ var parts = Mustache.parse(this.query.query);
+ var parameters = [];
+ var collectParams = function(parts) {
+ parameters = [];
+ _.each(parts, function(part) {
+ if (part[0] == 'name' || part[0] == '&') {
+ parameters.push(part[1]);
+ } else if (part[0] == '#') {
+ parameters = _.union(parameters, collectParams(part[4]));
+ }
+ });
+ return parameters;
+ };
+
+ parameters = collectParams(parts);
+
+ return parameters;
+ }
+
+ this.updateParameters = function() {
+ if (this.query.query === this.cachedQueryText) {
+ return;
+ }
+
+ this.cachedQueryText = this.query.query;
+ var parameterNames = this.parseQuery();
+
+ this.query.options.parameters = this.query.options.parameters || {};
+
+ var parametersMap = {};
+ _.each(this.query.options.parameters, function(param) {
+ parametersMap[param.name] = param;
+ });
+
+ _.each(parameterNames, function(param) {
+ if (!_.has(parametersMap, param)) {
+ this.query.options.parameters.push({
+ 'title': param,
+ 'name': param,
+ 'type': 'text',
+ 'value': null
+ });
+ }
+ }.bind(this));
+
+ this.query.options.parameters = _.filter(this.query.options.parameters, function(p) { return _.indexOf(parameterNames, p.name) !== -1});
+ }
+
+ this.initFromQueryString = function() {
+ var queryString = $location.search();
+ _.each(this.get(), function(param) {
+ var queryStringName = 'p_' + param.name;
+ if (_.has(queryString, queryStringName)) {
+ param.value = queryString[queryStringName];
}
});
- return parameters;
- };
+ }
- parameters = collectParams(parts);
+ this.updateParameters();
+ this.initFromQueryString();
+ }
+
+ Parameters.prototype.get = function() {
+ this.updateParameters();
+ return this.query.options.parameters;
+ };
- return parameters;
+ Parameters.prototype.getMissing = function() {
+ return _.pluck(_.filter(this.get(), function(p) { return p.value === null || p.value === ''; }), 'title');
}
- return Query;
- };
+ Parameters.prototype.isRequired = function() {
+ return !_.isEmpty(this.get());
+ }
+
+ Parameters.prototype.getValues = function() {
+ var params = this.get();
+ return _.object(_.pluck(params, 'name'), _.pluck(params, 'value'));
+ }
+ Query.prototype.getParameters = function() {
+ if (!this.$parameters) {
+ this.$parameters = new Parameters(this);
+ }
+
+ return this.$parameters;
+ }
+ Query.prototype.getParametersDefs = function() {
+ return this.getParameters().get();
+ }
+
+ return Query;
+ };
var DataSource = function ($resource) {
var actions = {
@@ -667,7 +761,7 @@
angular.module('redash.services')
.factory('QueryResult', ['$resource', '$timeout', '$q', QueryResult])
- .factory('Query', ['$resource', 'QueryResult', 'DataSource', Query])
+ .factory('Query', ['$resource', '$location', 'QueryResult', Query])
.factory('DataSource', ['$resource', DataSource])
.factory('Destination', ['$resource', Destination])
.factory('Alert', ['$resource', '$http', Alert])
diff --git a/rd_ui/app/styles/redash.css b/rd_ui/app/styles/redash.css
index c93fc193f5..d0780c24e6 100644
--- a/rd_ui/app/styles/redash.css
+++ b/rd_ui/app/styles/redash.css
@@ -158,23 +158,6 @@ a.navbar-brand img {
}
-/* Visualization Filters */
-
-.filters-container {
- display: flex;
- flex-wrap: wrap;
-}
-
-.filter {
- width: 33%;
- padding-left: 5px;
- padding-bottom: 5px;
-}
-
-.filter > div {
- width: 100%;
-}
-
/* Gridster */
.gridster ul {
@@ -658,7 +641,25 @@ div.table-name:hover {
background-color: rgba(0, 0, 0, 0.1);
}
-hr.subscription {
- height: 2px;
- background: #333;
+/* ui-select adjustments for SuperFlat */
+
+/* Same definition as .form-control */
+.ui-select-toggle.btn-default {
+ height: 35px;
+ padding: 6px 12px;
+ font-size: 13px;
+ line-height: 1.42857143;
+ color: #9E9E9E;
+ background: #fff none;
+ border: 1px solid #e8e8e8;
+ border-radius: 5px;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
+ -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
+ transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
+}
+
+.t-header.widget {
+ padding: 5px;
}
diff --git a/rd_ui/app/views/dashboard.html b/rd_ui/app/views/dashboard.html
index 4274b2d795..49ef6ceaa4 100644
--- a/rd_ui/app/views/dashboard.html
+++ b/rd_ui/app/views/dashboard.html
@@ -5,7 +5,7 @@