diff --git a/frontend/app/assets/css/redash.css b/frontend/app/assets/css/redash.css
index 8a0af319..fc43a7d8 100644
--- a/frontend/app/assets/css/redash.css
+++ b/frontend/app/assets/css/redash.css
@@ -679,3 +679,14 @@ div.table-name:hover {
stroke-opacity: .2;
}
+/*Dashboard list view */
+.no-margin{
+ margin:0px;
+}
+.two-px-margin{
+ margin:2px;
+}
+
+.taglist{
+ margin-top:20px;
+}
\ No newline at end of file
diff --git a/frontend/app/components/app-header/index.js b/frontend/app/components/app-header/index.js
index 5ab17c63..dad367e6 100644
--- a/frontend/app/components/app-header/index.js
+++ b/frontend/app/components/app-header/index.js
@@ -13,7 +13,7 @@ function controller($scope, $location, currentUser, Dashboard) {
this.currentUser = currentUser;
this.reloadDashboards = () => {
- Dashboard.query((dashboards) => {
+ Dashboard.recent((dashboards) => {
this.dashboards = sortBy(dashboards, 'name');
this.allDashboards = groupBy(this.dashboards, (d) => {
const parts = d.name.split(':');
diff --git a/frontend/app/pages/dashboards/dashboard-list.html b/frontend/app/pages/dashboards/dashboard-list.html
new file mode 100644
index 00000000..fd1cf414
--- /dev/null
+++ b/frontend/app/pages/dashboards/dashboard-list.html
@@ -0,0 +1,30 @@
+
\ No newline at end of file
diff --git a/frontend/app/pages/dashboards/dashboard-list.js b/frontend/app/pages/dashboards/dashboard-list.js
new file mode 100644
index 00000000..429536ae
--- /dev/null
+++ b/frontend/app/pages/dashboards/dashboard-list.js
@@ -0,0 +1,96 @@
+import template from './dashboard-list.html';
+import {_} from 'underscore';
+
+function DashboardListCtrl($scope, Dashboard, $location, currentUser, clientConfig, NgTableParams) {
+ const self = this;
+
+ self.logoUrl = clientConfig.logoUrl;
+ const page = parseInt($location.search().page || 1, 10);
+ const count = 25;
+
+ this.defaultOptions = {};
+ self.dashboards = Dashboard.query({}); // shared promise
+
+ $scope.selectedTags = []; // in scope because it needs to be accessed inside a table refresh
+ $scope.searchText = "";
+
+ $scope.$watch(function(){
+ return $scope.searchText;
+ }, function(){this.defaultOptions.reload()})
+
+ this.tagIsSelected = (tag) => {
+ return $scope.selectedTags.indexOf(tag) > -1;
+ }
+
+ this.toggleTag = (tag) => {
+ if(this.tagIsSelected(tag)){
+ $scope.selectedTags = $scope.selectedTags.filter((e) => e!=tag);
+ }else{
+ $scope.selectedTags.push(tag);
+ }
+ this.tableParams.reload();
+ }
+
+ this.allTags = [];
+ self.dashboards.$promise.then((data) => {
+ const out = data.results.map((dashboard) => {
+ return dashboard.name.match(/(^\w+):|(#\w+)/ig);
+ });
+ this.allTags = _.unique(_.flatten(out)).filter((e) => e);
+ });
+
+ this.tableParams = new NgTableParams({ page, count }, {
+ getData(params) {
+ const options = params.url();
+ $location.search('page', options.page);
+
+ const request = {};
+
+ return self.dashboards.$promise.then((data) => {
+ params.total(data.count);
+ return data.results.map((dashboard) => {
+ dashboard.tags = dashboard.name.match(/(^\w+):|(#\w+)/ig);
+ dashboard.untagged_name = dashboard.name.replace(/(\w+):|(#\w+)/ig, '').trim();
+ return dashboard;
+ }).filter((value) => {
+ if($scope.selectedTags.length){
+ const value_tags = new Set(value.tags);
+ const tag_match = $scope.selectedTags;
+ const filtered_match = tag_match.filter(x => value_tags.has(x));
+ if(tag_match.length != filtered_match.length){
+ return false;
+ }
+ }
+ if($scope.searchText && $scope.searchText.length){
+ if(!value.untagged_name.toLowerCase().includes($scope.searchText)){
+ return false;
+ }
+ }
+ return true;
+ });
+ });
+ }
+ });
+
+ this.tabs = [
+ { name: 'All Dashboards', path: 'dashboards' },
+ ];
+
+ self.currentUser = currentUser;
+}
+
+export default function (ngModule) {
+ ngModule.component('pageDashboardList', {
+ template,
+ controller: DashboardListCtrl,
+ });
+
+ const route = {
+ template: '',
+ reloadOnSearch: false,
+ };
+
+ return {
+ '/dashboards': route,
+ };
+}
diff --git a/frontend/app/pages/dashboards/index.js b/frontend/app/pages/dashboards/index.js
index 6752c541..9c25c968 100644
--- a/frontend/app/pages/dashboards/index.js
+++ b/frontend/app/pages/dashboards/index.js
@@ -1,4 +1,5 @@
import dashboardPage from './dashboard';
+import dashboardList from './dashboard-list';
import widgetComponent from './widget';
import addWidgetDialog from './add-widget-dialog';
import registerEditDashboardDialog from './edit-dashboard-dialog';
@@ -7,5 +8,5 @@ export default function (ngModule) {
addWidgetDialog(ngModule);
widgetComponent(ngModule);
registerEditDashboardDialog(ngModule);
- return dashboardPage(ngModule);
-}
+ return Object.assign({}, dashboardPage(ngModule), dashboardList(ngModule));
+}
\ No newline at end of file
diff --git a/frontend/app/services/dashboard.js b/frontend/app/services/dashboard.js
index 57a24669..135be4a4 100644
--- a/frontend/app/services/dashboard.js
+++ b/frontend/app/services/dashboard.js
@@ -18,14 +18,17 @@ function Dashboard($resource, $http, currentUser, Widget) {
const resource = $resource('api/dashboards/:slug', { slug: '@slug' }, {
get: { method: 'GET', transformResponse: transform },
save: { method: 'POST', transformResponse: transform },
- query: { method: 'GET', isArray: true, transformResponse: transform },
+ query: { method: 'GET', isArray: false, transformResponse: transform },
recent: {
method: 'get',
isArray: true,
url: 'api/dashboards/recent',
transformResponse: transform,
- } });
-
+ },
+ dashboards: {
+ isArray: false,
+ }
+ });
resource.prototype.canEdit = () => currentUser.canEdit(this) || this.can_edit;
return resource;
diff --git a/redash/handlers/dashboards.py b/redash/handlers/dashboards.py
index 39109af6..b8574831 100644
--- a/redash/handlers/dashboards.py
+++ b/redash/handlers/dashboards.py
@@ -7,7 +7,7 @@ from itertools import chain
from redash import models
from redash.models import ConflictDetectedError
from redash.permissions import require_permission, require_admin_or_owner, require_object_modify_permission, can_modify
-from redash.handlers.base import BaseResource, get_object_or_404
+from redash.handlers.base import BaseResource, get_object_or_404, paginate
class RecentDashboardsResource(BaseResource):
@@ -25,8 +25,11 @@ class RecentDashboardsResource(BaseResource):
class DashboardListResource(BaseResource):
@require_permission('list_dashboards')
def get(self):
- dashboards = [d.to_dict() for d in models.Dashboard.all(self.current_org, self.current_user.groups, self.current_user)]
- return dashboards
+ results = models.Dashboard.all(self.current_org, self.current_user.groups, self.current_user)
+ page = request.args.get('page', 1, type=int)
+ page_size = request.args.get('page_size', 25, type=int)
+ dashboards = models.Dashboard.all(self.current_org, self.current_user.groups, self.current_user)
+ return paginate(results, page, page_size, lambda q: q.to_dict())
@require_permission('create_dashboard')
def post(self):