Repository: jupyterhub/jupyterhub

Issue:
Admin UI suggestions
As I've been using the admin UI a bit for my class, I just had a few things that I thought would be nice to have/change in it:
- move the "add user" button to the top -- it is annoying with lots of users to have to scroll all the way down to find it
- add some simple statistics at the top of the page: number of running servers, number of currently active users, etc.
- it would be awesome to be able to sort users by the different columns -- admin, alphabetically, by last seen
- currently, if you shut down a user's server, it causes the page to refresh which often jumps you up to the top (or just somewhere else). It would be nice if the update could be done in the background without actually reloading the page.

Obviously, none of these are urgent, but I think they would make the admin experience a little easier. I can open separate issues for them if so desired.



Discussion:

Resolving patch:
diff --git a/jupyterhub/apihandlers/base.py b/jupyterhub/apihandlers/base.py
--- a/jupyterhub/apihandlers/base.py
+++ b/jupyterhub/apihandlers/base.py
@@ -42,7 +42,7 @@ def write_error(self, status_code, **kwargs):
             reason = getattr(exception, 'reason', '')
             if reason:
                 status_message = reason
-        
+        self.set_header('Content-Type', 'application/json')
         self.write(json.dumps({
             'status': status_code,
             'message': message or status_message,
diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py
--- a/jupyterhub/handlers/pages.py
+++ b/jupyterhub/handlers/pages.py
@@ -45,10 +45,52 @@ 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) ]
+        
+        users = self.db.query(orm.User).order_by(*ordered)
+        running = users.filter(orm.User.server != None)
+        
         html = self.render_template('admin.html',
             user=self.get_current_user(),
-            users=self.db.query(orm.User),
             admin_access=self.settings.get('admin_access', False),
+            users=users,
+            running=running,
+            sort={s:o for s,o in zip(sorts, orders)},
         )
         self.finish(html)
 


Test patch: