mirror of
https://github.com/valitydev/redash.git
synced 2024-11-07 17:38:54 +00:00
Add server-side sorting to dashboard list. (#2760)
Fix #2771. Refs #2731.
This commit is contained in:
parent
bc15c0b6d1
commit
bfd128413c
@ -4,7 +4,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-3 list-control-t">
|
||||
<div class="m-b-5">
|
||||
<input type='text' class='form-control' placeholder="Search Dashboards..." ng-change="$ctrl.update()" ng-model="$ctrl.searchText"
|
||||
<input type='text' class='form-control' placeholder="Search Dashboards..." ng-change="$ctrl.update()" ng-model="$ctrl.searchText" ng-model-options="{ allowInvalid: true, debounce: 200 }"
|
||||
autofocus/>
|
||||
</div>
|
||||
|
||||
@ -50,9 +50,15 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 33px"></th>
|
||||
<th>Name</th>
|
||||
<th class="sortable-column" ng-click="$ctrl.paginator.orderBy('name')">
|
||||
Name
|
||||
<sort-icon column="'name'" sort-column="$ctrl.paginator.orderByField" reverse="$ctrl.paginator.orderByReverse"></sort-icon>
|
||||
</th>
|
||||
<th></th>
|
||||
<th>Created At</th>
|
||||
<th class="sortable-column" ng-click="$ctrl.paginator.orderBy('created_at')">
|
||||
Created At
|
||||
<sort-icon column="'created_at'" sort-column="$ctrl.paginator.orderByField" reverse="$ctrl.paginator.orderByReverse"></sort-icon>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -82,7 +88,7 @@
|
||||
|
||||
<div class="col-md-3 list-control-r-b">
|
||||
<div class="m-b-5">
|
||||
<input type='text' class='form-control' placeholder="Search Dashboards..." ng-change="$ctrl.update()" ng-model="$ctrl.searchText"
|
||||
<input type='text' class='form-control' placeholder="Search Dashboards..." ng-change="$ctrl.update()" ng-model="$ctrl.searchText" ng-model-options="{ allowInvalid: true, debounce: 200 }"
|
||||
autofocus/>
|
||||
</div>
|
||||
|
||||
|
@ -4,66 +4,94 @@ import { LivePaginator } from '@/lib/pagination';
|
||||
import template from './dashboard-list.html';
|
||||
import './dashboard-list.css';
|
||||
|
||||
function DashboardListCtrl($scope, currentUser, $location, Dashboard) {
|
||||
const page = parseInt($location.search().page || 1, 10);
|
||||
class DashboardListCtrl {
|
||||
constructor($scope, currentUser, $location, Dashboard) {
|
||||
const page = parseInt($location.search().page || 1, 10);
|
||||
|
||||
// use $parent because we're using a component as route target instead of controller;
|
||||
// $parent refers to scope created for the page by router
|
||||
this.resource = $scope.$parent.$resolve.resource;
|
||||
this.currentPage = $scope.$parent.$resolve.currentPage;
|
||||
|
||||
this.defaultOptions = {};
|
||||
|
||||
this.searchText = $location.search().q;
|
||||
|
||||
this.currentUser = currentUser;
|
||||
|
||||
this.selectedTags = new Set();
|
||||
this.onTagsUpdate = (tags) => {
|
||||
this.selectedTags = tags;
|
||||
this.update();
|
||||
};
|
||||
|
||||
this.showEmptyState = false;
|
||||
this.loaded = false;
|
||||
|
||||
const fetcher = (requestedPage, itemsPerPage, orderByField, orderByReverse, paginator) => {
|
||||
$location.search('page', requestedPage);
|
||||
|
||||
const request = Object.assign({}, this.defaultOptions, {
|
||||
page: requestedPage,
|
||||
page_size: itemsPerPage,
|
||||
tags: [...this.selectedTags], // convert Set to Array
|
||||
});
|
||||
|
||||
if (isString(this.searchText) && this.searchText !== '') {
|
||||
request.q = this.searchText;
|
||||
const orderSeparator = '-';
|
||||
this.pageOrder = $location.search().order || '-created_at';
|
||||
this.pageOrderReverse = this.pageOrder.startsWith(orderSeparator);
|
||||
if (this.pageOrderReverse) {
|
||||
this.pageOrder = this.pageOrder.substr(1);
|
||||
}
|
||||
|
||||
// use $parent because we're using a component as route target instead of controller;
|
||||
// $parent refers to scope created for the page by router
|
||||
this.resource = $scope.$parent.$resolve.resource;
|
||||
this.currentPage = $scope.$parent.$resolve.currentPage;
|
||||
|
||||
this.defaultOptions = {};
|
||||
|
||||
this.searchText = $location.search().q;
|
||||
|
||||
this.currentUser = currentUser;
|
||||
|
||||
this.selectedTags = new Set();
|
||||
this.onTagsUpdate = (tags) => {
|
||||
this.selectedTags = tags;
|
||||
this.update();
|
||||
};
|
||||
|
||||
this.showEmptyState = false;
|
||||
this.loaded = false;
|
||||
return this.resource(request).$promise.then((data) => {
|
||||
this.loaded = true;
|
||||
const rows = data.results.map(d => new Dashboard(d));
|
||||
paginator.updateRows(rows, data.count);
|
||||
this.showEmptyState = data.count === 0;
|
||||
|
||||
const setSearchOrClear = (name, value) => {
|
||||
if (value) {
|
||||
$location.search(name, value);
|
||||
} else {
|
||||
$location.search(name, undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const fetcher = (requestedPage, itemsPerPage, orderByField, orderByReverse, paginator) => {
|
||||
$location.search('page', requestedPage);
|
||||
|
||||
if (orderByReverse && !orderByField.startsWith(orderSeparator)) {
|
||||
orderByField = orderSeparator + orderByField;
|
||||
}
|
||||
setSearchOrClear('order', orderByField);
|
||||
|
||||
const request = Object.assign({}, this.defaultOptions, {
|
||||
page: requestedPage,
|
||||
page_size: itemsPerPage,
|
||||
tags: [...this.selectedTags], // convert Set to Array
|
||||
order: orderByField,
|
||||
});
|
||||
|
||||
if (isString(this.searchText) && this.searchText !== '') {
|
||||
request.q = this.searchText;
|
||||
}
|
||||
|
||||
this.loaded = false;
|
||||
return this.resource(request).$promise.then((data) => {
|
||||
this.loaded = true;
|
||||
const rows = data.results.map(d => new Dashboard(d));
|
||||
paginator.updateRows(rows, data.count);
|
||||
this.showEmptyState = data.count === 0;
|
||||
});
|
||||
};
|
||||
|
||||
this.paginator = new LivePaginator(fetcher, {
|
||||
page,
|
||||
itemsPerPage: this.pageSize,
|
||||
orderByField: this.pageOrder,
|
||||
orderByReverse: this.pageOrderReverse,
|
||||
});
|
||||
};
|
||||
|
||||
this.paginator = new LivePaginator(fetcher, { page });
|
||||
this.navigateTo = ($event, url) => {
|
||||
if ($event.altKey || $event.ctrlKey || $event.metaKey || $event.shiftKey) {
|
||||
// keep default browser behavior
|
||||
return;
|
||||
}
|
||||
$event.preventDefault();
|
||||
$location.url(url);
|
||||
};
|
||||
|
||||
this.navigateTo = ($event, url) => {
|
||||
if ($event.altKey || $event.ctrlKey || $event.metaKey || $event.shiftKey) {
|
||||
// keep default browser behavior
|
||||
return;
|
||||
}
|
||||
$event.preventDefault();
|
||||
$location.url(url);
|
||||
};
|
||||
|
||||
this.update = () => {
|
||||
// trigger paginator refresh
|
||||
this.paginator.setPage(1);
|
||||
};
|
||||
this.update = () => {
|
||||
// trigger paginator refresh
|
||||
this.paginator.setPage(page);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
|
@ -1,11 +1,11 @@
|
||||
from itertools import chain
|
||||
|
||||
from flask import request, url_for
|
||||
from funcy import distinct, project, take
|
||||
from funcy import project, rpartial
|
||||
|
||||
from flask_restful import abort
|
||||
from redash import models, serializers, settings
|
||||
from redash.handlers.base import BaseResource, get_object_or_404, paginate, filter_by_tags
|
||||
from redash import models, serializers
|
||||
from redash.handlers.base import (BaseResource, get_object_or_404, paginate,
|
||||
filter_by_tags,
|
||||
order_results as _order_results)
|
||||
from redash.serializers import serialize_dashboard
|
||||
from redash.permissions import (can_modify, require_admin_or_owner,
|
||||
require_object_modify_permission,
|
||||
@ -13,24 +13,61 @@ from redash.permissions import (can_modify, require_admin_or_owner,
|
||||
from sqlalchemy.orm.exc import StaleDataError
|
||||
|
||||
|
||||
# Ordering map for relationships
|
||||
order_map = {
|
||||
'name': 'lowercase_name',
|
||||
'-name': '-lowercase_name',
|
||||
'created_at': 'created_at',
|
||||
'-created_at': '-created_at',
|
||||
}
|
||||
|
||||
order_results = rpartial(_order_results, '-created_at', order_map)
|
||||
|
||||
|
||||
class DashboardListResource(BaseResource):
|
||||
@require_permission('list_dashboards')
|
||||
def get(self):
|
||||
"""
|
||||
Lists all accessible dashboards.
|
||||
|
||||
:qparam number page_size: Number of queries to return per page
|
||||
:qparam number page: Page number to retrieve
|
||||
:qparam number order: Name of column to order by
|
||||
:qparam number q: Full text search term
|
||||
|
||||
Responds with an array of :ref:`dashboard <dashboard-response-label>`
|
||||
objects.
|
||||
"""
|
||||
search_term = request.args.get('q')
|
||||
|
||||
if search_term:
|
||||
results = models.Dashboard.search(self.current_org, self.current_user.group_ids, self.current_user.id, search_term)
|
||||
results = models.Dashboard.search(
|
||||
self.current_org,
|
||||
self.current_user.group_ids,
|
||||
self.current_user.id,
|
||||
search_term,
|
||||
)
|
||||
else:
|
||||
results = models.Dashboard.all(self.current_org, self.current_user.group_ids, self.current_user.id)
|
||||
results = models.Dashboard.all(
|
||||
self.current_org,
|
||||
self.current_user.group_ids,
|
||||
self.current_user.id,
|
||||
)
|
||||
|
||||
results = filter_by_tags(results, models.Dashboard.tags)
|
||||
|
||||
# order results according to passed order parameter
|
||||
ordered_results = order_results(results)
|
||||
|
||||
page = request.args.get('page', 1, type=int)
|
||||
page_size = request.args.get('page_size', 25, type=int)
|
||||
response = paginate(results, page, page_size, serialize_dashboard)
|
||||
|
||||
response = paginate(
|
||||
ordered_results,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
serializer=serialize_dashboard,
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
@ -1405,6 +1405,16 @@ class Dashboard(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model
|
||||
def get_by_slug_and_org(cls, slug, org):
|
||||
return cls.query.filter(cls.slug == slug, cls.org == org).one()
|
||||
|
||||
@hybrid_property
|
||||
def lowercase_name(self):
|
||||
"Optional property useful for sorting purposes."
|
||||
return self.name.lower()
|
||||
|
||||
@lowercase_name.expression
|
||||
def lowercase_name(cls):
|
||||
"The SQLAlchemy expression for the property above."
|
||||
return func.lower(cls.name)
|
||||
|
||||
def __unicode__(self):
|
||||
return u"%s=%s" % (self.id, self.name)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user