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 @@ +
+ +
+ +
+

Tags

+ + {{ tag }} + +
+
+
+ +
+ + + + + + +
+ + {{ dashboard.untagged_name }} + + {{ dashboard.created_at | dateTime }}
+
+
+
\ 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):