Merge pull request #93 from glyptodon/paginate-lists

GUAC-1099: Paginate list output in Guacamole UI
This commit is contained in:
James Muehlner
2015-02-26 22:52:25 -08:00
15 changed files with 541 additions and 113 deletions

View File

@@ -21,7 +21,8 @@
*/
/**
* A directive which displays the contents of a connection group.
* A directive which displays the contents of a connection group within an
* automatically-paginated view.
*/
angular.module('groupList').directive('guacGroupList', [function guacGroupList() {
@@ -75,7 +76,14 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList()
*
* @type Boolean
*/
showRootGroup : '='
showRootGroup : '=',
/**
* The maximum number of connections or groups to show per page.
*
* @type Number
*/
pageSize : '='
},

View File

@@ -24,4 +24,4 @@
* Module for displaying the contents of a connection group, allowing the user
* to select individual connections or groups.
*/
angular.module('groupList', ['rest']);
angular.module('groupList', ['pager', 'rest']);

View File

@@ -51,6 +51,13 @@
</script>
<div class="list-item" ng-repeat="item in rootItem.children | orderBy : 'name'" ng-include="'nestedGroup.html'"></div>
<!-- Root-level connections / groups -->
<div class="group-list-page">
<div class="list-item" ng-repeat="item in childrenPage" ng-include="'nestedGroup.html'"></div>
</div>
<!-- Pager for connections / groups -->
<guac-pager page="childrenPage" items="rootItem.children | orderBy : 'name'"
page-size="pageSize"></guac-pager>
</div>

View File

@@ -41,7 +41,8 @@
<guac-group-list
connection-group="rootConnectionGroup"
connection-template="'app/home/templates/connection.html'"
connection-group-template="'app/home/templates/connectionGroup.html'"></guac-group-list>
connection-group-template="'app/home/templates/connectionGroup.html'"
page-size="20"></guac-group-list>
</div>
</div>

View File

@@ -289,51 +289,6 @@ div.section {
background-image: url('images/group-icons/guac-open.png');
}
/*
* Settings formatting
*/
.form dt,
.settings dt {
border-bottom: 1px dotted #AAA;
padding-bottom: 0.25em;
}
.form dd,
.settings dd {
margin: 1.5em;
margin-left: 2.5em;
font-size: 0.75em;
}
.connections input.name,
.users input.name {
max-width: 80%;
width: 20em;
}
.connection-list,
.user-list {
border: 1px solid rgba(0, 0, 0, 0.25);
height: 20em;
-moz-border-radius: 0.2em;
-webkit-border-radius: 0.2em;
-khtml-border-radius: 0.2em;
border-radius: 0.2em;
overflow: auto;
}
.connections .add-connection,
.connections .add-connection-group,
.users .add-user {
font-size: 0.8em;
}
.connection-add-form,
.user-add-form {
margin-bottom: 0.5em;
}
div.logout-panel {
padding: 0.45em;
text-align: right;
@@ -346,66 +301,7 @@ div.logout-panel {
padding-right: 1em;
}
.first-page,
.prev-page,
.set-page,
.next-page,
.last-page {
cursor: pointer;
vertical-align: middle;
}
.first-page.disabled,
.prev-page.disabled,
.set-page.disabled,
.next-page.disabled,
.last-page.disabled {
cursor: auto;
opacity: 0.25;
}
.set-page,
.more-pages {
display: inline-block;
padding: 0.25em;
text-align: center;
min-width: 1.25em;
}
.set-page {
text-decoration: underline;
}
.set-page.current {
cursor: auto;
text-decoration: none;
font-weight: bold;
background: rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.1);
-moz-border-radius: 0.2em;
-webkit-border-radius: 0.2em;
-khtml-border-radius: 0.2em;
border-radius: 0.2em;
}
.icon.first-page {
background-image: url('images/action-icons/guac-first-page.png');
}
.icon.prev-page {
background-image: url('images/action-icons/guac-prev-page.png');
}
.icon.next-page {
background-image: url('images/action-icons/guac-next-page.png');
}
.icon.last-page {
background-image: url('images/action-icons/guac-last-page.png');
}
.buttons,
.list-pager-buttons {
.buttons {
text-align: center;
margin: 1em;
}

View File

@@ -23,5 +23,5 @@
/**
* The module for the administration functionality.
*/
angular.module('manage', ['groupList', 'locale', 'rest']);
angular.module('manage', ['groupList', 'locale', 'pager', 'rest']);

View File

@@ -20,6 +20,12 @@
* THE SOFTWARE.
*/
button.add-user,
a.button.add-connection,
a.button.add-connection-group {
font-size: 0.8em;
}
button.add-user {
background-image: url('images/action-icons/guac-user-add.png');

View File

@@ -30,3 +30,12 @@
text-align: center;
margin-bottom: 1em;
}
.manage .user-add-form {
margin-bottom: 0.5em;
}
.manage .user-add-form input.name {
max-width: 80%;
width: 20em;
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
.manage .connection-list .group-list-page,
.manage .user-list {
border: 1px solid rgba(0, 0, 0, 0.25);
min-height: 15em;
-moz-border-radius: 0.2em;
-webkit-border-radius: 0.2em;
-khtml-border-radius: 0.2em;
border-radius: 0.2em;
}

View File

@@ -44,7 +44,7 @@ THE SOFTWARE.
<!-- List of users this user has access to -->
<div class="user-list">
<div ng-repeat="user in users | orderBy : 'username'" class="user list-item">
<div ng-repeat="user in userPage" class="user list-item">
<a ng-href="#/manage/users/{{user.username}}">
<div class="caption">
<div class="icon user"></div>
@@ -53,6 +53,10 @@ THE SOFTWARE.
</a>
</div>
</div>
<!-- Pager controls for user list -->
<guac-pager page="userPage" items="users | orderBy : 'username'"></guac-pager>
</div>
</div>

View File

@@ -68,7 +68,8 @@ THE SOFTWARE.
context="groupListContext"
connection-group="rootGroup"
connection-template="'app/manage/templates/connectionPermission.html'"
connection-group-template="'app/manage/templates/connectionGroupPermission.html'"/>
connection-group-template="'app/manage/templates/connectionGroupPermission.html'"
page-size="20"/>
</div>
<!-- Form action buttons -->

View File

@@ -0,0 +1,303 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* A directive which provides pagination controls, along with a paginated
* subset of the elements of some given array.
*/
angular.module('pager').directive('guacPager', [function guacPager() {
return {
restrict: 'E',
replace: true,
scope: {
/**
* The property to which a subset of the provided array will be
* assigned.
*
* @type Array
*/
page : '=',
/**
* The maximum number of items per page.
*
* @type Number
*/
pageSize : '&',
/**
* The maximum number of page choices to provide, regardless of the
* total number of pages.
*
* @type Number
*/
pageCount : '&',
/**
* An array objects to paginate. Subsets of this array will be
* exposed as pages.
*
* @type Array
*/
items : '&'
},
templateUrl: 'app/pager/templates/guacPager.html',
controller: ['$scope', function guacPagerController($scope) {
/**
* The default size of a page, if not provided via the pageSize
* attribute.
*
* @type Number
*/
var DEFAULT_PAGE_SIZE = 10;
/**
* The default maximum number of page choices to provide, if a
* value is not providede via the pageCount attribute.
*
* @type Number
*/
var DEFAULT_PAGE_COUNT = 11;
/**
* An array of arrays, where the Nth array contains the contents of
* the Nth page.
*
* @type Array[]
*/
var pages = [];
/**
* The number of the first selectable page.
*
* @type Number;
*/
$scope.firstPage = 1;
/**
* The number of the page immediately before the currently-selected
* page.
*
* @type Number;
*/
$scope.previousPage = 1;
/**
* The number of the currently-selected page.
*
* @type Number;
*/
$scope.currentPage = 1;
/**
* The number of the page immediately after the currently-selected
* page.
*
* @type Number;
*/
$scope.nextPage = 1;
/**
* The number of the last selectable page.
*
* @type Number;
*/
$scope.lastPage = 1;
/**
* An array of relevant page numbers that the user may want to jump
* to directly.
*
* @type Number[]
*/
$scope.pageNumbers = [];
/**
* Updates the displayed page number choices.
*/
var updatePageNumbers = function updatePageNumbers() {
// Get page count
var pageCount = $scope.pageCount() || DEFAULT_PAGE_COUNT;
// Determine start/end of page window
var windowStart = $scope.currentPage - (pageCount - 1) / 2;
var windowEnd = windowStart + pageCount - 1;
// Shift window as necessary if it extends beyond the first page
if (windowStart < $scope.firstPage) {
windowEnd = Math.min($scope.lastPage, windowEnd - windowStart + $scope.firstPage);
windowStart = $scope.firstPage;
}
// Shift window as necessary if it extends beyond the last page
else if (windowEnd > $scope.lastPage) {
windowStart = Math.max(1, windowStart - windowEnd + $scope.lastPage);
windowEnd = $scope.lastPage;
}
// Generate list of relevant page numbers
$scope.pageNumbers = [];
for (var pageNumber = windowStart; pageNumber <= windowEnd; pageNumber++)
$scope.pageNumbers.push(pageNumber);
};
/**
* Iterates through the bound items array, splitting it into pages
* based on the current page size.
*/
var updatePages = function updatePages() {
// Get current items and page size
var items = $scope.items();
var pageSize = $scope.pageSize() || DEFAULT_PAGE_SIZE;
// Clear current pages
pages = [];
// Only split into pages if items actually exist
if (items) {
// Split into pages of pageSize items each
for (var i = 0; i < items.length; i += pageSize)
pages.push(items.slice(i, i + pageSize));
}
// Update minimum and maximum values
$scope.firstPage = 1;
$scope.lastPage = pages.length;
// Select an appropriate page
var adjustedCurrentPage = Math.min($scope.lastPage, Math.max($scope.firstPage, $scope.currentPage));
$scope.selectPage(adjustedCurrentPage);
};
/**
* Selects the page having the given number, assigning that page to
* the property bound to the page attribute. If no such page
* exists, the property will be set to undefined instead. Valid
* page numbers begin at 1.
*
* @param {Number} page
* The number of the page to select. Valid page numbers begin
* at 1.
*/
$scope.selectPage = function selectPage(page) {
// Select the chosen page
$scope.currentPage = page;
$scope.page = pages[page-1];
// Update next/previous page numbers
$scope.nextPage = Math.min($scope.lastPage, $scope.currentPage + 1);
$scope.previousPage = Math.max($scope.firstPage, $scope.currentPage - 1);
// Update which page numbers are shown
updatePageNumbers();
};
/**
* Returns whether the given page number can be legally selected
* via selectPage(), resulting in a different page being shown.
*
* @param {Number} page
* The page number to check.
*
* @returns {Boolean}
* true if the page having the given number can be selected,
* false otherwise.
*/
$scope.canSelectPage = function canSelectPage(page) {
return page !== $scope.currentPage
&& page >= $scope.firstPage
&& page <= $scope.lastPage;
};
/**
* Returns whether the page having the given number is currently
* selected.
*
* @param {Number} page
* The page number to check.
*
* @returns {Boolean}
* true if the page having the given number is currently
* selected, false otherwise.
*/
$scope.isSelected = function isSelected(page) {
return page === $scope.currentPage;
};
/**
* Returns whether pages exist before the first page listed in the
* pageNumbers array.
*
* @returns {Boolean}
* true if pages exist before the first page listed in the
* pageNumbers array, false otherwise.
*/
$scope.hasMorePagesBefore = function hasMorePagesBefore() {
var firstPageNumber = $scope.pageNumbers[0]
return firstPageNumber !== $scope.firstPage;
};
/**
* Returns whether pages exist after the last page listed in the
* pageNumbers array.
*
* @returns {Boolean}
* true if pages exist after the last page listed in the
* pageNumbers array, false otherwise.
*/
$scope.hasMorePagesAfter = function hasMorePagesAfter() {
var lastPageNumber = $scope.pageNumbers[$scope.pageNumbers.length - 1];
return lastPageNumber !== $scope.lastPage;
};
// Update available pages when available items are changed
$scope.$watchCollection($scope.items, function itemsChanged() {
updatePages();
});
// Update available pages when page size is changed
$scope.$watch($scope.pageSize, function pageSizeChanged() {
updatePages();
});
// Update available page numbers when page count is changed
$scope.$watch($scope.pageCount, function pageCountChanged() {
updatePageNumbers();
});
}]
};
}]);

View File

@@ -0,0 +1,26 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Module for displaying the contents of a list, split into multiple pages.
*/
angular.module('pager', []);

View File

@@ -0,0 +1,90 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
.pager {
text-align: center;
margin: 1em;
}
.pager .page-numbers {
display: inline-block;
margin: 0;
padding: 0;
}
.pager .first-page,
.pager .prev-page,
.pager .set-page,
.pager .next-page,
.pager .last-page {
cursor: pointer;
vertical-align: middle;
}
.pager .first-page.disabled,
.pager .prev-page.disabled,
.pager .set-page.disabled,
.pager .next-page.disabled,
.pager .last-page.disabled {
cursor: auto;
opacity: 0.25;
}
.pager .set-page,
.pager .more-pages {
display: inline-block;
padding: 0.25em;
text-align: center;
min-width: 1.25em;
}
.pager .set-page {
text-decoration: underline;
}
.pager .set-page.current {
cursor: auto;
text-decoration: none;
font-weight: bold;
background: rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.1);
-moz-border-radius: 0.2em;
-webkit-border-radius: 0.2em;
-khtml-border-radius: 0.2em;
border-radius: 0.2em;
}
.pager .icon.first-page {
background-image: url('images/action-icons/guac-first-page.png');
}
.pager .icon.prev-page {
background-image: url('images/action-icons/guac-prev-page.png');
}
.pager .icon.next-page {
background-image: url('images/action-icons/guac-next-page.png');
}
.pager .icon.last-page {
background-image: url('images/action-icons/guac-last-page.png');
}

View File

@@ -0,0 +1,46 @@
<div class="pager" ng-show="pageNumbers.length > 1">
<!--
Copyright (C) 2015 Glyptodon LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<!-- First / Previous -->
<div class="first-page icon" ng-class="{disabled: !canSelectPage(firstPage)}" ng-click="selectPage(firstPage)"/>
<div class="prev-page icon" ng-class="{disabled: !canSelectPage(previousPage)}" ng-click="selectPage(previousPage)"/>
<!-- Indicator of the existence of pages before the first page number shown -->
<div class="more-pages" ng-show="hasMorePagesBefore()">...</div>
<!-- Page numbers -->
<ul class="page-numbers">
<li class="set-page"
ng-class="{current: isSelected(pageNumber)}"
ng-repeat="pageNumber in pageNumbers"
ng-click="selectPage(pageNumber)">{{pageNumber}}</li>
</ul>
<!-- Indicator of the existence of pages beyond the last page number shown -->
<div class="more-pages" ng-show="hasMorePagesAfter()">...</div>
<!-- Next / Last -->
<div class="next-page icon" ng-class="{disabled: !canSelectPage(nextPage)}" ng-click="selectPage(nextPage)"/>
<div class="last-page icon" ng-class="{disabled: !canSelectPage(lastPage)}" ng-click="selectPage(lastPage)"/>
</div>