Compare commits

...

21 Commits

Author SHA1 Message Date
Arik Fraimovich
e8c946b88b Merge pull request #205 from joeysim/keyboard-shortcut
added support for cmd+enter execution
2014-05-08 10:57:30 +03:00
Joey Simhon
7b94260135 added support for cmd+enter execution 2014-05-07 22:45:39 +03:00
Arik Fraimovich
e7c6ba8c1d Merge pull request #204 from EverythingMe/performance
Add Bucky (client side metrics client).
2014-05-07 17:28:19 +03:00
Arik Fraimovich
509edf651b Add bucky (client side metrics client). 2014-05-07 17:25:43 +03:00
Arik Fraimovich
05c915cf00 Fix indendentation 2014-05-07 15:48:29 +03:00
Arik Fraimovich
0fa22500be Merge pull request #203 from EverythingMe/performance
Report to statsd request render time
2014-05-07 15:15:56 +03:00
Arik Fraimovich
4d4f41733d Report to statsd request render time 2014-05-07 15:13:29 +03:00
Arik Fraimovich
37bf79c9eb Merge pull request #201 from EverythingMe/feature_dashboard_filters
Use column type data (if available) to properly render data table.
2014-05-05 19:48:05 +03:00
Arik Fraimovich
38293fc155 Fix: query save fails if query has queryResult property. 2014-05-05 19:45:07 +03:00
Arik Fraimovich
52f44588e6 Use column type (if available) to better render tables. 2014-05-05 19:44:52 +03:00
Arik Fraimovich
0ffda9d002 Populate the column type field. 2014-05-05 19:44:28 +03:00
Arik Fraimovich
e7331633a4 Fix indentation. 2014-05-05 19:32:17 +03:00
Arik Fraimovich
19743f387b Merge pull request #200 from EverythingMe/feature_dashboard_filters
Feature: dashboard filters
2014-05-05 18:49:52 +03:00
Arik Fraimovich
77d628d2db Support for dashboard filters in the UI. 2014-05-05 18:46:38 +03:00
Arik Fraimovich
bcce69904d Global filters flag for dashboard. 2014-05-05 18:42:49 +03:00
Arik Fraimovich
7b4c04024c Use new getQuery accessor. 2014-05-05 18:36:12 +03:00
Arik Fraimovich
a40da45b1e Show filters in dashboards (if available). 2014-05-05 18:35:07 +03:00
Arik Fraimovich
638fb123ec Query: cache QueryResult so each call gets the same one. 2014-05-05 18:34:54 +03:00
Arik Fraimovich
f95a09a015 Widget: accessor function to get Query object. 2014-05-05 18:34:29 +03:00
Arik Fraimovich
b74f4639a0 Merge pull request #199 from EverythingMe/feature_dashboard_filters
Fix: set was messing up column order
2014-05-04 16:02:53 +03:00
Arik Fraimovich
a7b10db3f4 Fix: set was messing up column order 2014-05-04 16:00:57 +03:00
15 changed files with 445 additions and 297 deletions

View File

@@ -0,0 +1,12 @@
from playhouse.migrate import Migrator
from redash import db
from redash import models
if __name__ == '__main__':
db.connect_db()
migrator = Migrator(db.database)
with db.database.transaction():
migrator.add_column(models.Dashboard, models.Dashboard.dashboard_filters_enabled, 'dashboard_filters_enabled')
db.close_db(None)

View File

@@ -114,6 +114,7 @@
<script src="/scripts/ng_highchart.js"></script>
<script src="/scripts/ng_smart_table.js"></script>
<script src="/scripts/ui-bootstrap-tpls-0.5.0.min.js"></script>
<script src="/bower_components/bucky/bucky.js"></script>
<!-- endbuild -->
<!-- build:js({.tmp,app}) /scripts/scripts.js -->
@@ -139,6 +140,8 @@
<!-- endbuild -->
<script>
// TODO: move currentUser & features to be an Angular service
var featureFlags = {{ features|safe }};
var currentUser = {{ user|safe }};
currentUser.canEdit = function(object) {

View File

@@ -15,65 +15,73 @@ angular.module('redash', [
'smartTable.table',
'ngResource',
'ngRoute'
]).config(['$routeProvider', '$locationProvider', '$compileProvider', 'growlProvider',
function($routeProvider, $locationProvider, $compileProvider, growlProvider) {
]).config(['$routeProvider', '$locationProvider', '$compileProvider', 'growlProvider',
function ($routeProvider, $locationProvider, $compileProvider, growlProvider) {
if (featureFlags.clientSideMetrics) {
Bucky.setOptions({
host: '/api/metrics'
});
function getQuery(Query, $route) {
var query = Query.get({'id': $route.current.params.queryId });
return query.$promise;
};
Bucky.requests.monitor('ajax_requsts');
Bucky.requests.transforms.enable('dashboards', /dashboard\/[\w-]+/ig, '/dashboard');
}
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|http|data):/);
$locationProvider.html5Mode(true);
growlProvider.globalTimeToLive(2000);
function getQuery(Query, $route) {
var query = Query.get({'id': $route.current.params.queryId });
return query.$promise;
};
$routeProvider.when('/dashboard/:dashboardSlug', {
templateUrl: '/views/dashboard.html',
controller: 'DashboardCtrl'
});
$routeProvider.when('/queries', {
templateUrl: '/views/queries.html',
controller: 'QueriesCtrl',
reloadOnSearch: false
});
$routeProvider.when('/queries/new', {
templateUrl: '/views/query.html',
controller: 'QuerySourceCtrl',
reloadOnSearch: false,
resolve: {
'query': ['Query', function newQuery(Query) {
return Query.newQuery();
}]
}
});
$routeProvider.when('/queries/:queryId', {
templateUrl: '/views/query.html',
controller: 'QueryViewCtrl',
reloadOnSearch: false,
resolve: {
'query': ['Query', '$route', getQuery]
}
});
$routeProvider.when('/queries/:queryId/source', {
templateUrl: '/views/query.html',
controller: 'QuerySourceCtrl',
reloadOnSearch: false,
resolve: {
'query': ['Query', '$route', getQuery]
}
});
$routeProvider.when('/admin/status', {
templateUrl: '/views/admin_status.html',
controller: 'AdminStatusCtrl'
});
$routeProvider.when('/', {
templateUrl: '/views/index.html',
controller: 'IndexCtrl'
});
$routeProvider.otherwise({
redirectTo: '/'
});
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|http|data):/);
$locationProvider.html5Mode(true);
growlProvider.globalTimeToLive(2000);
$routeProvider.when('/dashboard/:dashboardSlug', {
templateUrl: '/views/dashboard.html',
controller: 'DashboardCtrl'
});
$routeProvider.when('/queries', {
templateUrl: '/views/queries.html',
controller: 'QueriesCtrl',
reloadOnSearch: false
});
$routeProvider.when('/queries/new', {
templateUrl: '/views/query.html',
controller: 'QuerySourceCtrl',
reloadOnSearch: false,
resolve: {
'query': ['Query', function newQuery(Query) {
return Query.newQuery();
}]
}
});
$routeProvider.when('/queries/:queryId', {
templateUrl: '/views/query.html',
controller: 'QueryViewCtrl',
reloadOnSearch: false,
resolve: {
'query': ['Query', '$route', getQuery]
}
});
$routeProvider.when('/queries/:queryId/source', {
templateUrl: '/views/query.html',
controller: 'QuerySourceCtrl',
reloadOnSearch: false,
resolve: {
'query': ['Query', '$route', getQuery]
}
});
$routeProvider.when('/admin/status', {
templateUrl: '/views/admin_status.html',
controller: 'AdminStatusCtrl'
});
$routeProvider.when('/', {
templateUrl: '/views/index.html',
controller: 'IndexCtrl'
});
$routeProvider.otherwise({
redirectTo: '/'
});
}
]);
]);

View File

@@ -1,157 +1,169 @@
(function () {
var QueriesCtrl = function($scope, $http, $location, $filter, Query) {
$scope.$parent.pageTitle = "All Queries";
$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: 50,
maxSize: 8,
isGlobalSearchActivated: true
}
$scope.allQueries = [];
$scope.queries = [];
var dateFormatter = function (value) {
if (!value) return "-";
return value.format("DD/MM/YY HH:mm");
}
var filterQueries = function() {
$scope.queries = _.filter($scope.allQueries, function(query) {
if (!$scope.selectedTab) {
return false;
}
if ($scope.selectedTab.key == 'my') {
return query.user.id == currentUser.id && query.name != 'New Query';
} else if ($scope.selectedTab.key == 'drafts') {
return query.user.id == currentUser.id && query.name == 'New Query';
}
return query.name != 'New Query';
});
}
Query.query(function(queries) {
$scope.allQueries = _.map(queries, function(query) {
query.created_at = moment(query.created_at);
query.last_retrieved_at = moment(query.last_retrieved_at);
return query;
});
filterQueries();
});
$scope.gridColumns = [
{
"label": "Name",
"map": "name",
"cellTemplateUrl": "/views/queries_query_name_cell.html"
},
{
'label': 'Created By',
'map': 'user.name'
},
{
'label': 'Created At',
'map': 'created_at',
'formatFunction': dateFormatter
},
{
'label': 'Runtime (avg)',
'map': 'avg_runtime',
'formatFunction': function(value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Runtime (min)',
'map': 'min_runtime',
'formatFunction': function(value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Runtime (max)',
'map': 'max_runtime',
'formatFunction': function(value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Last Executed At',
'map': 'last_retrieved_at',
'formatFunction': dateFormatter
},
{
'label': 'Times Executed',
'map': 'times_retrieved'
},
{
'label': 'Update Schedule',
'map': 'ttl',
'formatFunction': function(value) {
return $filter('refreshRateHumanize')(value);
}
}
]
$scope.tabs = [{"name": "My Queries", "key": "my"}, {"key": "all", "name": "All Queries"}, {"key": "drafts", "name": "Drafts"}];
$scope.$watch('selectedTab', function(tab) {
if (tab) {
$scope.$parent.pageTitle = tab.name;
}
filterQueries();
});
var QueriesCtrl = function ($scope, $http, $location, $filter, Query) {
$scope.$parent.pageTitle = "All Queries";
$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: 50,
maxSize: 8,
isGlobalSearchActivated: true
}
var MainCtrl = function ($scope, Dashboard, notifications) {
$scope.dashboards = [];
$scope.reloadDashboards = function() {
Dashboard.query(function (dashboards) {
$scope.dashboards = _.sortBy(dashboards, "name");
$scope.allDashboards = _.groupBy($scope.dashboards, function(d) {
parts = d.name.split(":");
if (parts.length == 1) {
return "Other";
}
return parts[0];
});
$scope.otherDashboards = $scope.allDashboards['Other'] || [];
$scope.groupedDashboards = _.omit($scope.allDashboards, 'Other');
});
$scope.allQueries = [];
$scope.queries = [];
var dateFormatter = function (value) {
if (!value) return "-";
return value.format("DD/MM/YY HH:mm");
}
var filterQueries = function () {
$scope.queries = _.filter($scope.allQueries, function (query) {
if (!$scope.selectedTab) {
return false;
}
$scope.reloadDashboards();
$scope.currentUser = currentUser;
$scope.newDashboard = {
'name': null,
'layout': null
if ($scope.selectedTab.key == 'my') {
return query.user.id == currentUser.id && query.name != 'New Query';
} else if ($scope.selectedTab.key == 'drafts') {
return query.user.id == currentUser.id && query.name == 'New Query';
}
$(window).click(function () {
notifications.getPermissions();
return query.name != 'New Query';
});
}
Query.query(function (queries) {
$scope.allQueries = _.map(queries, function (query) {
query.created_at = moment(query.created_at);
query.last_retrieved_at = moment(query.last_retrieved_at);
return query;
});
filterQueries();
});
$scope.gridColumns = [
{
"label": "Name",
"map": "name",
"cellTemplateUrl": "/views/queries_query_name_cell.html"
},
{
'label': 'Created By',
'map': 'user.name'
},
{
'label': 'Created At',
'map': 'created_at',
'formatFunction': dateFormatter
},
{
'label': 'Runtime (avg)',
'map': 'avg_runtime',
'formatFunction': function (value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Runtime (min)',
'map': 'min_runtime',
'formatFunction': function (value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Runtime (max)',
'map': 'max_runtime',
'formatFunction': function (value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Last Executed At',
'map': 'last_retrieved_at',
'formatFunction': dateFormatter
},
{
'label': 'Times Executed',
'map': 'times_retrieved'
},
{
'label': 'Update Schedule',
'map': 'ttl',
'formatFunction': function (value) {
return $filter('refreshRateHumanize')(value);
}
}
]
$scope.tabs = [
{"name": "My Queries", "key": "my"},
{"key": "all", "name": "All Queries"},
{"key": "drafts", "name": "Drafts"}
];
$scope.$watch('selectedTab', function (tab) {
if (tab) {
$scope.$parent.pageTitle = tab.name;
}
filterQueries();
});
}
var MainCtrl = function ($scope, Dashboard, notifications) {
if (featureFlags.clientSideMetrics) {
$scope.$on('$locationChangeSuccess', function(event, newLocation, oldLocation) {
// This will be called once per actual page load.
Bucky.sendPagePerformance();
});
}
$scope.dashboards = [];
$scope.reloadDashboards = function () {
Dashboard.query(function (dashboards) {
$scope.dashboards = _.sortBy(dashboards, "name");
$scope.allDashboards = _.groupBy($scope.dashboards, function (d) {
parts = d.name.split(":");
if (parts.length == 1) {
return "Other";
}
return parts[0];
});
$scope.otherDashboards = $scope.allDashboards['Other'] || [];
$scope.groupedDashboards = _.omit($scope.allDashboards, 'Other');
});
}
var IndexCtrl = function($scope, Events, Dashboard) {
Events.record(currentUser, "view", "page", "homepage");
$scope.$parent.pageTitle = "Home";
$scope.reloadDashboards();
$scope.archiveDashboard = function(dashboard) {
if (confirm('Are you sure you want to delete "' + dashboard.name + '" dashboard?')) {
Events.record(currentUser, "archive", "dashboard", dashboard.id);
dashboard.$delete(function() {
$scope.$parent.reloadDashboards();
});
}
}
$scope.currentUser = currentUser;
$scope.newDashboard = {
'name': null,
'layout': null
}
angular.module('redash.controllers', [])
.controller('QueriesCtrl', ['$scope', '$http', '$location', '$filter', 'Query', QueriesCtrl])
.controller('IndexCtrl', ['$scope', 'Events', 'Dashboard', IndexCtrl])
.controller('MainCtrl', ['$scope', 'Dashboard', 'notifications', MainCtrl]);
$(window).click(function () {
notifications.getPermissions();
});
}
var IndexCtrl = function ($scope, Events, Dashboard) {
Events.record(currentUser, "view", "page", "homepage");
$scope.$parent.pageTitle = "Home";
$scope.archiveDashboard = function (dashboard) {
if (confirm('Are you sure you want to delete "' + dashboard.name + '" dashboard?')) {
Events.record(currentUser, "archive", "dashboard", dashboard.id);
dashboard.$delete(function () {
$scope.$parent.reloadDashboards();
});
}
}
}
angular.module('redash.controllers', [])
.controller('QueriesCtrl', ['$scope', '$http', '$location', '$filter', 'Query', QueriesCtrl])
.controller('IndexCtrl', ['$scope', 'Events', 'Dashboard', IndexCtrl])
.controller('MainCtrl', ['$scope', 'Dashboard', 'notifications', MainCtrl]);
})();

View File

@@ -6,11 +6,40 @@
$scope.refreshRate = 60;
$scope.dashboard = Dashboard.get({ slug: $routeParams.dashboardSlug }, function (dashboard) {
$scope.$parent.pageTitle = dashboard.name;
var filters = {};
$scope.dashboard.widgets = _.map($scope.dashboard.widgets, function (row) {
return _.map(row, function (widget) {
return new Widget(widget);
var w = new Widget(widget);
if (w.visualization && dashboard.dashboard_filters_enabled) {
var queryFilters = w.getQuery().getQueryResult().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.
filters[filter.name] = filter;
filters[filter.name].originFilters = [];
$scope.$watch(function() { return filter.current }, function (value) {
_.each(filter.originFilters, function(originFilter) {
originFilter.current = value;
})
});
};
// TODO: merge values.
filters[filter.name].originFilters.push(filter);
});
}
return w;
});
});
if (dashboard.dashboard_filters_enabled) {
$scope.filters = _.values(filters);
}
});
var autoRefresh = function() {
@@ -77,7 +106,7 @@
Events.record(currentUser, "view", "query", $scope.widget.visualization.query.id);
Events.record(currentUser, "view", "visualization", $scope.widget.visualization.id);
$scope.query = new Query($scope.widget.visualization.query);
$scope.query = $scope.widget.getQuery();
$scope.queryResult = $scope.query.getQueryResult();
$scope.nextUpdateTime = moment(new Date(($scope.query.updated_at + $scope.query.ttl + $scope.query.runtime + 300) * 1000)).fromNow();

View File

@@ -20,6 +20,9 @@
if ($scope.canEdit) {
$scope.saveQuery();
}
},
'meta+enter': function () {
$scope.executeQuery();
}
};

View File

@@ -33,6 +33,7 @@
}, options);
delete $scope.query.latest_query_data;
delete $scope.query.queryResult;
return Query.save(data, function() {
growl.addSuccessMessage(options.successMessage);

View File

@@ -197,15 +197,22 @@
QueryResult.prototype.getColumns = function () {
if (this.columns == undefined && this.query_result.data) {
this.columns = _.map(this.query_result.data.columns, function (v) {
this.columns = this.query_result.data.columns;
}
return this.columns;
}
QueryResult.prototype.getColumnNames = function () {
if (this.columnNames == undefined && this.query_result.data) {
this.columnNames = _.map(this.query_result.data.columns, function (v) {
return v.name;
});
}
return this.columns;
return this.columnNames;
}
QueryResult.prototype.getColumnNameWithoutType = function (column) {
var parts = column.split('::');
return parts[0];
@@ -239,13 +246,13 @@
}
QueryResult.prototype.getColumnCleanNames = function () {
return _.map(this.getColumns(), function (col) {
return _.map(this.getColumnNames(), function (col) {
return this.getColumnCleanName(col);
}, this);
}
QueryResult.prototype.getColumnFriendlyNames = function () {
return _.map(this.getColumns(), function (col) {
return _.map(this.getColumnNames(), function (col) {
return this.getColumnFriendlyName(col);
}, this);
}
@@ -261,7 +268,7 @@
QueryResult.prototype.prepareFilters = function () {
var filters = [];
var filterTypes = ['filter', 'multi-filter'];
_.each(this.getColumns(), function (col) {
_.each(this.getColumnNames(), function (col) {
var type = col.split('::')[1]
if (_.contains(filterTypes, type)) {
// filter found
@@ -357,7 +364,10 @@
var queryResult = null;
if (this.latest_query_data && ttl != 0) {
queryResult = new QueryResult({'query_result': this.latest_query_data});
if (!this.queryResult) {
this.queryResult = new QueryResult({'query_result': this.latest_query_data});
}
queryResult = this.queryResult;
} else if (this.latest_query_data_id && ttl != 0) {
queryResult = QueryResult.getById(this.latest_query_data_id);
} else if (this.data_source_id) {
@@ -376,9 +386,17 @@
return DataSourceResource;
}
var Widget = function ($resource) {
var Widget = function ($resource, Query) {
var WidgetResource = $resource('/api/widgets/:id', {id: '@id'});
WidgetResource.prototype.getQuery = function () {
if (!this.query && this.visualization) {
this.query = new Query(this.visualization.query);
}
return this.query;
};
WidgetResource.prototype.getName = function () {
if (this.visualization) {
return this.visualization.query.name + ' (' + this.visualization.name + ')';
@@ -393,5 +411,5 @@
.factory('QueryResult', ['$resource', '$timeout', QueryResult])
.factory('Query', ['$resource', 'QueryResult', 'DataSource', Query])
.factory('DataSource', ['$resource', DataSource])
.factory('Widget', ['$resource', Widget]);
.factory('Widget', ['$resource', 'Query', Widget]);
})();

View File

@@ -1,90 +1,106 @@
(function () {
var tableVisualization = angular.module('redash.visualization');
var tableVisualization = angular.module('redash.visualization');
tableVisualization.config(['VisualizationProvider', function(VisualizationProvider) {
VisualizationProvider.registerVisualization({
type: 'TABLE',
name: 'Table',
renderTemplate: '<grid-renderer options="visualization.options" query-result="queryResult"></grid-renderer>',
skipTypes: true
});
}]);
tableVisualization.config(['VisualizationProvider', function (VisualizationProvider) {
VisualizationProvider.registerVisualization({
type: 'TABLE',
name: 'Table',
renderTemplate: '<grid-renderer options="visualization.options" query-result="queryResult"></grid-renderer>',
skipTypes: true
});
}]);
tableVisualization.directive('gridRenderer', function () {
return {
restrict: 'E',
scope: {
queryResult: '=',
itemsPerPage: '='
},
templateUrl: "/views/grid_renderer.html",
replace: false,
controller: ['$scope', function ($scope) {
$scope.gridColumns = [];
$scope.gridData = [];
$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: $scope.itemsPerPage || 15,
maxSize: 8
};
tableVisualization.directive('gridRenderer', function () {
return {
restrict: 'E',
scope: {
queryResult: '=',
itemsPerPage: '='
},
templateUrl: "/views/grid_renderer.html",
replace: false,
controller: ['$scope', function ($scope) {
$scope.gridColumns = [];
$scope.gridData = [];
$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: $scope.itemsPerPage || 15,
maxSize: 8
};
$scope.$watch('queryResult && queryResult.getData()', function (data) {
if (!data) {
return;
}
$scope.$watch('queryResult && queryResult.getData()', function (data) {
if (!data) {
return;
}
if ($scope.queryResult.getData() == null) {
$scope.gridColumns = [];
$scope.gridData = [];
$scope.filters = [];
if ($scope.queryResult.getData() == null) {
$scope.gridColumns = [];
$scope.gridData = [];
$scope.filters = [];
} else {
$scope.filters = $scope.queryResult.getFilters();
var prepareGridData = function (data) {
var gridData = _.map(data, function (row) {
var newRow = {};
_.each(row, function (val, key) {
newRow[$scope.queryResult.getColumnCleanName(key)] = val;
})
return newRow;
});
return gridData;
};
$scope.gridData = prepareGridData($scope.queryResult.getData());
var columns = $scope.queryResult.getColumns();
$scope.gridColumns = _.map($scope.queryResult.getColumnCleanNames(), function (col, i) {
var columnDefinition = {
'label': $scope.queryResult.getColumnFriendlyNames()[i],
'map': col
};
var columnType = columns[i].type;
if (!columnType) {
var rawData = $scope.queryResult.getRawData();
if (rawData.length > 0) {
var exampleData = rawData[0][col];
if (angular.isNumber(exampleData)) {
columnType = 'float';
} else if (moment.isMoment(exampleData)) {
if (exampleData._i.match(/^\d{4}-\d{2}-\d{2}T/)) {
columnType = 'datetime';
} else {
$scope.filters = $scope.queryResult.getFilters();
var prepareGridData = function(data) {
var gridData = _.map(data, function (row) {
var newRow = {};
_.each(row, function (val, key) {
newRow[$scope.queryResult.getColumnCleanName(key)] = val;
})
return newRow;
});
return gridData;
};
$scope.gridData = prepareGridData($scope.queryResult.getData());
$scope.gridColumns = _.map($scope.queryResult.getColumnCleanNames(), function (col, i) {
var columnDefinition = {
'label': $scope.queryResult.getColumnFriendlyNames()[i],
'map': col
};
var rawData = $scope.queryResult.getRawData();
if (rawData.length > 0) {
var exampleData = rawData[0][col];
if (angular.isNumber(exampleData)) {
columnDefinition['formatFunction'] = 'number';
columnDefinition['formatParameter'] = 2;
} else if (moment.isMoment(exampleData)) {
columnDefinition['formatFunction'] = function(value) {
// TODO: this is very hackish way to determine if we need
// to show the value as a time or date only. Better solution
// is to complete #70 and use the information it returns.
if (value._i.match(/^\d{4}-\d{2}-\d{2}T/)) {
return value.format("DD/MM/YY HH:mm");
}
return value.format("DD/MM/YY");
}
}
}
return columnDefinition;
});
columnType = 'date';
}
});
}]
}
})
}
}
}
if (columnType === 'integer') {
columnDefinition.formatFunction = 'number';
columnDefinition.formatParameter = 0;
} else if (columnType === 'float') {
columnDefinition.formatFunction = 'number';
columnDefinition.formatParameter = 2;
} else if (columnType === 'date') {
columnDefinition.formatFunction = function (value) {
return value.format("DD/MM/YY");
};
} else if (columnType === 'datetime') {
columnDefinition.formatFunction = function (value) {
return value.format("DD/MM/YY HH:mm");
};
}
return columnDefinition;
});
}
});
}]
}
})
}());

View File

@@ -14,6 +14,7 @@
</button>
</span>
</h2>
<filters></filters>
</div>
<div class="container" id="dashboard">

View File

@@ -22,7 +22,8 @@
"mousetrap": "~1.4.6",
"angular-ui-select2": "~0.0.5",
"underscore.string": "~2.3.3",
"marked": "~0.3.2"
"marked": "~0.3.2",
"bucky": "~0.2.6"
},
"devDependencies": {
"angular-mocks": "~1.0.7",

View File

@@ -19,7 +19,7 @@ from flask_login import current_user, login_user, logout_user
import sqlparse
import events
from permissions import require_permission
from redash import settings, utils, __version__
from redash import settings, utils, __version__, statsd_client
from redash import data
from redash import app, auth, api, redis_connection, data_manager
@@ -51,7 +51,12 @@ def index(**kwargs):
'permissions': current_user.permissions
}
features = {
'clientSideMetrics': settings.CLIENT_SIDE_METRICS
}
return render_template("index.html", user=json.dumps(user), name=settings.NAME,
features=json.dumps(features),
analytics=settings.ANALYTICS)
@@ -129,6 +134,11 @@ class BaseResource(Resource):
def current_user(self):
return current_user._get_current_object()
def dispatch_request(self, *args, **kwargs):
with statsd_client.timer('requests.{}.{}'.format(request.endpoint, request.method.lower())):
response = super(BaseResource, self).dispatch_request(*args, **kwargs)
return response
class EventAPI(BaseResource):
def post(self):
@@ -140,6 +150,17 @@ class EventAPI(BaseResource):
api.add_resource(EventAPI, '/api/events', endpoint='events')
class MetricsAPI(BaseResource):
def post(self):
for stat_line in request.data.split():
stat, value = stat_line.split(':')
statsd_client._send_stat('client.{}'.format(stat), value, 1)
return "OK."
api.add_resource(MetricsAPI, '/api/metrics/v1/send', endpoint='metrics')
class DataSourceListAPI(BaseResource):
def get(self):
data_sources = [ds.to_dict() for ds in models.DataSource.select()]

View File

@@ -13,6 +13,24 @@ import psycopg2
from redash.utils import JSONEncoder
types_map = {
20: 'integer',
21: 'integer',
23: 'integer',
700: 'float',
1700: 'float',
701: 'float',
16: 'boolean',
1082: 'date',
1114: 'datetime',
1184: 'datetime',
1014: 'string',
1015: 'string',
1008: 'string',
1009: 'string',
2951: 'string'
}
def pg(connection_string):
def column_friendly_name(column_name):
@@ -43,7 +61,9 @@ def pg(connection_string):
cursor.execute(query)
wait(connection)
column_names = set()
# While set would be more efficient here, it sorts the data which is not what we want, but due to the small
# size of the data we can assume it's ok.
column_names = []
columns = []
duplicates_counter = 1
@@ -54,12 +74,12 @@ def pg(connection_string):
column_name = column_name + str(duplicates_counter)
duplicates_counter += 1
column_names.add(column_name)
column_names.append(column_name)
columns.append({
'name': column_name,
'friendly_name': column_friendly_name(column_name),
'type': None
'type': types_map.get(column.type_code, None)
})
rows = [dict(zip(column_names, row)) for row in cursor]

View File

@@ -248,6 +248,7 @@ class Dashboard(BaseModel):
user_email = peewee.CharField(max_length=360, null=True)
user = peewee.ForeignKeyField(User)
layout = peewee.TextField()
dashboard_filters_enabled = peewee.BooleanField(default=False)
is_archived = peewee.BooleanField(default=False, index=True)
created_at = peewee.DateTimeField(default=datetime.datetime.now)
@@ -292,6 +293,7 @@ class Dashboard(BaseModel):
'name': self.name,
'user_id': self._data['user'],
'layout': layout,
'dashboard_filters_enabled': self.dashboard_filters_enabled,
'widgets': widgets_layout
}

View File

@@ -68,4 +68,5 @@ COOKIE_SECRET = os.environ.get("REDASH_COOKIE_SECRET", "c292a0a3aa32397cdb050e23
LOG_LEVEL = os.environ.get("REDASH_LOG_LEVEL", "INFO")
EVENTS_LOG_PATH = os.environ.get("REDASH_EVENTS_LOG_PATH", "")
EVENTS_CONSOLE_OUTPUT = parse_boolean(os.environ.get("REDASH_EVENTS_CONSOLE_OUTPUT", "false"))
CLIENT_SIDE_METRICS = parse_boolean(os.environ.get("REDASH_CLIENT_SIDE_METRICS", "false"))
ANALYTICS = os.environ.get("REDASH_ANALYTICS", "")