Compare commits

..

28 Commits

Author SHA1 Message Date
Arik Fraimovich
d82d5c3bdc Merge pull request #372 from EverythingMe/feature/variables
Several UI fixes
2015-02-08 18:05:05 +02:00
Arik Fraimovich
dfe58b3953 Give the user the option to disable sorting of chart data 2015-02-08 18:02:36 +02:00
Arik Fraimovich
44019b8357 Variables: allow nesting variables 2015-02-08 17:07:20 +02:00
Arik Fraimovich
3c15a44faf Fix: keyboard shortcuts were not unbinded 2015-02-08 17:07:06 +02:00
Arik Fraimovich
8d113dadd2 Revert "Fix #242: handle the case there is no connection to the server"
This reverts commit 3960005002.

Conflicts:
	rd_ui/app/index.html
	rd_ui/bower.json
2015-02-02 18:02:42 +02:00
Arik Fraimovich
c1dd26aee7 Merge pull request #370 from alexanderlz/master
add ISO datetime to filename when saving chart as image
2015-02-02 10:52:00 +02:00
Alexander Leibzon
b2228c2a39 replace 'possibly dangerous for some OSs' characters 2015-02-01 15:29:46 +02:00
Alexander Leibzon
d9618cb09c add ISO datetime to filename when saving chart as image 2015-02-01 14:52:58 +02:00
Arik Fraimovich
c8ca683d3a Merge pull request #368 from alexanderlz/master
Issue #168. Visualization: save as image.
2015-02-01 13:22:40 +02:00
Alexander Leibzon
888963ffaa Merge branch 'master' of https://github.com/alexanderlz/redash
Conflicts:
	rd_ui/app/index.html
	rd_ui/bower.json
2015-02-01 13:20:26 +02:00
Alexander Leibzon
ae947a8310 removing unwanted commit 2015-02-01 13:18:57 +02:00
Alexander Leibzon
bee9cde347 removing unwanted commit 2015-02-01 12:00:23 +02:00
Arik Fraimovich
c131dab125 Merge pull request #369 from EverythingMe/fix/dashboard_filters
Fix: filters got linked when they shouldn't have.
2015-02-01 11:57:46 +02:00
Arik Fraimovich
e113642ae4 Fix: filters got linked when they shouldn't have.
- Make a copy of the first filter, to prevent it controlling the other filters.
- If no query string value given or dashboard filters enabled, don't link filters.
2015-02-01 11:51:07 +02:00
Arik Fraimovich
b76906b168 Merge pull request #367 from EverythingMe/feature/offline
Fix #242: handle the case there is no connection to the server
2015-01-29 20:46:32 +02:00
Arik Fraimovich
3960005002 Fix #242: handle the case there is no connection to the server 2015-01-29 20:43:03 +02:00
Arik Fraimovich
3dde578b86 Only try to render params if they are required. 2015-01-29 15:33:08 +02:00
Arik Fraimovich
813f0e74ff Merge pull request #366 from EverythingMe/fix/mget_error
Several chart editor fixes and additions
2015-01-27 22:49:41 +02:00
Arik Fraimovich
1e4e37c2ce Ability to set y axis min/max (closes #257) 2015-01-27 22:45:16 +02:00
Arik Fraimovich
a00c80eab2 Don't change zIndex if it was already set 2015-01-27 22:30:26 +02:00
Arik Fraimovich
496e5ebe8c Fix: if new series was created in result set, it wasn't using the default chart type 2015-01-27 22:23:10 +02:00
Arik Fraimovich
18cc8434a0 Merge pull request #365 from EverythingMe/fix/mget_error
Fix: when no queries are being run, cleanup job fails with error
2015-01-27 18:39:26 +02:00
Arik Fraimovich
5eba318019 Fix: when no queries are being run, cleanup job fails with error 2015-01-27 18:23:21 +02:00
Arik Fraimovich
63274dbb17 Merge pull request #363 from EverythingMe/feature/query_parameters
Feature: support for query parameters
2015-01-27 18:22:29 +02:00
Arik Fraimovich
4c73e788ae Ability to set ttl (max age) from query string 2015-01-27 17:17:58 +02:00
Arik Fraimovich
b71a2b3651 Enable query params in dashboard 2015-01-27 17:00:21 +02:00
Arik Fraimovich
6fb97675ad Add mustache to Karma conf 2015-01-27 11:28:51 +02:00
Arik Fraimovich
c0c102207d Initial work on support for query parameters 2015-01-27 10:28:11 +02:00
11 changed files with 201 additions and 53 deletions

View File

@@ -126,6 +126,10 @@
<script src="/scripts/ui-bootstrap-tpls-0.5.0.min.js"></script>
<script src="/bower_components/bucky/bucky.js"></script>
<script src="/bower_components/pace/pace.js"></script>
<script src="/bower_components/mustache/mustache.js"></script>
<script src="/bower_components/canvg/rgbcolor.js"></script>
<script src="/bower_components/canvg/StackBlur.js"></script>
<script src="/bower_components/canvg/canvg.js"></script>
<!-- endbuild -->
<!-- build:js({.tmp,app}) /scripts/scripts.js -->

View File

@@ -16,7 +16,7 @@
var w = new Widget(widget);
if (w.visualization) {
promises.push(w.getQuery().getQueryResultPromise());
promises.push(w.getQuery().getQueryResult().toPromise());
}
return w;
@@ -27,12 +27,19 @@
var filters = {};
_.each(queryResults, function(queryResult) {
var queryFilters = queryResult.getFilters();
_.each(queryFilters, function (filter) {
if (!_.has(filters, filter.name)) {
// TODO: first object should be a copy, otherwise one of the chart filters behaves different than the others.
_.each(queryFilters, function (queryFilter) {
var hasQueryStringValue = _.has($location.search(), queryFilter.name);
if (!(hasQueryStringValue || dashboard.dashboard_filters_enabled)) {
// If dashboard filters not enabled, or no query string value given, skip filters linking.
return;
}
if (!_.has(filters, queryFilter.name)) {
var filter = _.extend({}, queryFilter);
filters[filter.name] = filter;
filters[filter.name].originFilters = [];
if (_.has($location.search(), filter.name)) {
if (hasQueryStringValue) {
filter.current = $location.search()[filter.name];
}
@@ -44,7 +51,7 @@
}
// TODO: merge values.
filters[filter.name].originFilters.push(filter);
filters[queryFilter.name].originFilters.push(queryFilter);
});
});
@@ -104,7 +111,7 @@
};
};
var WidgetCtrl = function($scope, Events, Query) {
var WidgetCtrl = function($scope, $location, Events, Query) {
$scope.deleteWidget = function() {
if (!confirm('Are you sure you want to remove "' + $scope.widget.getName() + '" from the dashboard?')) {
return;
@@ -128,7 +135,9 @@
Events.record(currentUser, "view", "visualization", $scope.widget.visualization.id);
$scope.query = $scope.widget.getQuery();
$scope.queryResult = $scope.query.getQueryResult();
var parameters = Query.collectParamsFromQueryString($location, $scope.query);
var maxAge = $location.search()['maxAge'];
$scope.queryResult = $scope.query.getQueryResult(maxAge, parameters);
$scope.nextUpdateTime = moment(new Date(($scope.query.updated_at + $scope.query.ttl + $scope.query.runtime + 300) * 1000)).fromNow();
$scope.type = 'visualization';
@@ -139,6 +148,6 @@
angular.module('redash.controllers')
.controller('DashboardCtrl', ['$scope', 'Events', 'Widget', '$routeParams', '$location', '$http', '$timeout', '$q', 'Dashboard', DashboardCtrl])
.controller('WidgetCtrl', ['$scope', 'Events', 'Query', WidgetCtrl])
.controller('WidgetCtrl', ['$scope', '$location', 'Events', 'Query', WidgetCtrl])
})();

View File

@@ -14,27 +14,7 @@
var isNewQuery = !$scope.query.id,
queryText = $scope.query.query,
// ref to QueryViewCtrl.saveQuery
saveQuery = $scope.saveQuery,
shortcuts = {
'meta+s': function () {
if ($scope.canEdit) {
$scope.saveQuery();
}
},
'ctrl+s': function () {
if ($scope.canEdit) {
$scope.saveQuery();
}
},
// Cmd+Enter for Mac
'meta+enter': function () {
$scope.executeQuery();
},
// Ctrl+Enter for PC
'ctrl+enter': function () {
$scope.executeQuery();
}
};
saveQuery = $scope.saveQuery;
$scope.sourceMode = true;
$scope.canEdit = currentUser.canEdit($scope.query);
@@ -49,6 +29,22 @@
}
});
var shortcuts = {
'meta+s': function () {
if ($scope.canEdit) {
$scope.saveQuery();
}
},
'ctrl+s': function () {
if ($scope.canEdit) {
$scope.saveQuery();
}
},
// Cmd+Enter for Mac
'meta+enter': $scope.executeQuery,
// Ctrl+Enter for PC
'ctrl+enter': $scope.executeQuery
};
KeyboardShortcuts.bind(shortcuts);

View File

@@ -4,9 +4,18 @@
function QueryViewCtrl($scope, Events, $route, $location, notifications, growl, Query, DataSource) {
var DEFAULT_TAB = 'table';
var getQueryResult = function(ttl) {
// Collect params, and getQueryResult with params; getQueryResult merges it into the query
var parameters = Query.collectParamsFromQueryString($location, $scope.query);
if (ttl == undefined) {
ttl = $location.search()['maxAge'];
}
$scope.queryResult = $scope.query.getQueryResult(ttl, parameters);
}
$scope.query = $route.current.locals.query;
Events.record(currentUser, 'view', 'query', $scope.query.id);
$scope.queryResult = $scope.query.getQueryResult();
getQueryResult();
$scope.queryExecuting = false;
$scope.isQueryOwner = currentUser.id === $scope.query.user.id;
@@ -57,7 +66,7 @@
};
$scope.executeQuery = function() {
$scope.queryResult = $scope.query.getQueryResult(0);
getQueryResult(0);
$scope.lockButton(true);
$scope.cancelling = false;
Events.record(currentUser, 'execute', 'query', $scope.query.id);

View File

@@ -153,6 +153,24 @@
this.redraw();
}
},
{
text: 'Save Image',
onclick: function () {
var canvas = document.createElement('canvas');
window.canvg(canvas, this.getSVG());
var href = canvas.toDataURL('image/png');
var a = document.createElement('a');
a.href = href;
var filenameSuffix = new Date().toISOString().replace(/:/g,'_').replace('Z', '');
if (this.title) {
filenameSuffix = this.title.text;
}
a.download = 'redash_charts_'+filenameSuffix+'.png';
document.body.appendChild(a);
a.click();
a.remove();
}
}
]
}
@@ -287,8 +305,28 @@
scope.chart.series[0].remove(false);
};
// We check either for true or undefined for backward compatibility.
var series = scope.series;
if (chartOptions['sortX'] === true || chartOptions['sortX'] === undefined) {
var seriesCopy = [];
_.each(series, function (s) {
// make a copy of series data, so we don't override original.
var fieldName = 'x';
if (s.data.length > 0 && _.has(s.data[0], 'name')) {
fieldName = 'name';
};
var sorted = _.extend({}, s, {data: _.sortBy(s.data, fieldName)});
seriesCopy.push(sorted);
});
series = seriesCopy;
}
if (!('xAxis' in chartOptions && 'type' in chartOptions['xAxis'])) {
if (scope.series.length > 0 && _.some(scope.series[0].data, function (p) {
if (series.length > 0 && _.some(series[0].data, function (p) {
return (angular.isString(p.x) || angular.isDefined(p.name));
})) {
chartOptions['xAxis'] = chartOptions['xAxis'] || {};
@@ -300,13 +338,13 @@
}
if (chartOptions['xAxis']['type'] == 'category' || chartOptions['series']['type']=='pie') {
if (!angular.isDefined(scope.series[0].data[0].name)) {
if (!angular.isDefined(series[0].data[0].name)) {
// We need to make sure that for each category, each series has a value.
var categories = _.union.apply(this, _.map(scope.series, function (s) {
var categories = _.union.apply(this, _.map(series, function (s) {
return _.pluck(s.data, 'x')
}));
_.each(scope.series, function (s) {
_.each(series, function (s) {
// TODO: move this logic to Query#getChartData
var yValues = _.groupBy(s.data, 'x');
@@ -317,11 +355,6 @@
}
});
if (categories.length == 1) {
newData = _.sortBy(newData, 'y').reverse();
}
;
s.data = newData;
});
}
@@ -329,7 +362,7 @@
scope.chart.counters.color = 0;
_.each(scope.series, function (s) {
_.each(series, function (s) {
// here we override the series with the visualization config
s = _.extend(s, chartOptions['series']);

View File

@@ -214,10 +214,6 @@
}
});
_.each(series, function (series) {
series.data = _.sortBy(series.data, 'x');
});
return _.values(series);
};
@@ -399,15 +395,54 @@
});
};
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';
};
Query.prototype.getQueryResult = function (ttl) {
Query.prototype.getQueryResult = function (ttl, parameters) {
if (ttl == undefined) {
ttl = this.ttl;
}
var queryText = this.query;
var queryParameters = this.getParameters();
var paramsRequired = !_.isEmpty(queryParameters);
var missingParams = parameters === undefined ? queryParameters : _.difference(queryParameters, _.keys(parameters));
if (paramsRequired && missingParams.length > 0) {
var paramsWord = "parameter";
if (missingParams.length > 1) {
paramsWord = "parameters";
}
return new QueryResult({job: {error: "Missing values for " + missingParams.join(', ') + " "+paramsWord+".", status: 4}});
}
if (paramsRequired) {
queryText = Mustache.render(queryText, parameters);
// Need to clear latest results, to make sure we don't used results for different params.
this.latest_query_data = null;
this.latest_query_data_id = null;
}
if (this.latest_query_data && ttl != 0) {
if (!this.queryResult) {
this.queryResult = new QueryResult({'query_result': this.latest_query_data});
@@ -417,7 +452,7 @@
this.queryResult = QueryResult.getById(this.latest_query_data_id);
}
} else if (this.data_source_id) {
this.queryResult = QueryResult.get(this.data_source_id, this.query, ttl);
this.queryResult = QueryResult.get(this.data_source_id, queryText, ttl);
}
return this.queryResult;
@@ -425,6 +460,26 @@
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]));
}
});
return parameters;
};
parameters = collectParams(parts);
return parameters;
}
return Query;

View File

@@ -41,6 +41,9 @@
_.each($scope.queryResult.getChartData($scope.options.columnMapping), function (s) {
var additional = {'stacking': 'normal'};
if ('globalSeriesType' in $scope.options) {
additional['type'] = $scope.options.globalSeriesType;
}
if ($scope.options.seriesOptions && $scope.options.seriesOptions[s.name]) {
additional = $scope.options.seriesOptions[s.name];
if (!additional.name || additional.name == "") {
@@ -89,7 +92,7 @@
'Pie': 'pie'
};
scope.globalSeriesType = 'column';
scope.globalSeriesType = scope.visualization.options.globalSeriesType || 'column';
scope.stackingOptions = {
"None": "none",
@@ -125,12 +128,15 @@
columnsWatch = null;
scope.$watch('globalSeriesType', function(type, old) {
scope.visualization.options.globalSeriesType = type;
if (type && old && type !== old && scope.visualization.options.seriesOptions) {
_.each(scope.visualization.options.seriesOptions, function(sOptions) {
sOptions.type = type;
});
}
});
scope.$watch('visualization.type', function (visualizationType) {
if (visualizationType == 'CHART') {
if (scope.visualization.options.series.stacking === null) {
@@ -141,6 +147,10 @@
scope.stacking = scope.visualization.options.series.stacking;
}
if (scope.visualization.options.sortX === undefined) {
scope.visualization.options.sortX = true;
}
var refreshSeries = function() {
scope.series = _.map(scope.queryResult.getChartData(scope.visualization.options.columnMapping), function (s) { return s.name; });
@@ -153,9 +163,9 @@
_.each(scope.series, function(s, i) {
if (scope.visualization.options.seriesOptions[s] == undefined) {
scope.visualization.options.seriesOptions[s] = {'type': 'column', 'yAxis': 0};
scope.visualization.options.seriesOptions[s] = {'type': scope.visualization.options.globalSeriesType, 'yAxis': 0};
}
scope.visualization.options.seriesOptions[s].zIndex = i;
scope.visualization.options.seriesOptions[s].zIndex = scope.visualization.options.seriesOptions[s].zIndex === undefined ? i : scope.visualization.options.seriesOptions[s].zIndex;
});
scope.zIndexes = _.range(scope.series.length);

View File

@@ -27,6 +27,33 @@
ng-model="globalSeriesType" class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">y Axis min</label>
<div class="col-sm-10">
<input name="yAxisMin" type="number" class="form-control"
ng-model="visualization.options.yAxis.min"
placeholder="Auto">
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">y Axis max</label>
<div class="col-sm-10">
<input name="yAxisMin" type="number" class="form-control"
ng-model="visualization.options.yAxis.max"
placeholder="Auto">
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">Sort X Values</label>
<div class="col-sm-10">
<input name="sortX" type="checkbox" class="form-control"
ng-model="visualization.options.sortX">
</div>
</div>
</div>
</div>

View File

@@ -27,7 +27,9 @@
"bucky": "~0.2.6",
"pace": "~0.5.1",
"angular-ui-select": "0.8.2",
"font-awesome": "~4.2.0"
"font-awesome": "~4.2.0",
"mustache": "~1.0.0",
"canvg": "gabelerner/canvg"
},
"devDependencies": {
"angular-mocks": "1.2.18",

View File

@@ -55,6 +55,7 @@ module.exports = function(config) {
'app/scripts/ui-bootstrap-tpls-0.5.0.min.js',
'app/bower_components/bucky/bucky.js',
'app/bower_components/pace/pace.js',
'app/bower_components/mustache/mustache.js',
'app/scripts/app.js',
'app/scripts/services/services.js',

View File

@@ -171,8 +171,10 @@ def refresh_queries():
def cleanup_tasks():
# in case of cold restart of the workers, there might be jobs that still have their "lock" object, but aren't really
# going to run. this job removes them.
lock_keys = redis_connection.keys("query_hash_job:*") # TODO: use set instead of keys command
if not lock_keys:
return
query_tasks = [QueryTask(job_id=j) for j in redis_connection.mget(lock_keys)]
logger.info("Found %d locks", len(query_tasks))