Handle named servers in UserSpawnHandler, home, admin pages

Made CSS and HTML (and Jinja2) of admin page compatible with named servers.
This commit is contained in:
William Krinsman
2018-08-15 15:11:43 -07:00
committed by Min RK
parent 571ca2dec6
commit 02cb5ec076
9 changed files with 388 additions and 66 deletions

View File

@@ -20,7 +20,7 @@ from sqlalchemy.exc import SQLAlchemyError
from tornado.log import app_log from tornado.log import app_log
from tornado.httputil import url_concat, HTTPHeaders from tornado.httputil import url_concat, HTTPHeaders
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tornado.web import RequestHandler from tornado.web import RequestHandler, MissingArgumentError
from tornado import gen, web from tornado import gen, web
from .. import __version__ from .. import __version__
@@ -854,10 +854,10 @@ class BaseHandler(RequestHandler):
await self.proxy.delete_user(user, server_name) await self.proxy.delete_user(user, server_name)
await user.stop(server_name) await user.stop(server_name)
async def stop_single_user(self, user, name=''): async def stop_single_user(self, user, server_name=''):
if name not in user.spawners: if server_name not in user.spawners:
raise KeyError("User %s has no such spawner %r", user.name, name) raise KeyError("User %s has no such spawner %r", user.name, server_name)
spawner = user.spawners[name] spawner = user.spawners[server_name]
if spawner.pending: if spawner.pending:
raise RuntimeError("%s pending %s" % (spawner._log_name, spawner.pending)) raise RuntimeError("%s pending %s" % (spawner._log_name, spawner.pending))
# set user._stop_pending before doing anything async # set user._stop_pending before doing anything async
@@ -873,8 +873,8 @@ class BaseHandler(RequestHandler):
""" """
tic = IOLoop.current().time() tic = IOLoop.current().time()
try: try:
await self.proxy.delete_user(user, name) await self.proxy.delete_user(user, server_name)
await user.stop(name) await user.stop(server_name)
finally: finally:
spawner._stop_pending = False spawner._stop_pending = False
toc = IOLoop.current().time() toc = IOLoop.current().time()
@@ -885,7 +885,7 @@ class BaseHandler(RequestHandler):
await gen.with_timeout(timedelta(seconds=self.slow_stop_timeout), stop()) await gen.with_timeout(timedelta(seconds=self.slow_stop_timeout), stop())
except gen.TimeoutError: except gen.TimeoutError:
# hit timeout, but stop is still pending # hit timeout, but stop is still pending
self.log.warning("User %s:%s server is slow to stop", user.name, name) self.log.warning("User %s:%s server is slow to stop", user.name, server_name)
#--------------------------------------------------------------- #---------------------------------------------------------------
# template rendering # template rendering
@@ -1019,7 +1019,7 @@ class PrefixRedirectHandler(BaseHandler):
class UserSpawnHandler(BaseHandler): class UserSpawnHandler(BaseHandler):
"""Redirect requests to /user/name/* handled by the Hub. """Redirect requests to /user/user_name/* handled by the Hub.
If logged in, spawn a single-user server and redirect request. If logged in, spawn a single-user server and redirect request.
If a user, alice, requests /user/bob/notebooks/mynotebook.ipynb, If a user, alice, requests /user/bob/notebooks/mynotebook.ipynb,
@@ -1035,21 +1035,21 @@ class UserSpawnHandler(BaseHandler):
self.write(json.dumps({"message": "%s is not running" % user.name})) self.write(json.dumps({"message": "%s is not running" % user.name}))
self.finish() self.finish()
async def get(self, name, user_path): async def get(self, user_name, user_path):
if not user_path: if not user_path:
user_path = '/' user_path = '/'
current_user = self.current_user current_user = self.current_user
if ( if (
current_user current_user
and current_user.name != name and current_user.name != user_name
and current_user.admin and current_user.admin
and self.settings.get('admin_access', False) and self.settings.get('admin_access', False)
): ):
# allow admins to spawn on behalf of users # allow admins to spawn on behalf of users
user = self.find_user(name) user = self.find_user(user_name)
if user is None: if user is None:
# no such user # no such user
raise web.HTTPError(404, "No such user %s" % name) raise web.HTTPError(404, "No such user %s" % user_name)
self.log.info("Admin %s requesting spawn on behalf of %s", self.log.info("Admin %s requesting spawn on behalf of %s",
current_user.name, user.name) current_user.name, user.name)
admin_spawn = True admin_spawn = True
@@ -1059,7 +1059,7 @@ class UserSpawnHandler(BaseHandler):
admin_spawn = False admin_spawn = False
# For non-admins, we should spawn if the user matches # For non-admins, we should spawn if the user matches
# otherwise redirect users to their own server # otherwise redirect users to their own server
should_spawn = (current_user and current_user.name == name) should_spawn = (current_user and current_user.name == user_name)
if "api" in user_path.split("/") and not user.active: if "api" in user_path.split("/") and not user.active:
# API request for not-running server (e.g. notebook UI left open) # API request for not-running server (e.g. notebook UI left open)
@@ -1071,7 +1071,7 @@ class UserSpawnHandler(BaseHandler):
# if spawning fails for any reason, point users to /hub/home to retry # if spawning fails for any reason, point users to /hub/home to retry
self.extra_error_html = self.spawn_home_error self.extra_error_html = self.spawn_home_error
# If people visit /user/:name directly on the Hub, # If people visit /user/:user_name directly on the Hub,
# the redirects will just loop, because the proxy is bypassed. # the redirects will just loop, because the proxy is bypassed.
# Try to check for that and warn, # Try to check for that and warn,
# though the user-facing behavior is unchanged # though the user-facing behavior is unchanged
@@ -1087,7 +1087,11 @@ class UserSpawnHandler(BaseHandler):
""", self.request.full_url(), self.proxy.public_url) """, self.request.full_url(), self.proxy.public_url)
# logged in as valid user, check for pending spawn # logged in as valid user, check for pending spawn
spawner = user.spawner if self.allow_named_servers:
server_name = self.get_argument('server', '')
else:
server_name = ''
spawner = user.spawners[server_name]
# First, check for previous failure. # First, check for previous failure.
if ( if (
@@ -1152,7 +1156,7 @@ class UserSpawnHandler(BaseHandler):
{'next': self.request.uri})) {'next': self.request.uri}))
return return
else: else:
await self.spawn_single_user(user) await self.spawn_single_user(user, server_name)
# spawn didn't finish, show pending page # spawn didn't finish, show pending page
if spawner.pending: if spawner.pending:
@@ -1208,7 +1212,7 @@ class UserSpawnHandler(BaseHandler):
url_parts = urlparse(target) url_parts = urlparse(target)
query_parts = parse_qs(url_parts.query) query_parts = parse_qs(url_parts.query)
query_parts['redirects'] = redirects + 1 query_parts['redirects'] = redirects + 1
url_parts = url_parts._replace(query=urlencode(query_parts)) url_parts = url_parts._replace(query=urlencode(query_parts, doseq=True))
target = urlunparse(url_parts) target = urlunparse(url_parts)
else: else:
target = url_concat(target, {'redirects': 1}) target = url_concat(target, {'redirects': 1})
@@ -1276,7 +1280,7 @@ class AddSlashHandler(BaseHandler):
default_handlers = [ default_handlers = [
(r'', AddSlashHandler), # add trailing / to `/hub` (r'', AddSlashHandler), # add trailing / to `/hub`
(r'/user/([^/]+)(/.*)?', UserSpawnHandler), (r'/user/(?P<user_name>[^/]+)(?P<user_path>/.*)?', UserSpawnHandler),
(r'/user-redirect/(.*)?', UserRedirectHandler), (r'/user-redirect/(.*)?', UserRedirectHandler),
(r'/security/csp-report', CSPReportHandler), (r'/security/csp-report', CSPReportHandler),
] ]

View File

@@ -50,13 +50,18 @@ class HomeHandler(BaseHandler):
# trigger poll_and_notify event in case of a server that died # trigger poll_and_notify event in case of a server that died
await user.spawner.poll_and_notify() await user.spawner.poll_and_notify()
# send the user to /spawn if they aren't running or pending a spawn, # send the user to /spawn if they have no active servers,
# to establish that this is an explicit spawn request rather # to establish that this is an explicit spawn request rather
# than an implicit one, which can be caused by any link to `/user/:name` # than an implicit one, which can be caused by any link to `/user/:name(/:server_name)`
url = user.url if user.spawner.active else url_path_join(self.hub.base_url, 'spawn') url = url_path_join(self.hub.base_url, 'user', user.name) if user.active else url_path_join(self.hub.base_url, 'spawn')
html = self.render_template('home.html', html = self.render_template('home.html',
user=user, user=user,
url=url, url=url,
allow_named_servers=self.allow_named_servers,
url_path_join=url_path_join,
# can't use user.spawners because the stop method of User pops named servers from user.spawners when they're stopped
spawners = user.orm_user._orm_spawners,
default_server = user.spawner,
) )
self.finish(html) self.finish(html)
@@ -214,11 +219,12 @@ class AdminHandler(BaseHandler):
running = [ u for u in users if u.running ] running = [ u for u in users if u.running ]
html = self.render_template('admin.html', html = self.render_template('admin.html',
user=self.current_user, current_user=self.current_user,
admin_access=self.settings.get('admin_access', False), admin_access=self.settings.get('admin_access', False),
users=users, users=users,
running=running, running=running,
sort={s:o for s,o in zip(sorts, orders)}, sort={s:o for s,o in zip(sorts, orders)},
allow_named_servers=self.allow_named_servers,
) )
self.finish(html) self.finish(html)

View File

@@ -182,19 +182,19 @@ class User:
await self.save_auth_state(auth_state) await self.save_auth_state(auth_state)
return auth_state return auth_state
def _new_spawner(self, name, spawner_class=None, **kwargs): def _new_spawner(self, server_name, spawner_class=None, **kwargs):
"""Create a new spawner""" """Create a new spawner"""
if spawner_class is None: if spawner_class is None:
spawner_class = self.spawner_class spawner_class = self.spawner_class
self.log.debug("Creating %s for %s:%s", spawner_class, self.name, name) self.log.debug("Creating %s for %s:%s", spawner_class, self.name, server_name)
orm_spawner = self.orm_spawners.get(name) orm_spawner = self.orm_spawners.get(server_name)
if orm_spawner is None: if orm_spawner is None:
orm_spawner = orm.Spawner(user=self.orm_user, name=name) orm_spawner = orm.Spawner(user=self.orm_user, name=server_name)
self.db.add(orm_spawner) self.db.add(orm_spawner)
self.db.commit() self.db.commit()
assert name in self.orm_spawners assert server_name in self.orm_spawners
if name == '' and self.state: if server_name == '' and self.state:
# migrate user.state to spawner.state # migrate user.state to spawner.state
orm_spawner.state = self.state orm_spawner.state = self.state
self.state = None self.state = None
@@ -202,15 +202,15 @@ class User:
# use fully quoted name for client_id because it will be used in cookie-name # use fully quoted name for client_id because it will be used in cookie-name
# self.escaped_name may contain @ which is legal in URLs but not cookie keys # self.escaped_name may contain @ which is legal in URLs but not cookie keys
client_id = 'jupyterhub-user-%s' % quote(self.name) client_id = 'jupyterhub-user-%s' % quote(self.name)
if name: if server_name:
client_id = '%s-%s' % (client_id, quote(name)) client_id = '%s-%s' % (client_id, quote(server_name))
spawn_kwargs = dict( spawn_kwargs = dict(
user=self, user=self,
orm_spawner=orm_spawner, orm_spawner=orm_spawner,
hub=self.settings.get('hub'), hub=self.settings.get('hub'),
authenticator=self.authenticator, authenticator=self.authenticator,
config=self.settings.get('config'), config=self.settings.get('config'),
proxy_spec=url_path_join(self.proxy_spec, name, '/'), proxy_spec=url_path_join(self.proxy_spec, server_name, '/'),
db=self.db, db=self.db,
oauth_client_id=client_id, oauth_client_id=client_id,
cookie_options = self.settings.get('cookie_options', {}), cookie_options = self.settings.get('cookie_options', {}),

View File

@@ -62,24 +62,50 @@ require(["jquery", "bootstrap", "moment", "jhapi", "utils"], function ($, bs, mo
el.text(m.isValid() ? m.fromNow() : "Never"); el.text(m.isValid() ? m.fromNow() : "Never");
}); });
$(".stop-server").click(function () { $(".stop-server.default-server").click(function () {
var el = $(this); var el = $(this);
var row = get_row(el); var row = get_row(el);
var user = row.data('user'); var user = row.data('user');
el.text("stopping..."); el.text("stopping...");
api.stop_server(user, { api.stop_server(user, {
success: function () { success: function () {
el.text('stop server').addClass('hidden'); if (el.data("named_servers") === true) {
row.find('.access-server').addClass('hidden'); el.text("stop default server").addClass("hidden");
row.find('.start-server').removeClass('hidden'); } else {
} el.text("stop server").addClass("hidden");
}
row.find(".access-server").addClass("hidden");
row.find(".start-server").removeClass("hidden");
},
}); });
}); });
$(".access-server").map(function (i, el) { $(".stop-server[id^='stop-']").click(function () {
var el = $(this);
var row = get_row(el);
var user = row.data("user");
var server_name = (this.id).replace(/^(stop-)/, "");
el.text("stopping...");
api.stop_named_server(user, server_name, {
success: function() {
el.text("stop "+server_name).addClass("hidden");
row.find(".access-server").addClass("hidden");
row.find(".start-server").removeClass("hidden");
},
});
});
$(".access-server.default-server").map(function (i, el) {
el = $(el); el = $(el);
var user = get_row(el).data('user'); var user = get_row(el).data("user");
el.attr('href', utils.url_path_join(prefix, 'user', user) + '/'); el.attr("href", utils.url_path_join(prefix, "user", user) + "/");
});
$(".access-server[id^='access-']").map(function (i, el) {
el = $(el);
var user = get_row(el).data("user");
var server_name = (this.id).replace(/^(access-)/, "");
el.attr("href", utils.url_path_join(prefix, "user", user, server_name) + "/" );
}); });
if (admin_access && options_form) { if (admin_access && options_form) {
@@ -94,16 +120,35 @@ require(["jquery", "bootstrap", "moment", "jhapi", "utils"], function ($, bs, mo
// since it would mean opening a bunch of tabs // since it would mean opening a bunch of tabs
$('#start-all-servers').addClass('hidden'); $('#start-all-servers').addClass('hidden');
} else { } else {
$(".start-server").click(function () { $(".start-server.default-server").click(function () {
var el = $(this); var el = $(this);
var row = get_row(el); var row = get_row(el);
var user = row.data('user'); var user = row.data('user');
el.text("starting..."); el.text("starting...");
api.start_server(user, { api.start_server(user, {
success: function () { success: function () {
el.text('start server').addClass('hidden'); if (el.data("named_servers") === true ){
row.find('.stop-server').removeClass('hidden'); el.text("start default server").addClass("hidden");
row.find('.access-server').removeClass('hidden'); } else {
el.text("start server").addClass("hidden");
}
row.find(".stop-server").removeClass("hidden");
row.find(".access-server").removeClass("hidden");
}
});
});
$(".start-server[id^='start-']").click(function () {
var el = $(this);
var row = get_row(el);
var user = row.data("user");
var server_name = (this.id).replace(/^(start-)/, "");
el.text("starting...");
api.start_named_server(user, server_name, {
success: function () {
el.text("start "+server_name).addClass("hidden");
row.find(".stop-server").removeClass("hidden");
row.find(".access-server").removeClass("hidden");
} }
}); });
}); });

View File

@@ -17,13 +17,156 @@ require(["jquery", "jhapi"], function($, JHAPI) {
}); });
api.stop_server(user, { api.stop_server(user, {
success: function() { success: function() {
if ($("#start").data("named_servers") === true) {
$("#start")
.text("Start Default Server")
.attr("title", "Start the default server")
.attr("disabled", false)
.off("click");
} else {
$("#start") $("#start")
.text("Start My Server") .text("Start My Server")
.attr("title", "Start your server") .attr("title", "Start your server")
.attr("disabled", false) .attr("disabled", false)
.off("click"); .off("click");
$("#stop").hide(); $("#stop").hide();
} }
},
}); });
}); });
//// Named servers buttons
function stop() {
let server_name = (this.id).replace(/^(stop-)/, "");
// before request
$("#stop-"+server_name)
.attr("disabled", true)
.off("click")
.click(function() {
return false;});
$("#goto-"+server_name)
.attr("disabled", true)
.off("click")
.click(function() {
return false;});
// request
api.stop_named_server(user, server_name, {
success: function() {
// after request --> establish final local state
$("#stop-"+server_name)
.data("state", false)
.hide()
$("#goto-"+server_name)
.data("state", false)
.hide()
$("#start-"+server_name)
.data("state", false)
.show()
.attr("disabled", false)
.off("click")
.click(start);
},
});
};
function goto() {
let server_name = (this.id).replace(/^(goto-)/, "");
// before request
$("#stop-"+server_name)
.attr("disabled", true)
.off("click")
.click(function() {
return false;});
$("#goto-"+server_name)
.attr("disabled", true)
.off("click")
.click(function() {
return false;});
window.location = base_url+"/user/"+user+"/"+server_name+"/tree"; }
function start() {
let server_name = (this.id).replace(/^(start-)/, "");
// before request
$("#start-"+server_name)
.attr("disabled", true)
.off("click")
.click(function() {
return false;});
// request
api.start_named_server(user, server_name, {
success: function() {
// after request --> establish final local state
$("#stop-"+server_name)
.data("state", true)
.show()
.attr("disabled", false)
.off("click")
.click(stop);
$("#goto-"+server_name)
.data("state", true)
.show()
.attr("disabled", false)
.off("click")
.click(goto);
$("#start-"+server_name)
.data("state", true)
.hide()
}});};
// Initial state: TRUE (server running) -- local state transitions occur by clicking stop
// - stop visible
// - goto visible
// - start invisible
$("[data-state='true'][id^='stop-']")
.show()
.attr("disabled", false)
.click(stop);
$("[data-state='true'][id^='goto-']")
.show()
.attr("disabled", false)
.click(goto);
$("[data-state='true'][id^='start-']")
.hide()
.attr("disabled", true)
.off("click")
.click(function() {
return false;});
// Initial state: FALSE (server not running) -- local state transitions occur by clicking start
// - stop invisible
// - goto invisible
// - start visible
$("[data-state='false'][id^='stop-']")
.hide()
.attr("disabled", true)
.off("click")
.click(function() {
return false;});
$("[data-state='false'][id^='goto-']")
.hide()
.attr("disabled", true)
.off("click")
.click(function() {
return false;});
$("[data-state='false'][id^='start-']")
.show()
.attr("disabled", false)
.click(start);
}); });

View File

@@ -52,6 +52,15 @@ define(['jquery', 'utils'], function ($, utils) {
); );
}; };
JHAPI.prototype.start_named_server = function (user, server_name, options) {
options = options || {};
options = update(options, {type: 'POST', dataType: null});
this.api_request(
utils.url_path_join('users', user, 'servers', server_name),
options
);
};
JHAPI.prototype.stop_server = function (user, options) { JHAPI.prototype.stop_server = function (user, options) {
options = options || {}; options = options || {};
options = update(options, {type: 'DELETE', dataType: null}); options = update(options, {type: 'DELETE', dataType: null});
@@ -61,6 +70,15 @@ define(['jquery', 'utils'], function ($, utils) {
); );
}; };
JHAPI.prototype.stop_named_server = function (user, server_name, options) {
options = options || {};
options = update(options, {type: 'DELETE', dataType: null});
this.api_request(
utils.url_path_join('users', user, 'servers', server_name),
options
);
};
JHAPI.prototype.list_users = function (options) { JHAPI.prototype.list_users = function (options) {
this.api_request('users', options); this.api_request('users', options);
}; };

View File

@@ -1,3 +1,3 @@
i.sort-icon { i.sort-icon {
margin-left: 4px; margin-left: 4px;
} }

View File

@@ -40,37 +40,87 @@
<a id="shutdown-hub" role="button" class="col-xs-2 col-xs-offset-1 btn btn-danger">Shutdown Hub</a> <a id="shutdown-hub" role="button" class="col-xs-2 col-xs-offset-1 btn btn-danger">Shutdown Hub</a>
</td> </td>
</tr> </tr>
{% for u in users %} {% for user in users %}
<tr class="user-row" data-user="{{u.name}}" data-admin="{{u.admin}}"> <tr class="user-row{% if allow_named_servers %} default-server-row{% endif %}" id="user-{{user.name}}" data-user="{{ user.name }}" data-admin="{{user.admin}}">
{% block user_row scoped %} {% block user_row scoped %}
<td class="name-col col-sm-2">{{u.name}}</td> <td class="name-col col-sm-2">{{user.name}}</td>
<td class="admin-col col-sm-2">{% if u.admin %}admin{% endif %}</td> <td class="admin-col col-sm-2">{% if user.admin %}admin{% endif %}</td>
<td class="time-col col-sm-3"> <td class="time-col col-sm-3">
{%- if u.last_activity -%} {%- if user.last_activity -%}
{{ u.last_activity.isoformat() + 'Z' }} {{ user.last_activity.isoformat() + 'Z' }}
{%- else -%} {%- else -%}
Never Never
{%- endif -%} {%- endif -%}
</td> </td>
<td class="server-col col-sm-2 text-center"> <td class="server-col col-sm-2 text-center">
<a role="button" class="stop-server btn btn-xs btn-danger {% if not u.running %}hidden{% endif %}">stop server</a> <a role="button" class="stop-server btn btn-xs btn-danger{% if not user.running %} hidden{% endif %} default-server" data-named_servers={% if allow_named_servers %}"true"{% else %}"false"{% endif %}>
<a role="button" class="start-server btn btn-xs btn-primary {% if u.running %}hidden{% endif %}">start server</a> {% if allow_named_servers %}
stop default server
{% else %}
stop server
{% endif %}
</a>
<a role="button" class="start-server btn btn-xs btn-primary{% if user.running %} hidden{% endif %} default-server" data-named_servers={% if allow_named_servers %}"true"{% else %}"false"{% endif %}>
{% if allow_named_servers %}
start default server
{% else %}
start server</a>
{% endif %}
</td> </td>
<td class="server-col col-sm-1 text-center"> <td class="server-col col-sm-1 text-center">
{% if admin_access %} {% if admin_access %}
<a role="button" class="access-server btn btn-xs btn-primary {% if not u.running %}hidden{% endif %}">access server</a> <a role="button" class="access-server btn btn-xs btn-primary{% if not user.running %} hidden{% endif %} default-server" data-named_servers={% if allow_named_servers %}"true"{% else %}"false"{% endif %}>
{% if allow_named_servers %}
access default server
{% else %}
access server
{% endif %}
</a>
{% endif %} {% endif %}
</td> </td>
<td class="edit-col col-sm-1 text-center"> <td class="edit-col col-sm-1 text-center">
<a role="button" class="edit-user btn btn-xs btn-primary">edit</a> <a role="button" class="edit-user btn btn-xs btn-primary">edit{% if allow_named_servers %} {{ user.name }}{% endif %}</a>
</td> </td>
<td class="edit-col col-sm-1 text-center"> <td class="edit-col col-sm-1 text-center">
{% if u.name != user.name %} {% if user.name != current_user.name %}
<a role="button" class="delete-user btn btn-xs btn-danger">delete</a> <a role="button" class="delete-user btn btn-xs btn-danger">delete{% if allow_named_servers %} {{ user.name }}{% endif %}</a>
{% endif %} {% endif %}
</td> </td>
</tr>
{% if allow_named_servers %}
{% for spawner in user.orm_user._orm_spawners %}
{% if spawner.name %}
<tr class="user-row" data-user="{{user.name}}" data-admin="{{user.admin}}">
{# {% block user_row scoped %} #}
<td class="name-col col-sm-2"> </td>
<td class="admin-col col-sm-2"> </td>
<td class="time-col col-sm-3">
{%- if spawner.last_activity -%}
{{ spawner.last_activity.isoformat() + 'Z' }}
{%- else -%}
Never
{%- endif -%}
</td>
<td class="server-col col-sm-2 text-center">
<a role="button" class="stop-server btn btn-xs btn-danger{% if not spawner.state %} hidden{% endif %}" id="stop-{{ spawner.name }}">stop {{ spawner.name }}</a>
<a role="button" class="start-server btn btn-xs btn-primary{% if spawner.state %} hidden{% endif %}" id="start-{{ spawner.name }}">start {{ spawner.name }}</a>
</td>
<td class="server-col col-sm-1 text-center">
{% if admin_access %}
<a role="button" class="access-server btn btn-xs btn-primary{% if not spawner.state %} hidden{% endif %}" id="access-{{ spawner.name }}">access server</a>
{% endif %}
</td>
<td class="edit-col col-sm-1 text-center">
<!-- edit user button empty for named servers -->
</td>
<td class="edit-col col-sm-1 text-center">
<!-- delete user button empty for named servers -->
</td>
</tr>
{% endif %}
{% endfor %}
{% endif %}
{% endblock user_row %} {% endblock user_row %}
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@@ -6,20 +6,76 @@
{% block main %} {% block main %}
<div class="container"> <div class="container">
{% if user.active %}
<div class="row" style="margin: 1%">
<div class="text-center">
{% if default_server.active %}
<a id="stop" role="button" class="btn btn-lg btn-danger">
{% if allow_named_servers %}
Stop Default Server
{% else %}
Stop My Server
{% endif %}
</a>
{% if allow_named_servers %}
<a id="start" role="button" class="btn btn-lg btn-primary" href="{{ url }}" data-named_servers="true">
Default Server
{% else %}
<a id="start" role="button" class="btn btn-lg btn-primary" href="{{ url }}" data-named_servers="false">
My Server
{% endif %}
</a>
{% else %}
<a id="start" role="button" class="btn btn-lg btn-primary" href="{{ url }}">
Start Default Server
</a>
{% endif %}
</div>
</div>
{% for spawner in spawners %}
{% if spawner.name %}
<div class="row" style="margin: 1%">
<div class="text-center">
{% if spawner.state %}
<a id="stop-{{ spawner.name }}" role="button" class="btn btn-lg btn-danger" data-state="true">
Stop {{ spawner.name }}
</a>
<a id="goto-{{ spawner.name }}" role="button" class="btn btn-lg btn-primary" href="{{ url_path_join(url, spawner.name) }}" data-state="true">
{{ spawner.name }}
</a>
<a id="start-{{ spawner.name }}" role="button" class="btn btn-lg btn-primary" data-state="true" style="display: none">
Start {{ spawner.name }}
</a>
{% else %}
<a id="stop-{{ spawner.name }}" role="button" class="btn btn-lg btn-danger" data-state="false" style="display: none">
Stop {{ spawner.name }}
</a>
<a id="goto-{{ spawner.name }}" role="button" class="btn btn-lg btn-primary" href="{{ url_path_join(url, spawner.name) }}" data-state="false" style="display: none">
{{ spawner.name }}
</a>
<a id="start-{{ spawner.name }}" role="button" class="btn btn-lg btn-primary" data-state="false">
Start {{ spawner.name }}
</a>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
{% else %}
<div class="row"> <div class="row">
<div class="text-center"> <div class="text-center">
{% if user.running %}
<a id="stop" role="button" class="btn btn-lg btn-danger">Stop My Server</a>
{% endif %}
<a id="start" role="button" class="btn btn-lg btn-primary" href="{{ url }}"> <a id="start" role="button" class="btn btn-lg btn-primary" href="{{ url }}">
{% if not user.active %} {% if allow_named_servers %}
Start Start a New Server
{% else %}
Start My Server
{% endif %} {% endif %}
My Server
</a> </a>
</div> </div>
</div> </div>
</div> {% endif %}
{% endblock %} {% endblock %}