diff --git a/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js b/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js
index d1190bab6..2bf75a89d 100644
--- a/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js
+++ b/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js
@@ -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 : '='
},
diff --git a/guacamole/src/main/webapp/app/groupList/groupListModule.js b/guacamole/src/main/webapp/app/groupList/groupListModule.js
index 99841eeea..084f3b931 100644
--- a/guacamole/src/main/webapp/app/groupList/groupListModule.js
+++ b/guacamole/src/main/webapp/app/groupList/groupListModule.js
@@ -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']);
diff --git a/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html b/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html
index 287804321..ae1ef679f 100644
--- a/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html
+++ b/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html
@@ -51,6 +51,13 @@
-
+
+
+
+
+
diff --git a/guacamole/src/main/webapp/app/home/templates/home.html b/guacamole/src/main/webapp/app/home/templates/home.html
index 5fabad7c5..4396625b1 100644
--- a/guacamole/src/main/webapp/app/home/templates/home.html
+++ b/guacamole/src/main/webapp/app/home/templates/home.html
@@ -41,7 +41,8 @@
+ connection-group-template="'app/home/templates/connectionGroup.html'"
+ page-size="20">
diff --git a/guacamole/src/main/webapp/app/index/styles/ui.css b/guacamole/src/main/webapp/app/index/styles/ui.css
index bba2c9970..f17797341 100644
--- a/guacamole/src/main/webapp/app/index/styles/ui.css
+++ b/guacamole/src/main/webapp/app/index/styles/ui.css
@@ -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;
}
diff --git a/guacamole/src/main/webapp/app/manage/manageModule.js b/guacamole/src/main/webapp/app/manage/manageModule.js
index 7b392a9dc..e32aa54d1 100644
--- a/guacamole/src/main/webapp/app/manage/manageModule.js
+++ b/guacamole/src/main/webapp/app/manage/manageModule.js
@@ -23,5 +23,5 @@
/**
* The module for the administration functionality.
*/
-angular.module('manage', ['groupList', 'locale', 'rest']);
+angular.module('manage', ['groupList', 'locale', 'pager', 'rest']);
diff --git a/guacamole/src/main/webapp/app/manage/styles/buttons.css b/guacamole/src/main/webapp/app/manage/styles/buttons.css
index 0e4925855..d67e5dd41 100644
--- a/guacamole/src/main/webapp/app/manage/styles/buttons.css
+++ b/guacamole/src/main/webapp/app/manage/styles/buttons.css
@@ -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');
diff --git a/guacamole/src/main/webapp/app/manage/styles/forms.css b/guacamole/src/main/webapp/app/manage/styles/forms.css
index 0724562e2..d8d63fd3c 100644
--- a/guacamole/src/main/webapp/app/manage/styles/forms.css
+++ b/guacamole/src/main/webapp/app/manage/styles/forms.css
@@ -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;
+}
diff --git a/guacamole/src/main/webapp/app/manage/styles/lists.css b/guacamole/src/main/webapp/app/manage/styles/lists.css
new file mode 100644
index 000000000..df30143db
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/styles/lists.css
@@ -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;
+}
diff --git a/guacamole/src/main/webapp/app/manage/templates/manage.html b/guacamole/src/main/webapp/app/manage/templates/manage.html
index 9795ebe07..b5834a33a 100644
--- a/guacamole/src/main/webapp/app/manage/templates/manage.html
+++ b/guacamole/src/main/webapp/app/manage/templates/manage.html
@@ -44,7 +44,7 @@ THE SOFTWARE.
diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html
index 4028e74ff..7f9112d3e 100644
--- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html
+++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html
@@ -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"/>
diff --git a/guacamole/src/main/webapp/app/pager/directives/guacPager.js b/guacamole/src/main/webapp/app/pager/directives/guacPager.js
new file mode 100644
index 000000000..8074f1362
--- /dev/null
+++ b/guacamole/src/main/webapp/app/pager/directives/guacPager.js
@@ -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();
+ });
+
+ }]
+
+ };
+}]);
diff --git a/guacamole/src/main/webapp/app/pager/pagerModule.js b/guacamole/src/main/webapp/app/pager/pagerModule.js
new file mode 100644
index 000000000..026a64286
--- /dev/null
+++ b/guacamole/src/main/webapp/app/pager/pagerModule.js
@@ -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', []);
diff --git a/guacamole/src/main/webapp/app/pager/styles/pager.css b/guacamole/src/main/webapp/app/pager/styles/pager.css
new file mode 100644
index 000000000..bb0b9229c
--- /dev/null
+++ b/guacamole/src/main/webapp/app/pager/styles/pager.css
@@ -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');
+}
diff --git a/guacamole/src/main/webapp/app/pager/templates/guacPager.html b/guacamole/src/main/webapp/app/pager/templates/guacPager.html
new file mode 100644
index 000000000..0ebe152b5
--- /dev/null
+++ b/guacamole/src/main/webapp/app/pager/templates/guacPager.html
@@ -0,0 +1,46 @@
+