diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index b1cc258a..6ef98b71 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -45,10 +45,47 @@ class AdminHandler(BaseHandler): @admin_only def get(self): + available = {'name', 'admin', 'running', 'last_activity'} + default_sort = ['admin', 'name'] + mapping = { + 'running': '_server_id' + } + default_order = { + 'name': 'asc', + 'last_activity': 'desc', + 'admin': 'desc', + 'running': 'desc', + } + sorts = self.get_arguments('sort') or default_sort + orders = self.get_arguments('order') + + for bad in set(sorts).difference(available): + self.log.warn("ignoring invalid sort: %r", bad) + sorts.remove(bad) + for bad in set(orders).difference({'asc', 'desc'}): + self.log.warn("ignoring invalid order: %r", bad) + orders.remove(bad) + + # add default sort as secondary + for s in default_sort: + if s not in sorts: + sorts.append(s) + if len(orders) < len(sorts): + for col in sorts[len(orders):]: + orders.append(default_order[col]) + else: + orders = orders[:len(sorts)] + + # this could be one incomprehensible nested list comprehension + # get User columns + cols = [ getattr(orm.User, mapping.get(c, c)) for c in sorts ] + # get User.col.desc() order objects + ordered = [ getattr(c, o)() for c, o in zip(cols, orders) ] html = self.render_template('admin.html', user=self.get_current_user(), - users=self.db.query(orm.User), + users=self.db.query(orm.User).order_by(*ordered), admin_access=self.settings.get('admin_access', False), + sort={s:o for s,o in zip(sorts, orders)}, ) self.finish(html) diff --git a/share/jupyter/hub/static/js/admin.js b/share/jupyter/hub/static/js/admin.js index 22390345..d50edb08 100644 --- a/share/jupyter/hub/static/js/admin.js +++ b/share/jupyter/hub/static/js/admin.js @@ -9,12 +9,46 @@ require(["jquery", "bootstrap", "moment", "jhapi", "utils"], function ($, bs, mo var api = new JHAPI(base_url); - var get_row = function (element) { + function get_row (element) { while (!element.hasClass("user-row")) { element = element.parent(); } return element; - }; + } + + function resort (col, order) { + var query = window.location.search.slice(1).split('&'); + // if col already present in args, remove it + var i = 0; + while (i < query.length) { + if (query[i] === 'sort=' + col) { + query.splice(i,1); + if (query[i] && query[i].substr(0, 6) === 'order=') { + query.splice(i,1); + } + } else { + i += 1; + } + } + // add new order to the front + if (order) { + query.unshift('order=' + order); + } + query.unshift('sort=' + col); + // reload page with new order + window.location = window.location.pathname + '?' + query.join('&'); + } + + $("th").map(function (i, th) { + th = $(th); + var col = th.data('sort'); + var order = th.find('i').hasClass('fa-sort-desc') ? 'asc':'desc'; + th.find('a').click( + function () { + resort(col, order); + } + ) + }); $(".time-col").map(function (i, el) { // convert ISO datestamps to nice momentjs ones diff --git a/share/jupyter/hub/static/less/admin.less b/share/jupyter/hub/static/less/admin.less new file mode 100644 index 00000000..7283d91f --- /dev/null +++ b/share/jupyter/hub/static/less/admin.less @@ -0,0 +1,3 @@ +i.sort-icon { + margin-left: 4px; +} \ No newline at end of file diff --git a/share/jupyter/hub/static/less/style.less b/share/jupyter/hub/static/less/style.less index 9611fc0c..ce866e94 100644 --- a/share/jupyter/hub/static/less/style.less +++ b/share/jupyter/hub/static/less/style.less @@ -22,6 +22,7 @@ @import "./variables.less"; @import "./page.less"; +@import "./admin.less"; @import "./error.less"; @import "./logout.less"; @import "./login.less"; diff --git a/share/jupyter/hub/templates/admin.html b/share/jupyter/hub/templates/admin.html index 655a962b..c917ce5a 100644 --- a/share/jupyter/hub/templates/admin.html +++ b/share/jupyter/hub/templates/admin.html @@ -1,13 +1,28 @@ {% extends "page.html" %} +{% macro th(key, label, order, colspan) %} +{{label}} + + + +{% endmacro %} + {% block main %}
- - + {{ th('name', "User", sort.get('name'), 1) }} + {{ th('admin', "Admin", sort.get('admin'), 1) }} + {{ th('last_activity', "Last Seen", sort.get('last_activity'), 1) }} + {{ th('running', "Running", sort.get('running'), 2) }}
UserLast Seen