diff --git a/Gruntfile.js b/Gruntfile.js
index 138eec91d..62bbb53c7 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -69,6 +69,15 @@ module.exports = function(grunt) {
'eucaconsole/static/js/thirdparty/magic-search': 'angular-magic-search/magic_search.*'
}
},
+ angular_smart_table: {
+ options: {
+ destPrefix: 'eucaconsole/static/js/thirdparty/angular'
+ },
+ files: {
+ 'angular-smart-table.js': 'angular-smart-table/dist/smart-table.js',
+ 'angular-smart-table.min.js': 'angular-smart-table/dist/smart-table.min.js'
+ }
+ },
d3: {
options: {
destPrefix: 'eucaconsole/static/js/thirdparty/d3'
diff --git a/bower.json b/bower.json
index fc4c33a2f..391f7e412 100644
--- a/bower.json
+++ b/bower.json
@@ -5,6 +5,7 @@
"angular": "1.3.8",
"angular-sanitize": "1.3.8",
"angular-mocks": "1.3.8",
+ "angular-smart-table": "2.1.3",
"components-font-awesome": "4.3.0",
"foundation": "5.5.1",
"jquery": "2.1.3",
diff --git a/eucaconsole/layout.py b/eucaconsole/layout.py
index f2af1cb34..0bf309e2c 100644
--- a/eucaconsole/layout.py
+++ b/eucaconsole/layout.py
@@ -97,6 +97,8 @@ def __init__(self, context, request):
self.searchtext_prompt = _(u'Select facets for filter, or enter text to search')
self.searchtext_prompt2 = _(u'Enter text to search')
self.searchtext_text_facet = _(u'Text')
+ self.standard_table_repeat = 'item in items | orderBy: sortBy | limitTo:displayCount'
+ self.smart_table_repeat = 'item in displayedCollection | limitTo:displayCount'
def get_notifications(self):
"""Get notifications, categorized by message type ('info', 'success', 'warning', or 'error')
diff --git a/eucaconsole/static/css/eucaconsole.css b/eucaconsole/static/css/eucaconsole.css
index 6fd71bae3..e4bdb32eb 100644
--- a/eucaconsole/static/css/eucaconsole.css
+++ b/eucaconsole/static/css/eucaconsole.css
@@ -185,7 +185,7 @@ textarea { height: auto; min-height: 50px; }
select { width: 100%; }
-.alert-box { border-style: solid; border-width: 1px; display: block; font-weight: normal; margin-bottom: 1.25rem; position: relative; padding: 0.875rem 1.5rem 0.875rem 0.875rem; font-size: 0.8125rem; transition: opacity 300ms ease-out; background-color: #007dba; border-color: #006ca0; color: #FFFFFF; }
+.alert-box { border-style: solid; border-width: 1px; display: block; font-weight: normal; margin-bottom: 1.25rem; position: relative; padding: 0.875rem 1.5rem 0.875rem 0.875rem; font-size: 0.8125rem; transition: opacity 300ms ease-out; background-color: #007dba; border-color: #006ba0; color: #FFFFFF; }
.alert-box .close { font-size: 1.375rem; padding: 0 6px 4px; line-height: .9; position: absolute; top: 50%; margin-top: -0.6875rem; right: 0.25rem; color: #333333; opacity: 0.3; background: inherit; }
.alert-box .close:hover, .alert-box .close:focus { opacity: 0.5; }
.alert-box.radius { border-radius: 3px; }
@@ -921,7 +921,7 @@ label.error { color: #f04124; }
.panel.callout h1, .panel.callout h2, .panel.callout h3, .panel.callout h4, .panel.callout h5, .panel.callout h6 { line-height: 1; margin-bottom: 0.625rem; }
.panel.callout h1.subheader, .panel.callout h2.subheader, .panel.callout h3.subheader, .panel.callout h4.subheader, .panel.callout h5.subheader, .panel.callout h6.subheader { line-height: 1.4; }
.panel.callout a:not(.button) { color: #007dba; }
-.panel.callout a:not(.button):hover, .panel.callout a:not(.button):focus { color: #006ca0; }
+.panel.callout a:not(.button):hover, .panel.callout a:not(.button):focus { color: #006ba0; }
.panel.radius { border-radius: 3px; }
/* Progress Bar */
@@ -992,7 +992,7 @@ label.error { color: #f04124; }
.sub-nav dt a, .sub-nav dd a, .sub-nav li a { text-decoration: none; color: #999999; padding: 0.1875rem 1rem; }
.sub-nav dt a:hover, .sub-nav dd a:hover, .sub-nav li a:hover { color: #737373; }
.sub-nav dt.active a, .sub-nav dd.active a, .sub-nav li.active a { border-radius: 3px; font-weight: normal; background: #007dba; padding: 0.1875rem 1rem; cursor: default; color: #FFFFFF; }
-.sub-nav dt.active a:hover, .sub-nav dd.active a:hover, .sub-nav li.active a:hover { background: #006ca0; }
+.sub-nav dt.active a:hover, .sub-nav dd.active a:hover, .sub-nav li.active a:hover { background: #006ba0; }
div.switch { position: relative; padding: 0; display: block; overflow: hidden; border-style: solid; border-width: 1px; margin-bottom: 1.25rem; height: 2.25rem; background: #fff; border-color: #cccccc; }
div.switch label { position: relative; left: 0; z-index: 2; float: left; width: 50%; height: 100%; margin: 0; font-weight: bold; text-align: left; transition: all 0.1s ease-out; }
@@ -1047,7 +1047,7 @@ table thead tr th, table tfoot tr th, table tfoot tr td, table tbody tr th, tabl
.tabs:after { clear: both; }
.tabs dd, .tabs .tab-title { position: relative; margin-bottom: 0 !important; list-style: none; float: left; }
.tabs dd > a, .tabs .tab-title > a { display: block; background-color: #007dba; color: #222222; padding: 0.25rem 0.5rem; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 1rem; }
-.tabs dd > a:hover, .tabs .tab-title > a:hover { background-color: #008ed4; }
+.tabs dd > a:hover, .tabs .tab-title > a:hover { background-color: #008ed3; }
.tabs dd > a:focus, .tabs .tab-title > a:focus { outline: none; }
.tabs dd.active a, .tabs .tab-title.active a { background-color: #FFFFFF; color: #222222; }
.tabs.radius dd:first-child a, .tabs.radius .tab:first-child a { -webkit-border-bottom-left-radius: 3px; -webkit-border-top-left-radius: 3px; border-bottom-left-radius: 3px; border-top-left-radius: 3px; }
@@ -1271,7 +1271,7 @@ div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, form, p, blockquote, t
/* Default Link Styles */
a { color: #007dba; text-decoration: none; line-height: inherit; }
-a:hover, a:focus { color: #006ca0; }
+a:hover, a:focus { color: #006ba0; }
a img { border: none; }
/* Default paragraph styles */
@@ -1878,6 +1878,9 @@ table.table thead tr th.count { text-align: center; }
table.table tr td.count { text-align: center; }
table.table tr td { vertical-align: top; }
table.table td.breakall { word-break: break-all !important; }
+table.table.smart-table th[st-sort]:hover { cursor: pointer; }
+table.table.smart-table .st-sort-ascent:before { content: '\25B2'; }
+table.table.smart-table .st-sort-descent:before { content: '\25BC'; }
/* ----------------------------------------- Footer
----------------------------------------- */
diff --git a/eucaconsole/static/js/pages/volumes.js b/eucaconsole/static/js/pages/volumes.js
index 3c57a9d14..29f3d42b6 100644
--- a/eucaconsole/static/js/pages/volumes.js
+++ b/eucaconsole/static/js/pages/volumes.js
@@ -4,7 +4,7 @@
*
*/
-angular.module('VolumesPage', ['LandingPage', 'EucaConsoleUtils'])
+angular.module('VolumesPage', ['LandingPage', 'EucaConsoleUtils', 'smart-table'])
.controller('VolumesCtrl', function ($scope, $http, $timeout, eucaUnescapeJson) {
$scope.volumeID = '';
$scope.volumeName = '';
diff --git a/eucaconsole/static/js/thirdparty/angular/angular-smart-table.js b/eucaconsole/static/js/thirdparty/angular/angular-smart-table.js
new file mode 100644
index 000000000..573c19845
--- /dev/null
+++ b/eucaconsole/static/js/thirdparty/angular/angular-smart-table.js
@@ -0,0 +1,508 @@
+/**
+* @version 2.1.3
+* @license MIT
+*/
+(function (ng, undefined){
+ 'use strict';
+
+ng.module('smart-table', []).run(['$templateCache', function ($templateCache) {
+ $templateCache.put('template/smart-table/pagination.html',
+ '');
+}]);
+
+
+ng.module('smart-table')
+ .constant('stConfig', {
+ pagination: {
+ template: 'template/smart-table/pagination.html',
+ itemsByPage: 10,
+ displayedPages: 5
+ },
+ search: {
+ delay: 400, // ms
+ inputEvent: 'input'
+ },
+ select: {
+ mode: 'single',
+ selectedClass: 'st-selected'
+ },
+ sort: {
+ ascentClass: 'st-sort-ascent',
+ descentClass: 'st-sort-descent',
+ skipNatural: false
+ },
+ pipe: {
+ delay: 100 //ms
+ }
+ });
+ng.module('smart-table')
+ .controller('stTableController', ['$scope', '$parse', '$filter', '$attrs', function StTableController ($scope, $parse, $filter, $attrs) {
+ var propertyName = $attrs.stTable;
+ var displayGetter = $parse(propertyName);
+ var displaySetter = displayGetter.assign;
+ var safeGetter;
+ var orderBy = $filter('orderBy');
+ var filter = $filter('filter');
+ var safeCopy = copyRefs(displayGetter($scope));
+ var tableState = {
+ sort: {},
+ search: {},
+ pagination: {
+ start: 0,
+ totalItemCount: 0
+ }
+ };
+ var filtered;
+ var pipeAfterSafeCopy = true;
+ var ctrl = this;
+ var lastSelected;
+
+ function copyRefs (src) {
+ return src ? [].concat(src) : [];
+ }
+
+ function updateSafeCopy () {
+ safeCopy = copyRefs(safeGetter($scope));
+ if (pipeAfterSafeCopy === true) {
+ ctrl.pipe();
+ }
+ }
+
+ function deepDelete (object, path) {
+ if (path.indexOf('.') != -1) {
+ var partials = path.split('.');
+ var key = partials.pop();
+ var parentPath = partials.join('.');
+ var parentObject = $parse(parentPath)(object)
+ delete parentObject[key];
+ if (Object.keys(parentObject).length == 0) {
+ deepDelete(object, parentPath);
+ }
+ } else {
+ delete object[path];
+ }
+ }
+
+ if ($attrs.stSafeSrc) {
+ safeGetter = $parse($attrs.stSafeSrc);
+ $scope.$watch(function () {
+ var safeSrc = safeGetter($scope);
+ return safeSrc ? safeSrc.length : 0;
+
+ }, function (newValue, oldValue) {
+ if (newValue !== safeCopy.length) {
+ updateSafeCopy();
+ }
+ });
+ $scope.$watch(function () {
+ return safeGetter($scope);
+ }, function (newValue, oldValue) {
+ if (newValue !== oldValue) {
+ tableState.pagination.start = 0;
+ updateSafeCopy();
+ }
+ });
+ }
+
+ /**
+ * sort the rows
+ * @param {Function | String} predicate - function or string which will be used as predicate for the sorting
+ * @param [reverse] - if you want to reverse the order
+ */
+ this.sortBy = function sortBy (predicate, reverse) {
+ tableState.sort.predicate = predicate;
+ tableState.sort.reverse = reverse === true;
+
+ if (ng.isFunction(predicate)) {
+ tableState.sort.functionName = predicate.name;
+ } else {
+ delete tableState.sort.functionName;
+ }
+
+ tableState.pagination.start = 0;
+ return this.pipe();
+ };
+
+ /**
+ * search matching rows
+ * @param {String} input - the input string
+ * @param {String} [predicate] - the property name against you want to check the match, otherwise it will search on all properties
+ */
+ this.search = function search (input, predicate) {
+ var predicateObject = tableState.search.predicateObject || {};
+ var prop = predicate ? predicate : '$';
+
+ input = ng.isString(input) ? input.trim() : input;
+ $parse(prop).assign(predicateObject, input);
+ // to avoid to filter out null value
+ if (!input) {
+ deepDelete(predicateObject, prop);
+ }
+ tableState.search.predicateObject = predicateObject;
+ tableState.pagination.start = 0;
+ return this.pipe();
+ };
+
+ /**
+ * this will chain the operations of sorting and filtering based on the current table state (sort options, filtering, ect)
+ */
+ this.pipe = function pipe () {
+ var pagination = tableState.pagination;
+ var output;
+ filtered = tableState.search.predicateObject ? filter(safeCopy, tableState.search.predicateObject) : safeCopy;
+ if (tableState.sort.predicate) {
+ filtered = orderBy(filtered, tableState.sort.predicate, tableState.sort.reverse);
+ }
+ pagination.totalItemCount = filtered.length;
+ if (pagination.number !== undefined) {
+ pagination.numberOfPages = filtered.length > 0 ? Math.ceil(filtered.length / pagination.number) : 1;
+ pagination.start = pagination.start >= filtered.length ? (pagination.numberOfPages - 1) * pagination.number : pagination.start;
+ output = filtered.slice(pagination.start, pagination.start + parseInt(pagination.number));
+ }
+ displaySetter($scope, output || filtered);
+ };
+
+ /**
+ * select a dataRow (it will add the attribute isSelected to the row object)
+ * @param {Object} row - the row to select
+ * @param {String} [mode] - "single" or "multiple" (multiple by default)
+ */
+ this.select = function select (row, mode) {
+ var rows = copyRefs(displayGetter($scope));
+ var index = rows.indexOf(row);
+ if (index !== -1) {
+ if (mode === 'single') {
+ row.isSelected = row.isSelected !== true;
+ if (lastSelected) {
+ lastSelected.isSelected = false;
+ }
+ lastSelected = row.isSelected === true ? row : undefined;
+ } else {
+ rows[index].isSelected = !rows[index].isSelected;
+ }
+ }
+ };
+
+ /**
+ * take a slice of the current sorted/filtered collection (pagination)
+ *
+ * @param {Number} start - start index of the slice
+ * @param {Number} number - the number of item in the slice
+ */
+ this.slice = function splice (start, number) {
+ tableState.pagination.start = start;
+ tableState.pagination.number = number;
+ return this.pipe();
+ };
+
+ /**
+ * return the current state of the table
+ * @returns {{sort: {}, search: {}, pagination: {start: number}}}
+ */
+ this.tableState = function getTableState () {
+ return tableState;
+ };
+
+ this.getFilteredCollection = function getFilteredCollection () {
+ return filtered || safeCopy;
+ };
+
+ /**
+ * Use a different filter function than the angular FilterFilter
+ * @param filterName the name under which the custom filter is registered
+ */
+ this.setFilterFunction = function setFilterFunction (filterName) {
+ filter = $filter(filterName);
+ };
+
+ /**
+ * Use a different function than the angular orderBy
+ * @param sortFunctionName the name under which the custom order function is registered
+ */
+ this.setSortFunction = function setSortFunction (sortFunctionName) {
+ orderBy = $filter(sortFunctionName);
+ };
+
+ /**
+ * Usually when the safe copy is updated the pipe function is called.
+ * Calling this method will prevent it, which is something required when using a custom pipe function
+ */
+ this.preventPipeOnWatch = function preventPipe () {
+ pipeAfterSafeCopy = false;
+ };
+ }])
+ .directive('stTable', function () {
+ return {
+ restrict: 'A',
+ controller: 'stTableController',
+ link: function (scope, element, attr, ctrl) {
+
+ if (attr.stSetFilter) {
+ ctrl.setFilterFunction(attr.stSetFilter);
+ }
+
+ if (attr.stSetSort) {
+ ctrl.setSortFunction(attr.stSetSort);
+ }
+ }
+ };
+ });
+
+ng.module('smart-table')
+ .directive('stSearch', ['stConfig', '$timeout','$parse', function (stConfig, $timeout, $parse) {
+ return {
+ require: '^stTable',
+ link: function (scope, element, attr, ctrl) {
+ var tableCtrl = ctrl;
+ var promise = null;
+ var throttle = attr.stDelay || stConfig.search.delay;
+ var event = attr.stInputEvent || stConfig.search.inputEvent;
+
+ attr.$observe('stSearch', function (newValue, oldValue) {
+ var input = element[0].value;
+ if (newValue !== oldValue && input) {
+ ctrl.tableState().search = {};
+ tableCtrl.search(input, newValue);
+ }
+ });
+
+ //table state -> view
+ scope.$watch(function () {
+ return ctrl.tableState().search;
+ }, function (newValue, oldValue) {
+ var predicateExpression = attr.stSearch || '$';
+ if (newValue.predicateObject && $parse(predicateExpression)(newValue.predicateObject) !== element[0].value) {
+ element[0].value = $parse(predicateExpression)(newValue.predicateObject) || '';
+ }
+ }, true);
+
+ // view -> table state
+ element.bind(event, function (evt) {
+ evt = evt.originalEvent || evt;
+ if (promise !== null) {
+ $timeout.cancel(promise);
+ }
+
+ promise = $timeout(function () {
+ tableCtrl.search(evt.target.value, attr.stSearch || '');
+ promise = null;
+ }, throttle);
+ });
+ }
+ };
+ }]);
+
+ng.module('smart-table')
+ .directive('stSelectRow', ['stConfig', function (stConfig) {
+ return {
+ restrict: 'A',
+ require: '^stTable',
+ scope: {
+ row: '=stSelectRow'
+ },
+ link: function (scope, element, attr, ctrl) {
+ var mode = attr.stSelectMode || stConfig.select.mode;
+ element.bind('click', function () {
+ scope.$apply(function () {
+ ctrl.select(scope.row, mode);
+ });
+ });
+
+ scope.$watch('row.isSelected', function (newValue) {
+ if (newValue === true) {
+ element.addClass(stConfig.select.selectedClass);
+ } else {
+ element.removeClass(stConfig.select.selectedClass);
+ }
+ });
+ }
+ };
+ }]);
+
+ng.module('smart-table')
+ .directive('stSort', ['stConfig', '$parse', function (stConfig, $parse) {
+ return {
+ restrict: 'A',
+ require: '^stTable',
+ link: function (scope, element, attr, ctrl) {
+
+ var predicate = attr.stSort;
+ var getter = $parse(predicate);
+ var index = 0;
+ var classAscent = attr.stClassAscent || stConfig.sort.ascentClass;
+ var classDescent = attr.stClassDescent || stConfig.sort.descentClass;
+ var stateClasses = [classAscent, classDescent];
+ var sortDefault;
+ var skipNatural = attr.stSkipNatural !== undefined ? attr.stSkipNatural : stConfig.sort.skipNatural;
+
+ if (attr.stSortDefault) {
+ sortDefault = scope.$eval(attr.stSortDefault) !== undefined ? scope.$eval(attr.stSortDefault) : attr.stSortDefault;
+ }
+
+ //view --> table state
+ function sort () {
+ index++;
+ predicate = ng.isFunction(getter(scope)) ? getter(scope) : attr.stSort;
+ if (index % 3 === 0 && !!skipNatural !== true) {
+ //manual reset
+ index = 0;
+ ctrl.tableState().sort = {};
+ ctrl.tableState().pagination.start = 0;
+ ctrl.pipe();
+ } else {
+ ctrl.sortBy(predicate, index % 2 === 0);
+ }
+ }
+
+ element.bind('click', function sortClick () {
+ if (predicate) {
+ scope.$apply(sort);
+ }
+ });
+
+ if (sortDefault) {
+ index = sortDefault === 'reverse' ? 1 : 0;
+ sort();
+ }
+
+ //table state --> view
+ scope.$watch(function () {
+ return ctrl.tableState().sort;
+ }, function (newValue) {
+ if (newValue.predicate !== predicate) {
+ index = 0;
+ element
+ .removeClass(classAscent)
+ .removeClass(classDescent);
+ } else {
+ index = newValue.reverse === true ? 2 : 1;
+ element
+ .removeClass(stateClasses[index % 2])
+ .addClass(stateClasses[index - 1]);
+ }
+ }, true);
+ }
+ };
+ }]);
+
+ng.module('smart-table')
+ .directive('stPagination', ['stConfig', function (stConfig) {
+ return {
+ restrict: 'EA',
+ require: '^stTable',
+ scope: {
+ stItemsByPage: '=?',
+ stDisplayedPages: '=?',
+ stPageChange: '&'
+ },
+ templateUrl: function (element, attrs) {
+ if (attrs.stTemplate) {
+ return attrs.stTemplate;
+ }
+ return stConfig.pagination.template;
+ },
+ link: function (scope, element, attrs, ctrl) {
+
+ scope.stItemsByPage = scope.stItemsByPage ? +(scope.stItemsByPage) : stConfig.pagination.itemsByPage;
+ scope.stDisplayedPages = scope.stDisplayedPages ? +(scope.stDisplayedPages) : stConfig.pagination.displayedPages;
+
+ scope.currentPage = 1;
+ scope.pages = [];
+
+ function redraw () {
+ var paginationState = ctrl.tableState().pagination;
+ var start = 1;
+ var end;
+ var i;
+ var prevPage = scope.currentPage;
+ scope.totalItemCount = paginationState.totalItemCount;
+ scope.currentPage = Math.floor(paginationState.start / paginationState.number) + 1;
+
+ start = Math.max(start, scope.currentPage - Math.abs(Math.floor(scope.stDisplayedPages / 2)));
+ end = start + scope.stDisplayedPages;
+
+ if (end > paginationState.numberOfPages) {
+ end = paginationState.numberOfPages + 1;
+ start = Math.max(1, end - scope.stDisplayedPages);
+ }
+
+ scope.pages = [];
+ scope.numPages = paginationState.numberOfPages;
+
+ for (i = start; i < end; i++) {
+ scope.pages.push(i);
+ }
+
+ if (prevPage !== scope.currentPage) {
+ scope.stPageChange({newPage: scope.currentPage});
+ }
+ }
+
+ //table state --> view
+ scope.$watch(function () {
+ return ctrl.tableState().pagination;
+ }, redraw, true);
+
+ //scope --> table state (--> view)
+ scope.$watch('stItemsByPage', function (newValue, oldValue) {
+ if (newValue !== oldValue) {
+ scope.selectPage(1);
+ }
+ });
+
+ scope.$watch('stDisplayedPages', redraw);
+
+ //view -> table state
+ scope.selectPage = function (page) {
+ if (page > 0 && page <= scope.numPages) {
+ ctrl.slice((page - 1) * scope.stItemsByPage, scope.stItemsByPage);
+ }
+ };
+
+ if (!ctrl.tableState().pagination.number) {
+ ctrl.slice(0, scope.stItemsByPage);
+ }
+ }
+ };
+ }]);
+
+ng.module('smart-table')
+ .directive('stPipe', ['stConfig', '$timeout', function (config, $timeout) {
+ return {
+ require: 'stTable',
+ scope: {
+ stPipe: '='
+ },
+ link: {
+
+ pre: function (scope, element, attrs, ctrl) {
+
+ var pipePromise = null;
+
+ if (ng.isFunction(scope.stPipe)) {
+ ctrl.preventPipeOnWatch();
+ ctrl.pipe = function () {
+
+ if (pipePromise !== null) {
+ $timeout.cancel(pipePromise)
+ }
+
+ pipePromise = $timeout(function () {
+ scope.stPipe(ctrl.tableState(), ctrl);
+ }, config.pipe.delay);
+
+ return pipePromise;
+ }
+ }
+ },
+
+ post: function (scope, element, attrs, ctrl) {
+ ctrl.pipe();
+ }
+ }
+ };
+ }]);
+
+})(angular);
\ No newline at end of file
diff --git a/eucaconsole/static/js/thirdparty/angular/angular-smart-table.min.js b/eucaconsole/static/js/thirdparty/angular/angular-smart-table.min.js
new file mode 100644
index 000000000..c4407eae4
--- /dev/null
+++ b/eucaconsole/static/js/thirdparty/angular/angular-smart-table.min.js
@@ -0,0 +1,6 @@
+/**
+* @version 2.1.3
+* @license MIT
+*/
+!function(t,e){"use strict";t.module("smart-table",[]).run(["$templateCache",function(t){t.put("template/smart-table/pagination.html",'')}]),t.module("smart-table").constant("stConfig",{pagination:{template:"template/smart-table/pagination.html",itemsByPage:10,displayedPages:5},search:{delay:400,inputEvent:"input"},select:{mode:"single",selectedClass:"st-selected"},sort:{ascentClass:"st-sort-ascent",descentClass:"st-sort-descent",skipNatural:!1},pipe:{delay:100}}),t.module("smart-table").controller("stTableController",["$scope","$parse","$filter","$attrs",function(a,s,n,i){function r(t){return t?[].concat(t):[]}function l(){b=r(o(a)),S===!0&&v.pipe()}function c(t,e){if(-1!=e.indexOf(".")){var a=e.split("."),n=a.pop(),i=a.join("."),r=s(i)(t);delete r[n],0==Object.keys(r).length&&c(t,i)}else delete t[e]}var o,u,p,g=i.stTable,f=s(g),d=f.assign,m=n("orderBy"),h=n("filter"),b=r(f(a)),P={sort:{},search:{},pagination:{start:0,totalItemCount:0}},S=!0,v=this;i.stSafeSrc&&(o=s(i.stSafeSrc),a.$watch(function(){var t=o(a);return t?t.length:0},function(t){t!==b.length&&l()}),a.$watch(function(){return o(a)},function(t,e){t!==e&&(P.pagination.start=0,l())})),this.sortBy=function(e,a){return P.sort.predicate=e,P.sort.reverse=a===!0,t.isFunction(e)?P.sort.functionName=e.name:delete P.sort.functionName,P.pagination.start=0,this.pipe()},this.search=function(e,a){var n=P.search.predicateObject||{},i=a?a:"$";return e=t.isString(e)?e.trim():e,s(i).assign(n,e),e||c(n,i),P.search.predicateObject=n,P.pagination.start=0,this.pipe()},this.pipe=function(){var t,s=P.pagination;u=P.search.predicateObject?h(b,P.search.predicateObject):b,P.sort.predicate&&(u=m(u,P.sort.predicate,P.sort.reverse)),s.totalItemCount=u.length,s.number!==e&&(s.numberOfPages=u.length>0?Math.ceil(u.length/s.number):1,s.start=s.start>=u.length?(s.numberOfPages-1)*s.number:s.start,t=u.slice(s.start,s.start+parseInt(s.number))),d(a,t||u)},this.select=function(t,s){var n=r(f(a)),i=n.indexOf(t);-1!==i&&("single"===s?(t.isSelected=t.isSelected!==!0,p&&(p.isSelected=!1),p=t.isSelected===!0?t:e):n[i].isSelected=!n[i].isSelected)},this.slice=function(t,e){return P.pagination.start=t,P.pagination.number=e,this.pipe()},this.tableState=function(){return P},this.getFilteredCollection=function(){return u||b},this.setFilterFunction=function(t){h=n(t)},this.setSortFunction=function(t){m=n(t)},this.preventPipeOnWatch=function(){S=!1}}]).directive("stTable",function(){return{restrict:"A",controller:"stTableController",link:function(t,e,a,s){a.stSetFilter&&s.setFilterFunction(a.stSetFilter),a.stSetSort&&s.setSortFunction(a.stSetSort)}}}),t.module("smart-table").directive("stSearch",["stConfig","$timeout","$parse",function(t,e,a){return{require:"^stTable",link:function(s,n,i,r){var l=r,c=null,o=i.stDelay||t.search.delay,u=i.stInputEvent||t.search.inputEvent;i.$observe("stSearch",function(t,e){var a=n[0].value;t!==e&&a&&(r.tableState().search={},l.search(a,t))}),s.$watch(function(){return r.tableState().search},function(t){var e=i.stSearch||"$";t.predicateObject&&a(e)(t.predicateObject)!==n[0].value&&(n[0].value=a(e)(t.predicateObject)||"")},!0),n.bind(u,function(t){t=t.originalEvent||t,null!==c&&e.cancel(c),c=e(function(){l.search(t.target.value,i.stSearch||""),c=null},o)})}}}]),t.module("smart-table").directive("stSelectRow",["stConfig",function(t){return{restrict:"A",require:"^stTable",scope:{row:"=stSelectRow"},link:function(e,a,s,n){var i=s.stSelectMode||t.select.mode;a.bind("click",function(){e.$apply(function(){n.select(e.row,i)})}),e.$watch("row.isSelected",function(e){e===!0?a.addClass(t.select.selectedClass):a.removeClass(t.select.selectedClass)})}}}]),t.module("smart-table").directive("stSort",["stConfig","$parse",function(a,s){return{restrict:"A",require:"^stTable",link:function(n,i,r,l){function c(){g++,u=t.isFunction(p(n))?p(n):r.stSort,g%3===0&&!!h!=!0?(g=0,l.tableState().sort={},l.tableState().pagination.start=0,l.pipe()):l.sortBy(u,g%2===0)}var o,u=r.stSort,p=s(u),g=0,f=r.stClassAscent||a.sort.ascentClass,d=r.stClassDescent||a.sort.descentClass,m=[f,d],h=r.stSkipNatural!==e?r.stSkipNatural:a.sort.skipNatural;r.stSortDefault&&(o=n.$eval(r.stSortDefault)!==e?n.$eval(r.stSortDefault):r.stSortDefault),i.bind("click",function(){u&&n.$apply(c)}),o&&(g="reverse"===o?1:0,c()),n.$watch(function(){return l.tableState().sort},function(t){t.predicate!==u?(g=0,i.removeClass(f).removeClass(d)):(g=t.reverse===!0?2:1,i.removeClass(m[g%2]).addClass(m[g-1]))},!0)}}}]),t.module("smart-table").directive("stPagination",["stConfig",function(t){return{restrict:"EA",require:"^stTable",scope:{stItemsByPage:"=?",stDisplayedPages:"=?",stPageChange:"&"},templateUrl:function(e,a){return a.stTemplate?a.stTemplate:t.pagination.template},link:function(e,a,s,n){function i(){var t,a,s=n.tableState().pagination,i=1,r=e.currentPage;for(e.totalItemCount=s.totalItemCount,e.currentPage=Math.floor(s.start/s.number)+1,i=Math.max(i,e.currentPage-Math.abs(Math.floor(e.stDisplayedPages/2))),t=i+e.stDisplayedPages,t>s.numberOfPages&&(t=s.numberOfPages+1,i=Math.max(1,t-e.stDisplayedPages)),e.pages=[],e.numPages=s.numberOfPages,a=i;t>a;a++)e.pages.push(a);r!==e.currentPage&&e.stPageChange({newPage:e.currentPage})}e.stItemsByPage=e.stItemsByPage?+e.stItemsByPage:t.pagination.itemsByPage,e.stDisplayedPages=e.stDisplayedPages?+e.stDisplayedPages:t.pagination.displayedPages,e.currentPage=1,e.pages=[],e.$watch(function(){return n.tableState().pagination},i,!0),e.$watch("stItemsByPage",function(t,a){t!==a&&e.selectPage(1)}),e.$watch("stDisplayedPages",i),e.selectPage=function(t){t>0&&t<=e.numPages&&n.slice((t-1)*e.stItemsByPage,e.stItemsByPage)},n.tableState().pagination.number||n.slice(0,e.stItemsByPage)}}}]),t.module("smart-table").directive("stPipe",["stConfig","$timeout",function(e,a){return{require:"stTable",scope:{stPipe:"="},link:{pre:function(s,n,i,r){var l=null;t.isFunction(s.stPipe)&&(r.preventPipeOnWatch(),r.pipe=function(){return null!==l&&a.cancel(l),l=a(function(){s.stPipe(r.tableState(),r)},e.pipe.delay)})},post:function(t,e,a,s){s.pipe()}}}}])}(angular);
+//# sourceMappingURL=smart-table.min.js.map
\ No newline at end of file
diff --git a/eucaconsole/static/sass/eucaconsole.scss b/eucaconsole/static/sass/eucaconsole.scss
index df6e8252c..f423dd42a 100644
--- a/eucaconsole/static/sass/eucaconsole.scss
+++ b/eucaconsole/static/sass/eucaconsole.scss
@@ -1051,6 +1051,17 @@ table.table {
td.breakall {
word-break: break-all !important;
}
+ &.smart-table {
+ th[st-sort]:hover {
+ cursor: pointer;
+ }
+ .st-sort-ascent:before {
+ content: '\25B2';
+ }
+ .st-sort-descent:before {
+ content: '\25BC';
+ }
+ }
}
diff --git a/eucaconsole/templates/macros.pt b/eucaconsole/templates/macros.pt
index 85fb00f69..c259378de 100644
--- a/eucaconsole/templates/macros.pt
+++ b/eucaconsole/templates/macros.pt
@@ -19,12 +19,13 @@
-