mirror of
https://github.com/getredash/redash.git
synced 2025-12-19 17:37:19 -05:00
Merge pull request #1069 from getredash/feature/params_ui
Feature: UI for query parameters
This commit is contained in:
10
migrations/0024_add_options_to_query.py
Normal file
10
migrations/0024_add_options_to_query.py
Normal file
@@ -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),
|
||||||
|
)
|
||||||
@@ -211,14 +211,20 @@
|
|||||||
|
|
||||||
Events.record(currentUser, "view", "widget", $scope.widget.id);
|
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) {
|
if ($scope.widget.visualization) {
|
||||||
Events.record(currentUser, "view", "query", $scope.widget.visualization.query.id);
|
Events.record(currentUser, "view", "query", $scope.widget.visualization.query.id);
|
||||||
Events.record(currentUser, "view", "visualization", $scope.widget.visualization.id);
|
Events.record(currentUser, "view", "visualization", $scope.widget.visualization.id);
|
||||||
|
|
||||||
$scope.query = $scope.widget.getQuery();
|
$scope.query = $scope.widget.getQuery();
|
||||||
var parameters = Query.collectParamsFromQueryString($location, $scope.query);
|
$scope.reload(false);
|
||||||
var maxAge = $location.search()['maxAge'];
|
|
||||||
$scope.queryResult = $scope.query.getQueryResult(maxAge, parameters);
|
|
||||||
|
|
||||||
$scope.type = 'visualization';
|
$scope.type = 'visualization';
|
||||||
} else if ($scope.widget.restricted) {
|
} else if ($scope.widget.restricted) {
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
var DEFAULT_TAB = 'table';
|
var DEFAULT_TAB = 'table';
|
||||||
|
|
||||||
var getQueryResult = function(maxAge) {
|
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) {
|
if (maxAge === undefined) {
|
||||||
maxAge = $location.search()['maxAge'];
|
maxAge = $location.search()['maxAge'];
|
||||||
}
|
}
|
||||||
@@ -16,7 +14,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$scope.showLog = false;
|
$scope.showLog = false;
|
||||||
$scope.queryResult = $scope.query.getQueryResult(maxAge, parameters);
|
$scope.queryResult = $scope.query.getQueryResult(maxAge);
|
||||||
};
|
};
|
||||||
|
|
||||||
var getDataSourceId = function() {
|
var getDataSourceId = function() {
|
||||||
@@ -127,7 +125,10 @@
|
|||||||
if (data) {
|
if (data) {
|
||||||
data.id = $scope.query.id;
|
data.id = $scope.query.id;
|
||||||
} else {
|
} 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({}, {
|
options = _.extend({}, {
|
||||||
@@ -135,9 +136,6 @@
|
|||||||
errorMessage: 'Query could not be saved'
|
errorMessage: 'Query could not be saved'
|
||||||
}, options);
|
}, options);
|
||||||
|
|
||||||
delete data.latest_query_data;
|
|
||||||
delete data.queryResult;
|
|
||||||
|
|
||||||
return Query.save(data, function() {
|
return Query.save(data, function() {
|
||||||
growl.addSuccessMessage(options.successMessage);
|
growl.addSuccessMessage(options.successMessage);
|
||||||
}, function(httpResponse) {
|
}, function(httpResponse) {
|
||||||
|
|||||||
@@ -92,13 +92,14 @@
|
|||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
scope: {
|
scope: {
|
||||||
'tabId': '@',
|
'tabId': '@',
|
||||||
'name': '@'
|
'name': '@',
|
||||||
|
'basePath': '=?'
|
||||||
},
|
},
|
||||||
transclude: true,
|
transclude: true,
|
||||||
template: '<li class="rd-tab" ng-class="{active: tabId==selectedTab}"><a href="{{basePath}}#{{tabId}}">{{name}}<span ng-transclude></span></a></li>',
|
template: '<li class="rd-tab" ng-class="{active: tabId==selectedTab}"><a href="{{basePath}}#{{tabId}}">{{name}}<span ng-transclude></span></a></li>',
|
||||||
replace: true,
|
replace: true,
|
||||||
link: function (scope) {
|
link: function (scope) {
|
||||||
scope.basePath = $location.path().substring(1);
|
scope.basePath = scope.basePath || $location.path().substring(1);
|
||||||
scope.$watch(function () {
|
scope.$watch(function () {
|
||||||
return scope.$parent.selectedTab
|
return scope.$parent.selectedTab
|
||||||
}, function (tab) {
|
}, 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;
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -10,29 +10,29 @@
|
|||||||
},
|
},
|
||||||
template: '<a ng-href="{{link}}" class="query-link">{{query.name}}</a>',
|
template: '<a ng-href="{{link}}" class="query-link">{{query.name}}</a>',
|
||||||
link: function(scope, element) {
|
link: function(scope, element) {
|
||||||
scope.link = 'queries/' + scope.query.id;
|
var hash = null;
|
||||||
if (scope.visualization) {
|
if (scope.visualization) {
|
||||||
if (scope.visualization.type === 'TABLE') {
|
if (scope.visualization.type === 'TABLE') {
|
||||||
// link to hard-coded table tab instead of the (hidden) visualization tab
|
// link to hard-coded table tab instead of the (hidden) visualization tab
|
||||||
scope.link += '#table';
|
hash = 'table';
|
||||||
} else {
|
} 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 {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
template: '<span ng-show="query.id && canViewSource">\
|
template: '<span ng-show="query.id && canViewSource">\
|
||||||
<a ng-show="!sourceMode"\
|
<a ng-show="!sourceMode"\
|
||||||
ng-href="queries/{{query.id}}/source#{{selectedTab}}" class="btn btn-default">Show Source\
|
ng-href="{{query.getUrl(true, selectedTab)}}" class="btn btn-default">Show Source\
|
||||||
</a>\
|
</a>\
|
||||||
<a ng-show="sourceMode"\
|
<a ng-show="sourceMode"\
|
||||||
ng-href="queries/{{query.id}}#{{selectedTab}}" class="btn btn-default">Hide Source\
|
ng-href="{{query.getUrl(false, selectedTab)}}" class="btn btn-default">Hide Source\
|
||||||
</a>\
|
</a>\
|
||||||
</span>'
|
</span>'
|
||||||
}
|
}
|
||||||
@@ -285,7 +285,7 @@
|
|||||||
|
|
||||||
angular.module('redash.directives')
|
angular.module('redash.directives')
|
||||||
.directive('queryLink', queryLink)
|
.directive('queryLink', queryLink)
|
||||||
.directive('querySourceLink', querySourceLink)
|
.directive('querySourceLink', ['$location', querySourceLink])
|
||||||
.directive('queryResultLink', queryResultLink)
|
.directive('queryResultLink', queryResultLink)
|
||||||
.directive('queryEditor', queryEditor)
|
.directive('queryEditor', queryEditor)
|
||||||
.directive('queryRefreshSelect', queryRefreshSelect)
|
.directive('queryRefreshSelect', queryRefreshSelect)
|
||||||
|
|||||||
@@ -120,4 +120,10 @@ angular.module('redash.filters', []).
|
|||||||
filtered.push(items[i])
|
filtered.push(items[i])
|
||||||
return filtered;
|
return filtered;
|
||||||
};
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
.filter('notEmpty', function() {
|
||||||
|
return function(collection) {
|
||||||
|
return !_.isEmpty(collection);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -417,7 +417,7 @@
|
|||||||
return QueryResult;
|
return QueryResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
var Query = function ($resource, QueryResult, DataSource) {
|
var Query = function ($resource, $location, QueryResult) {
|
||||||
var Query = $resource('api/queries/:id', {id: '@id'},
|
var Query = $resource('api/queries/:id', {id: '@id'},
|
||||||
{
|
{
|
||||||
search: {
|
search: {
|
||||||
@@ -429,32 +429,19 @@
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
isArray: true,
|
isArray: true,
|
||||||
url: "api/queries/recent"
|
url: "api/queries/recent"
|
||||||
}});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Query.newQuery = function () {
|
Query.newQuery = function () {
|
||||||
return new Query({
|
return new Query({
|
||||||
query: "",
|
query: "",
|
||||||
name: "New Query",
|
name: "New Query",
|
||||||
schedule: null,
|
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 () {
|
Query.prototype.getSourceLink = function () {
|
||||||
return '/queries/' + this.id + '/source';
|
return '/queries/' + this.id + '/source';
|
||||||
};
|
};
|
||||||
@@ -477,32 +464,31 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
Query.prototype.paramsRequired = function() {
|
Query.prototype.paramsRequired = function() {
|
||||||
var queryParameters = this.getParameters();
|
return this.getParameters().isRequired();
|
||||||
return !_.isEmpty(queryParameters);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Query.prototype.getQueryResult = function (maxAge, parameters) {
|
Query.prototype.getQueryResult = function (maxAge) {
|
||||||
if (!this.query) {
|
if (!this.query) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var queryText = this.query;
|
var queryText = this.query;
|
||||||
|
|
||||||
var queryParameters = this.getParameters();
|
var parameters = this.getParameters();
|
||||||
var paramsRequired = !_.isEmpty(queryParameters);
|
var missingParams = parameters.getMissing();
|
||||||
|
|
||||||
var missingParams = parameters === undefined ? queryParameters : _.difference(queryParameters, _.keys(parameters));
|
if (missingParams.length > 0) {
|
||||||
|
|
||||||
if (paramsRequired && missingParams.length > 0) {
|
|
||||||
var paramsWord = "parameter";
|
var paramsWord = "parameter";
|
||||||
|
var valuesWord = "value";
|
||||||
if (missingParams.length > 1) {
|
if (missingParams.length > 1) {
|
||||||
paramsWord = "parameters";
|
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) {
|
if (parameters.isRequired()) {
|
||||||
queryText = Mustache.render(queryText, parameters);
|
queryText = Mustache.render(queryText, parameters.getValues());
|
||||||
|
|
||||||
// Need to clear latest results, to make sure we don't use results for different params.
|
// Need to clear latest results, to make sure we don't use results for different params.
|
||||||
this.latest_query_data = null;
|
this.latest_query_data = null;
|
||||||
@@ -526,35 +512,143 @@
|
|||||||
return this.queryResult;
|
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() {
|
Query.prototype.getQueryResultPromise = function() {
|
||||||
return this.getQueryResult().toPromise();
|
return this.getQueryResult().toPromise();
|
||||||
};
|
};
|
||||||
|
|
||||||
Query.prototype.getParameters = function() {
|
|
||||||
var parts = Mustache.parse(this.query);
|
var Parameters = function(query) {
|
||||||
var parameters = [];
|
this.query = query;
|
||||||
var collectParams = function(parts) {
|
|
||||||
parameters = [];
|
this.parseQuery = function() {
|
||||||
_.each(parts, function(part) {
|
var parts = Mustache.parse(this.query.query);
|
||||||
if (part[0] == 'name' || part[0] == '&') {
|
var parameters = [];
|
||||||
parameters.push(part[1]);
|
var collectParams = function(parts) {
|
||||||
} else if (part[0] == '#') {
|
parameters = [];
|
||||||
parameters = _.union(parameters, collectParams(part[4]));
|
_.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();
|
||||||
|
}
|
||||||
|
|
||||||
return parameters;
|
Parameters.prototype.get = function() {
|
||||||
|
this.updateParameters();
|
||||||
|
return this.query.options.parameters;
|
||||||
|
};
|
||||||
|
|
||||||
|
Parameters.prototype.getMissing = function() {
|
||||||
|
return _.pluck(_.filter(this.get(), function(p) { return p.value === null || p.value === ''; }), 'title');
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
return Query;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var DataSource = function ($resource) {
|
var DataSource = function ($resource) {
|
||||||
var actions = {
|
var actions = {
|
||||||
'get': {'method': 'GET', 'cache': false, 'isArray': false},
|
'get': {'method': 'GET', 'cache': false, 'isArray': false},
|
||||||
@@ -667,7 +761,7 @@
|
|||||||
|
|
||||||
angular.module('redash.services')
|
angular.module('redash.services')
|
||||||
.factory('QueryResult', ['$resource', '$timeout', '$q', QueryResult])
|
.factory('QueryResult', ['$resource', '$timeout', '$q', QueryResult])
|
||||||
.factory('Query', ['$resource', 'QueryResult', 'DataSource', Query])
|
.factory('Query', ['$resource', '$location', 'QueryResult', Query])
|
||||||
.factory('DataSource', ['$resource', DataSource])
|
.factory('DataSource', ['$resource', DataSource])
|
||||||
.factory('Destination', ['$resource', Destination])
|
.factory('Destination', ['$resource', Destination])
|
||||||
.factory('Alert', ['$resource', '$http', Alert])
|
.factory('Alert', ['$resource', '$http', Alert])
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|
||||||
.gridster ul {
|
.gridster ul {
|
||||||
@@ -658,7 +641,25 @@ div.table-name:hover {
|
|||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
hr.subscription {
|
/* ui-select adjustments for SuperFlat */
|
||||||
height: 2px;
|
|
||||||
background: #333;
|
/* 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<page-header title="{{dashboard.name}}">
|
<page-header title="{{dashboard.name}}">
|
||||||
<span ng-if="!dashboard.is_archived && !public" class="hidden-print">
|
<span ng-if="!dashboard.is_archived && !public" class="hidden-print">
|
||||||
<button type="button" class="btn btn-sm" ng-class="{'btn-default': !refreshEnabled, 'btn-primary': refreshEnabled}" tooltip="Enable/Disable Auto Refresh" ng-click="triggerRefresh()">
|
<button type="button" class="btn btn-sm" ng-class="{'btn-default': !refreshEnabled, 'btn-primary': refreshEnabled}" tooltip="Enable/Disable Auto Refresh" ng-click="triggerRefresh()">
|
||||||
<span class="zmdi zmdi-refresh-sync"></span>
|
<span class="zmdi zmdi-refresh"></span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm" ng-class="{'btn-default': !isFullscreen, 'btn-primary': isFullscreen}" tooltip="Enable/Disable Fullscreen display" ng-click="toggleFullscreen()">
|
<button type="button" class="btn btn-sm" ng-class="{'btn-default': !isFullscreen, 'btn-primary': isFullscreen}" tooltip="Enable/Disable Fullscreen display" ng-click="toggleFullscreen()">
|
||||||
<span class="zmdi zmdi-fullscreen"></span>
|
<span class="zmdi zmdi-fullscreen"></span>
|
||||||
@@ -28,13 +28,15 @@
|
|||||||
<div class="col-lg-12 p-5 m-b-10 bg-orange c-white" ng-if="dashboard.is_archived">
|
<div class="col-lg-12 p-5 m-b-10 bg-orange c-white" ng-if="dashboard.is_archived">
|
||||||
This dashboard is archived and won't appear in the dashboards list or search results.
|
This dashboard is archived and won't appear in the dashboards list or search results.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="m-b-5">
|
||||||
<filters ng-if="dashboard.dashboard_filters_enabled"></filters>
|
<filters ng-if="dashboard.dashboard_filters_enabled"></filters>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div ng-repeat="row in dashboard.widgets" class="row">
|
<div ng-repeat="row in dashboard.widgets" class="row">
|
||||||
<div ng-repeat="widget in row" class="col-lg-{{widget.width | colWidth}}" ng-controller='WidgetCtrl'>
|
<div ng-repeat="widget in row" class="col-lg-{{widget.width | colWidth}}" ng-controller='WidgetCtrl'>
|
||||||
<div class="tile" ng-if="type=='visualization'">
|
<div class="tile" ng-if="type=='visualization'">
|
||||||
<div class="t-header">
|
<div class="t-header widget">
|
||||||
<div class="th-title">
|
<div class="th-title">
|
||||||
<p class="hidden-print">
|
<p class="hidden-print">
|
||||||
<span ng-hide="currentUser.hasPermission('view_query')">{{query.name}}</span>
|
<span ng-hide="currentUser.hasPermission('view_query')">{{query.name}}</span>
|
||||||
@@ -59,13 +61,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<visualization-renderer visualization="widget.visualization" query-result="queryResult" class="t-body"></visualization-renderer>
|
<parameters parameters="widget.query.getParametersDefs()"></parameters>
|
||||||
|
|
||||||
<div class="panel-footer">
|
<div ng-switch="queryResult.getStatus()">
|
||||||
<span class="label label-default hidden-print">Updated: <span am-time-ago="queryResult.getUpdatedAt()"></span></span>
|
<div ng-switch-when="failed">
|
||||||
|
<div class="alert alert-danger m-5" ng-show="queryResult.getError()">Error running query: <strong>{{queryResult.getError()}}</strong></div>
|
||||||
|
</div>
|
||||||
|
<div ng-switch-when="done">
|
||||||
|
<visualization-renderer visualization="widget.visualization" query-result="queryResult" class="t-body"></visualization-renderer>
|
||||||
|
</div>
|
||||||
|
<div ng-switch-default class="text-center">
|
||||||
|
<i class="zmdi zmdi-refresh zmdi-hc-spin zmdi-hc-5x"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-5 clearfix" style="line-height:28px;">
|
||||||
|
<span class="small hidden-print">Updated: <span am-time-ago="queryResult.getUpdatedAt()"></span></span>
|
||||||
<span class="visible-print">
|
<span class="visible-print">
|
||||||
Updated: {{queryResult.getUpdatedAt() | dateTime}}
|
Updated: {{queryResult.getUpdatedAt() | dateTime}}
|
||||||
</span>
|
</span>
|
||||||
|
<button class="btn btn-sm btn-default pull-right hidden-print" ng-click="reload(true)" ng-if="!public"><i class="zmdi zmdi-refresh"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
<dynamic-form target="destination" type="destinations">
|
<dynamic-form target="destination" type="destinations">
|
||||||
<button class="btn btn-danger" ng-if="destination.id" ng-click="delete()">Delete</button>
|
<button class="btn btn-danger" ng-if="destination.id" ng-click="delete()">Delete</button>
|
||||||
</dynamic-form>
|
</dynamic-form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</settings-screen>
|
</settings-screen>
|
||||||
|
|||||||
21
rd_ui/app/views/dialogs/parameter_settings.html
Normal file
21
rd_ui/app/views/dialogs/parameter_settings.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" aria-label="Close" ng-click="close()"><span aria-hidden="true">×</span></button>
|
||||||
|
<h4 class="modal-title">{{parameter.name}}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Title</label>
|
||||||
|
<input type="text" class="form-control" ng-model="parameter.title">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Type</label>
|
||||||
|
<select ng-model="parameter.type" class="form-control">
|
||||||
|
<option value="text">Text</option>
|
||||||
|
<option value="number">Number</option>
|
||||||
|
<option value="date">Date</option>
|
||||||
|
<option value="datetime-local">Date and Time</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
7
rd_ui/app/views/directives/parameters.html
Normal file
7
rd_ui/app/views/directives/parameters.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<div class="form-inline bg-white p-5" ng-if="parameters | notEmpty" ui-sortable="{ 'ui-floating': true, 'disabled': !editable }" ng-model="parameters">
|
||||||
|
<div class="form-group" ng-repeat="param in parameters">
|
||||||
|
<label>{{param.title}}</label>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="showParameterSettings(param)" ng-if="editable"><i class="zmdi zmdi-settings"></i></button>
|
||||||
|
<input type="{{param.type}}" class="form-control" ng-model="param.value">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -221,6 +221,7 @@
|
|||||||
<div class="t-body">
|
<div class="t-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
|
<parameters parameters="query.getParametersDefs()" sync-values="!query.isNew()" editable="sourceMode && canEdit"></parameters>
|
||||||
<!-- Query Execution Status -->
|
<!-- Query Execution Status -->
|
||||||
<div class="alert alert-info" ng-show="queryResult.getStatus() == 'processing'">
|
<div class="alert alert-info" ng-show="queryResult.getStatus() == 'processing'">
|
||||||
Executing query…
|
Executing query…
|
||||||
@@ -255,9 +256,8 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<ul class="tab-nav">
|
<ul class="tab-nav">
|
||||||
<rd-tab tab-id="table" name="Table"></rd-tab>
|
<rd-tab tab-id="table" name="Table" base-path="query.getUrl(sourceMode)"></rd-tab>
|
||||||
<rd-tab tab-id="pivot" name="Pivot Table" ng-if="sourceMode && canEdit"></rd-tab>
|
<rd-tab tab-id="{{vis.id}}" name="{{vis.name}}" ng-if="vis.type!='TABLE'" base-path="query.getUrl(sourceMode)"
|
||||||
<rd-tab tab-id="{{vis.id}}" name="{{vis.name}}" ng-if="vis.type!='TABLE'"
|
|
||||||
ng-repeat="vis in query.visualizations">
|
ng-repeat="vis in query.visualizations">
|
||||||
<span class="remove" ng-click="deleteVisualization($event, vis)"
|
<span class="remove" ng-click="deleteVisualization($event, vis)"
|
||||||
ng-show="canEdit"> ×</span>
|
ng-show="canEdit"> ×</span>
|
||||||
@@ -276,12 +276,6 @@
|
|||||||
<button class="btn btn-default" ng-if="!query.isNew()" ng-click="showEmbedDialog(query, vis)"><i class="zmdi zmdi-code"></i> Embed</button>
|
<button class="btn btn-default" ng-if="!query.isNew()" ng-click="showEmbedDialog(query, vis)"><i class="zmdi zmdi-code"></i> Embed</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ng-show="sourceMode && canEdit && selectedTab == 'pivot'">
|
|
||||||
<h3>
|
|
||||||
Pivot tables are now regular visualization, which you can create from the
|
|
||||||
<a ng-click="openVisualizationEditor()">"New Visualization" screen</a> and <strong>save</strong>.
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div ng-if="selectedTab == vis.id" ng-repeat="vis in query.visualizations">
|
<div ng-if="selectedTab == vis.id" ng-repeat="vis in query.visualizations">
|
||||||
<visualization-renderer visualization="vis" query-result="queryResult"></visualization-renderer>
|
<visualization-renderer visualization="vis" query-result="queryResult"></visualization-renderer>
|
||||||
<div class="bg-ace p-5">
|
<div class="bg-ace p-5">
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
<div class="well well-sm filters-container" ng-show="filters">
|
<div class="container bg-white p-5" ng-show="filters">
|
||||||
<div class="filter" ng-repeat="filter in filters">
|
<div class="row" ng-show="filters">
|
||||||
<ui-select ng-model="filter.current" ng-if="!filter.multiple">
|
<div class="col-sm-6 m-t-5" ng-repeat="filter in filters">
|
||||||
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$select.selected | filterValue:filter}}</ui-select-match>
|
<ui-select ng-model="filter.current" ng-if="!filter.multiple">
|
||||||
<ui-select-choices repeat="value in filter.values | filter: $select.search">
|
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$select.selected | filterValue:filter}}</ui-select-match>
|
||||||
{{value | filterValue:filter }}
|
<ui-select-choices repeat="value in filter.values | filter: $select.search">
|
||||||
</ui-select-choices>
|
{{value | filterValue:filter }}
|
||||||
</ui-select>
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
<ui-select ng-model="filter.current" multiple ng-if="filter.multiple">
|
<ui-select ng-model="filter.current" multiple ng-if="filter.multiple">
|
||||||
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$item | filterValue:filter}}</ui-select-match>
|
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$item | filterValue:filter}}</ui-select-match>
|
||||||
<ui-select-choices repeat="value in filter.values | filter: $select.search">
|
<ui-select-choices repeat="value in filter.values | filter: $select.search">
|
||||||
{{value | filterValue:filter }}
|
{{value | filterValue:filter }}
|
||||||
</ui-select-choices>
|
</ui-select-choices>
|
||||||
</ui-select>
|
</ui-select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -86,8 +86,6 @@ class QueryResource(BaseResource):
|
|||||||
for field in ['id', 'created_at', 'api_key', 'visualizations', 'latest_query_data', 'user', 'last_modified_by', 'org']:
|
for field in ['id', 'created_at', 'api_key', 'visualizations', 'latest_query_data', 'user', 'last_modified_by', 'org']:
|
||||||
query_def.pop(field, None)
|
query_def.pop(field, None)
|
||||||
|
|
||||||
# TODO(@arikfr): after running a query it updates all relevant queries with the new result. So is this really
|
|
||||||
# needed?
|
|
||||||
if 'latest_query_data_id' in query_def:
|
if 'latest_query_data_id' in query_def:
|
||||||
query_def['latest_query_data'] = query_def.pop('latest_query_data_id')
|
query_def['latest_query_data'] = query_def.pop('latest_query_data_id')
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ class JSONField(peewee.TextField):
|
|||||||
return json.dumps(value)
|
return json.dumps(value)
|
||||||
|
|
||||||
def python_value(self, value):
|
def python_value(self, value):
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
return json.loads(value)
|
return json.loads(value)
|
||||||
|
|
||||||
|
|
||||||
@@ -585,11 +587,11 @@ class Query(ModelTimestampsMixin, BaseModel, BelongsToOrgMixin):
|
|||||||
query = peewee.TextField()
|
query = peewee.TextField()
|
||||||
query_hash = peewee.CharField(max_length=32)
|
query_hash = peewee.CharField(max_length=32)
|
||||||
api_key = peewee.CharField(max_length=40)
|
api_key = peewee.CharField(max_length=40)
|
||||||
user_email = peewee.CharField(max_length=360, null=True)
|
|
||||||
user = peewee.ForeignKeyField(User)
|
user = peewee.ForeignKeyField(User)
|
||||||
last_modified_by = peewee.ForeignKeyField(User, null=True, related_name="modified_queries")
|
last_modified_by = peewee.ForeignKeyField(User, null=True, related_name="modified_queries")
|
||||||
is_archived = peewee.BooleanField(default=False, index=True)
|
is_archived = peewee.BooleanField(default=False, index=True)
|
||||||
schedule = peewee.CharField(max_length=10, null=True)
|
schedule = peewee.CharField(max_length=10, null=True)
|
||||||
|
options = JSONField(default={})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'queries'
|
db_table = 'queries'
|
||||||
@@ -607,7 +609,8 @@ class Query(ModelTimestampsMixin, BaseModel, BelongsToOrgMixin):
|
|||||||
'is_archived': self.is_archived,
|
'is_archived': self.is_archived,
|
||||||
'updated_at': self.updated_at,
|
'updated_at': self.updated_at,
|
||||||
'created_at': self.created_at,
|
'created_at': self.created_at,
|
||||||
'data_source_id': self.data_source_id
|
'data_source_id': self.data_source_id,
|
||||||
|
'options': self.options
|
||||||
}
|
}
|
||||||
|
|
||||||
if with_user:
|
if with_user:
|
||||||
@@ -833,7 +836,6 @@ class Dashboard(ModelTimestampsMixin, BaseModel, BelongsToOrgMixin):
|
|||||||
org = peewee.ForeignKeyField(Organization, related_name="dashboards")
|
org = peewee.ForeignKeyField(Organization, related_name="dashboards")
|
||||||
slug = peewee.CharField(max_length=140, index=True)
|
slug = peewee.CharField(max_length=140, index=True)
|
||||||
name = peewee.CharField(max_length=100)
|
name = peewee.CharField(max_length=100)
|
||||||
user_email = peewee.CharField(max_length=360, null=True)
|
|
||||||
user = peewee.ForeignKeyField(User)
|
user = peewee.ForeignKeyField(User)
|
||||||
layout = peewee.TextField()
|
layout = peewee.TextField()
|
||||||
dashboard_filters_enabled = peewee.BooleanField(default=False)
|
dashboard_filters_enabled = peewee.BooleanField(default=False)
|
||||||
|
|||||||
Reference in New Issue
Block a user