Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions rd_ui/app/scripts/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ angular.module('redash', [
$locationProvider.html5Mode(true);
growlProvider.globalTimeToLive(2000);

$routeProvider.when('/admin/queries/outdated', {
templateUrl: '/views/admin/outdated_queries.html',
controller: 'AdminOutdatedQueriesCtrl'
});
$routeProvider.when('/admin/queries/tasks', {
templateUrl: '/views/admin/tasks.html',
controller: 'AdminTasksCtrl'
});
$routeProvider.when('/dashboard/:dashboardSlug', {
templateUrl: '/views/dashboard.html',
controller: 'DashboardCtrl',
Expand Down
224 changes: 223 additions & 1 deletion rd_ui/app/scripts/controllers/admin_controllers.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,234 @@
$scope.status = data;
});

$timeout(refresh, 59 * 1000);
var timer = $timeout(refresh, 59 * 1000);

$scope.$on("$destroy", function () {
if (timer) {
$timeout.cancel(timer);
}
});
};

refresh();
};

var dateFormatter = function (value) {
if (!value) {
return "-";
}

return moment(value).format(clientConfig.dateTimeFormat);
};

var timestampFormatter = function(value) {
if (value) {
return dateFormatter(value * 1000.0);
}

return "-";
}

var AdminTasksCtrl = function ($scope, $location, Events, $http, $timeout, $filter) {
Events.record(currentUser, "view", "page", "admin/tasks");
$scope.$parent.pageTitle = "Running Queries";

$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: 50,
maxSize: 8,
};
$scope.selectedTab = 'in_progress';
$scope.tasks = {
'pending': [],
'in_progress': [],
'done': []
};

$scope.allGridColumns = [
{
label: 'Data Source ID',
map: 'data_source_id'
},
{
label: 'Username',
map: 'username'
},
{
'label': 'State',
'map': 'state',
"cellTemplate": '{{dataRow.state}} <span ng-if="dataRow.state == \'failed\'" popover="{{dataRow.error}}" popover-trigger="mouseenter" class="zmdi zmdi-help"></span>'
},
{
"label": "Query ID",
"map": "query_id"
},
{
label: 'Query Hash',
map: 'query_hash'
},
{
'label': 'Runtime',
'map': 'run_time',
'formatFunction': function (value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Created At',
'map': 'created_at',
'formatFunction': timestampFormatter
},
{
'label': 'Started At',
'map': 'started_at',
'formatFunction': timestampFormatter
},
{
'label': 'Updated At',
'map': 'updated_at',
'formatFunction': timestampFormatter
}
];

$scope.inProgressGridColumns = angular.copy($scope.allGridColumns);
$scope.inProgressGridColumns.push({
'label': '',
"cellTemplate": '<cancel-query-button query-id="dataRow.query_id" task-id="dataRow.task_id"></cancel-query-button>'
});

$scope.setTab = function(tab) {
$scope.selectedTab = tab;
$scope.showingTasks = $scope.tasks[tab];
if (tab == 'in_progress') {
$scope.gridColumns = $scope.inProgressGridColumns;
} else {
$scope.gridColumns = $scope.allGridColumns;
}
};

$scope.setTab($location.hash() || 'in_progress');

var refresh = function () {
$scope.refresh_time = moment().add('minutes', 1);
$http.get('/api/admin/queries/tasks').success(function (data) {
$scope.tasks = data;
$scope.showingTasks = $scope.tasks[$scope.selectedTab];
});

var timer = $timeout(refresh, 5 * 1000);

$scope.$on("$destroy", function () {
if (timer) {
$timeout.cancel(timer);
}
});
};

refresh();
};

var AdminOutdatedQueriesCtrl = function ($scope, Events, $http, $timeout, $filter) {
Events.record(currentUser, "view", "page", "admin/outdated_queries");
$scope.$parent.pageTitle = "Outdated Queries";

$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: 50,
maxSize: 8,
};

$scope.gridColumns = [
{
label: 'Data Source ID',
map: 'data_source_id'
},
{
"label": "Name",
"map": "name",
"cellTemplateUrl": "/views/queries_query_name_cell.html"
},
{
'label': 'Created By',
'map': 'user.name'
},
{
'label': 'Runtime',
'map': 'runtime',
'formatFunction': function (value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Last Executed At',
'map': 'retrieved_at',
'formatFunction': dateFormatter
},
{
'label': 'Created At',
'map': 'created_at',
'formatFunction': dateFormatter
},
{
'label': 'Update Schedule',
'map': 'schedule',
'formatFunction': function (value) {
return $filter('scheduleHumanize')(value);
}
}
];

var refresh = function () {
$scope.refresh_time = moment().add('minutes', 1);
$http.get('/api/admin/queries/outdated').success(function (data) {
$scope.queries = data.queries;
$scope.updatedAt = data.updated_at * 1000.0;
});

var timer = $timeout(refresh, 59 * 1000);

$scope.$on("$destroy", function () {
if (timer) {
$timeout.cancel(timer);
}
});
};

refresh();
};

var cancelQueryButton = function () {
return {
restrict: 'E',
scope: {
'queryId': '=',
'taskId': '='
},
transclude: true,
template: '<button class="btn btn-default" ng-disabled="inProgress" ng-click="cancelExecution()"><i class="zmdi zmdi-spinner zmdi-hc-spin" ng-if="inProgress"></i> Cancel</button>',
replace: true,
controller: ['$scope', '$http', 'Events', function ($scope, $http, Events) {
$scope.inProgress = false;

$scope.cancelExecution = function() {
$http.delete('api/jobs/' + $scope.taskId).success(function() {
});

var queryId = $scope.queryId;
if ($scope.queryId == 'adhoc') {
queryId = null;
}

Events.record(currentUser, 'cancel_execute', 'query', queryId, {'admin': true});
$scope.inProgress = true;
}
}]
}
};

angular.module('redash.admin_controllers', [])
.controller('AdminStatusCtrl', ['$scope', 'Events', '$http', '$timeout', AdminStatusCtrl])
.controller('AdminTasksCtrl', ['$scope', '$location', 'Events', '$http', '$timeout', '$filter', AdminTasksCtrl])
.controller('AdminOutdatedQueriesCtrl', ['$scope', 'Events', '$http', '$timeout', '$filter', AdminOutdatedQueriesCtrl])
.directive('cancelQueryButton', cancelQueryButton)
})();
2 changes: 1 addition & 1 deletion rd_ui/app/scripts/controllers/controllers.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
},
{
'label': 'Runtime',
'map': 'runtime',
'map': 'run_time',
'formatFunction': function (value) {
return $filter('durationHumanize')(value);
}
Expand Down
19 changes: 19 additions & 0 deletions rd_ui/app/views/admin/outdated_queries.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<page-header title="Admin">
</page-header>

<div class="container">
<div class="container bg-white p-5">
<ul class="tab-nav">
<li><a href="admin/status">System Status</a></li>
<li><a href="admin/queries/tasks">Queries Queue</a></li>
<li class="active"><a href="admin/queries/outdated">Outdated Queries</a></li>
</ul>

<smart-table rows="queries" columns="gridColumns"
config="gridConfig"
class="table table-condensed table-hover"></smart-table>
<div class="badge">
Last update: <span am-time-ago="updatedAt"></span>
</div>
</div>
</div>
23 changes: 23 additions & 0 deletions rd_ui/app/views/admin/tasks.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<page-header title="Admin">
</page-header>

<div class="container">
<div class="container bg-white p-5">
<ul class="tab-nav">
<li><a href="admin/status">System Status</a></li>
<li class="active"><a href="admin/queries/tasks">Queries Queue</a></li>
<li><a href="admin/queries/outdated">Outdated Queries</a></li>
</ul>

<ul class="tab-nav">
<rd-tab tab-id="in_progress" name="In Progress ({{tasks.in_progress.length}})" ng-click="setTab('in_progress')"></rd-tab>
<rd-tab tab-id="waiting" name="Waiting ({{tasks.waiting.length}})" ng-click="setTab('waiting')"></rd-tab>
<rd-tab tab-id="done" name="Done ({{tasks.done.length}})" ng-click="setTab('done')"></rd-tab>
</ul>

<smart-table rows="showingTasks" columns="gridColumns"
config="gridConfig"
class="table table-condensed table-hover"></smart-table>
</div>
</div>

8 changes: 5 additions & 3 deletions rd_ui/app/views/admin_status.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
</page-header>

<div class="container">
<div class="container bg-white">
<div class="container bg-white p-5">
<ul class="tab-nav">
<li class="active"><a>System Status</a></li>
<li class="active"><a href="admin/status">System Status</a></li>
<li><a href="admin/queries/tasks">Queries Queue</a></li>
<li><a href="admin/queries/outdated">Outdated Queries</a></li>
</ul>

<div class="p-5">
<div>

<ul class="list-group col-lg-4">
<li class="list-group-item active">General</li>
Expand Down
3 changes: 1 addition & 2 deletions redash/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ def ping():
@require_super_admin
def status_api():
status = get_status()

return jsonify(status)


Expand All @@ -40,6 +39,6 @@ def inject_variables():


def init_app(app):
from redash.handlers import embed, queries, static, authentication
from redash.handlers import embed, queries, static, authentication, admin
app.register_blueprint(routes)
api.init_app(app)
48 changes: 48 additions & 0 deletions redash/handlers/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import json
from flask import current_app
from flask_login import login_required

from redash import models, redis_connection
from redash.utils import json_dumps
from redash.handlers import routes
from redash.permissions import require_super_admin
from redash.tasks.queries import QueryTaskTracker


def json_response(response):
return current_app.response_class(json_dumps(response), mimetype='application/json')


@routes.route('/api/admin/queries/outdated', methods=['GET'])
@require_super_admin
@login_required
def outdated_queries():
manager_status = redis_connection.hgetall('redash:status')
query_ids = json.loads(manager_status.get('query_ids', '[]'))
if query_ids:
outdated_queries = models.Query.select(models.Query, models.QueryResult.retrieved_at, models.QueryResult.runtime) \
.join(models.QueryResult, join_type=models.peewee.JOIN_LEFT_OUTER) \
.where(models.Query.id << query_ids) \
.order_by(models.Query.created_at.desc())
else:
outdated_queries = []

return json_response(dict(queries=[q.to_dict(with_stats=True, with_last_modified_by=False) for q in outdated_queries], updated_at=manager_status['last_refresh_at']))


@routes.route('/api/admin/queries/tasks', methods=['GET'])
@require_super_admin
@login_required
def queries_tasks():
waiting = QueryTaskTracker.all(QueryTaskTracker.WAITING_LIST)
in_progress = QueryTaskTracker.all(QueryTaskTracker.IN_PROGRESS_LIST)
done = QueryTaskTracker.all(QueryTaskTracker.DONE_LIST, limit=50)

response = {
'waiting': [t.data for t in waiting],
'in_progress': [t.data for t in in_progress],
'done': [t.data for t in done]
}

return json_response(response)

Loading