mirror of
https://github.com/getredash/redash.git
synced 2025-12-19 09:27:23 -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);
|
||||
|
||||
$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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -92,13 +92,14 @@
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
'tabId': '@',
|
||||
'name': '@'
|
||||
'name': '@',
|
||||
'basePath': '=?'
|
||||
},
|
||||
transclude: true,
|
||||
template: '<li class="rd-tab" ng-class="{active: tabId==selectedTab}"><a href="{{basePath}}#{{tabId}}">{{name}}<span ng-transclude></span></a></li>',
|
||||
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;
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}]);
|
||||
|
||||
})();
|
||||
|
||||
@@ -10,29 +10,29 @@
|
||||
},
|
||||
template: '<a ng-href="{{link}}" class="query-link">{{query.name}}</a>',
|
||||
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: '<span ng-show="query.id && canViewSource">\
|
||||
<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 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>\
|
||||
</span>'
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -120,4 +120,10 @@ angular.module('redash.filters', []).
|
||||
filtered.push(items[i])
|
||||
return filtered;
|
||||
};
|
||||
})
|
||||
|
||||
.filter('notEmpty', function() {
|
||||
return function(collection) {
|
||||
return !_.isEmpty(collection);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 parameters = this.getParameters();
|
||||
var missingParams = parameters.getMissing();
|
||||
|
||||
var missingParams = parameters === undefined ? queryParameters : _.difference(queryParameters, _.keys(parameters));
|
||||
|
||||
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,35 +512,143 @@
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
var DataSource = function ($resource) {
|
||||
var actions = {
|
||||
'get': {'method': 'GET', 'cache': false, 'isArray': false},
|
||||
@@ -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])
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<page-header title="{{dashboard.name}}">
|
||||
<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()">
|
||||
<span class="zmdi zmdi-refresh-sync"></span>
|
||||
<span class="zmdi zmdi-refresh"></span>
|
||||
</button>
|
||||
<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>
|
||||
@@ -28,13 +28,15 @@
|
||||
<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.
|
||||
</div>
|
||||
|
||||
|
||||
<div class="m-b-5">
|
||||
<filters ng-if="dashboard.dashboard_filters_enabled"></filters>
|
||||
|
||||
</div>
|
||||
|
||||
<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 class="tile" ng-if="type=='visualization'">
|
||||
<div class="t-header">
|
||||
<div class="t-header widget">
|
||||
<div class="th-title">
|
||||
<p class="hidden-print">
|
||||
<span ng-hide="currentUser.hasPermission('view_query')">{{query.name}}</span>
|
||||
@@ -59,13 +61,26 @@
|
||||
</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">
|
||||
<span class="label label-default hidden-print">Updated: <span am-time-ago="queryResult.getUpdatedAt()"></span></span>
|
||||
<div ng-switch="queryResult.getStatus()">
|
||||
<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">
|
||||
Updated: {{queryResult.getUpdatedAt() | dateTime}}
|
||||
</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>
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
<dynamic-form target="destination" type="destinations">
|
||||
<button class="btn btn-danger" ng-if="destination.id" ng-click="delete()">Delete</button>
|
||||
</dynamic-form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</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="row">
|
||||
<div class="col-lg-12">
|
||||
<parameters parameters="query.getParametersDefs()" sync-values="!query.isNew()" editable="sourceMode && canEdit"></parameters>
|
||||
<!-- Query Execution Status -->
|
||||
<div class="alert alert-info" ng-show="queryResult.getStatus() == 'processing'">
|
||||
Executing query…
|
||||
@@ -255,9 +256,8 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<ul class="tab-nav">
|
||||
<rd-tab tab-id="table" name="Table"></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'"
|
||||
<rd-tab tab-id="table" name="Table" base-path="query.getUrl(sourceMode)"></rd-tab>
|
||||
<rd-tab tab-id="{{vis.id}}" name="{{vis.name}}" ng-if="vis.type!='TABLE'" base-path="query.getUrl(sourceMode)"
|
||||
ng-repeat="vis in query.visualizations">
|
||||
<span class="remove" ng-click="deleteVisualization($event, vis)"
|
||||
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>
|
||||
</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">
|
||||
<visualization-renderer visualization="vis" query-result="queryResult"></visualization-renderer>
|
||||
<div class="bg-ace p-5">
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<div class="well well-sm filters-container" ng-show="filters">
|
||||
<div class="filter" ng-repeat="filter in filters">
|
||||
<ui-select ng-model="filter.current" ng-if="!filter.multiple">
|
||||
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$select.selected | filterValue:filter}}</ui-select-match>
|
||||
<ui-select-choices repeat="value in filter.values | filter: $select.search">
|
||||
{{value | filterValue:filter }}
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
<div class="container bg-white p-5" ng-show="filters">
|
||||
<div class="row" ng-show="filters">
|
||||
<div class="col-sm-6 m-t-5" ng-repeat="filter in filters">
|
||||
<ui-select ng-model="filter.current" ng-if="!filter.multiple">
|
||||
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$select.selected | filterValue:filter}}</ui-select-match>
|
||||
<ui-select-choices repeat="value in filter.values | filter: $select.search">
|
||||
{{value | filterValue:filter }}
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
|
||||
<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-choices repeat="value in filter.values | filter: $select.search">
|
||||
{{value | filterValue:filter }}
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
<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-choices repeat="value in filter.values | filter: $select.search">
|
||||
{{value | filterValue:filter }}
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</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']:
|
||||
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:
|
||||
query_def['latest_query_data'] = query_def.pop('latest_query_data_id')
|
||||
|
||||
|
||||
@@ -72,6 +72,8 @@ class JSONField(peewee.TextField):
|
||||
return json.dumps(value)
|
||||
|
||||
def python_value(self, value):
|
||||
if not value:
|
||||
return value
|
||||
return json.loads(value)
|
||||
|
||||
|
||||
@@ -585,11 +587,11 @@ class Query(ModelTimestampsMixin, BaseModel, BelongsToOrgMixin):
|
||||
query = peewee.TextField()
|
||||
query_hash = peewee.CharField(max_length=32)
|
||||
api_key = peewee.CharField(max_length=40)
|
||||
user_email = peewee.CharField(max_length=360, null=True)
|
||||
user = peewee.ForeignKeyField(User)
|
||||
last_modified_by = peewee.ForeignKeyField(User, null=True, related_name="modified_queries")
|
||||
is_archived = peewee.BooleanField(default=False, index=True)
|
||||
schedule = peewee.CharField(max_length=10, null=True)
|
||||
options = JSONField(default={})
|
||||
|
||||
class Meta:
|
||||
db_table = 'queries'
|
||||
@@ -607,7 +609,8 @@ class Query(ModelTimestampsMixin, BaseModel, BelongsToOrgMixin):
|
||||
'is_archived': self.is_archived,
|
||||
'updated_at': self.updated_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:
|
||||
@@ -833,7 +836,6 @@ class Dashboard(ModelTimestampsMixin, BaseModel, BelongsToOrgMixin):
|
||||
org = peewee.ForeignKeyField(Organization, related_name="dashboards")
|
||||
slug = peewee.CharField(max_length=140, index=True)
|
||||
name = peewee.CharField(max_length=100)
|
||||
user_email = peewee.CharField(max_length=360, null=True)
|
||||
user = peewee.ForeignKeyField(User)
|
||||
layout = peewee.TextField()
|
||||
dashboard_filters_enabled = peewee.BooleanField(default=False)
|
||||
|
||||
Reference in New Issue
Block a user