mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 10:34:10 +00:00
Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2985562c2f | ||
![]() |
754f850e95 | ||
![]() |
dccb85d225 | ||
![]() |
a0e401bc87 | ||
![]() |
c6885a2124 | ||
![]() |
7528fb7d9b | ||
![]() |
e7df5a299c | ||
![]() |
ff997bbce5 | ||
![]() |
1e21e00e1a | ||
![]() |
77d3ee98f9 | ||
![]() |
1f861b2c90 | ||
![]() |
14a00e67b4 | ||
![]() |
14f63c168d | ||
![]() |
e70dbb3d32 | ||
![]() |
b679275a68 | ||
![]() |
0c1478a67e | ||
![]() |
d26e2346a2 | ||
![]() |
9a09c841b9 | ||
![]() |
f1d4f5a733 | ||
![]() |
d970dd4c89 | ||
![]() |
f3279bf849 | ||
![]() |
db0878a495 | ||
![]() |
c9b1042791 | ||
![]() |
cd81320d8f |
16
README.md
16
README.md
@@ -6,7 +6,7 @@ Questions, comments? Visit our Google Group:
|
|||||||
[](https://travis-ci.org/jupyterhub/jupyterhub)
|
[](https://travis-ci.org/jupyterhub/jupyterhub)
|
||||||
[](https://circleci.com/gh/jupyterhub/jupyterhub)
|
[](https://circleci.com/gh/jupyterhub/jupyterhub)
|
||||||
[](http://jupyterhub.readthedocs.org/en/latest/?badge=latest)
|
[](http://jupyterhub.readthedocs.org/en/latest/?badge=latest)
|
||||||
[](https://codecov.io/github/jupyter/jupyterhub?branch=master)
|
[](https://codecov.io/github/jupyterhub/jupyterhub?branch=master)
|
||||||
|
|
||||||
|
|
||||||
JupyterHub, a multi-user server, manages and proxies multiple instances of the single-user <del>IPython</del> Jupyter notebook server.
|
JupyterHub, a multi-user server, manages and proxies multiple instances of the single-user <del>IPython</del> Jupyter notebook server.
|
||||||
@@ -67,7 +67,7 @@ Jupyter ~~IPython~~ notebook:
|
|||||||
|
|
||||||
For a development install, clone the repository and then install from source:
|
For a development install, clone the repository and then install from source:
|
||||||
|
|
||||||
git clone https://github.com/jupyter/jupyterhub
|
git clone https://github.com/jupyterhub/jupyterhub
|
||||||
cd jupyterhub
|
cd jupyterhub
|
||||||
pip3 install -r dev-requirements.txt -e .
|
pip3 install -r dev-requirements.txt -e .
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ and then visit `http://localhost:8000`, and sign in with your unix credentials.
|
|||||||
|
|
||||||
To allow multiple users to sign into the server, you will need to
|
To allow multiple users to sign into the server, you will need to
|
||||||
run the `jupyterhub` command as a *privileged user*, such as root.
|
run the `jupyterhub` command as a *privileged user*, such as root.
|
||||||
The [wiki](https://github.com/jupyter/jupyterhub/wiki/Using-sudo-to-run-JupyterHub-without-root-privileges)
|
The [wiki](https://github.com/jupyterhub/jupyterhub/wiki/Using-sudo-to-run-JupyterHub-without-root-privileges)
|
||||||
describes how to run the server as a *less privileged user*, which requires more
|
describes how to run the server as a *less privileged user*, which requires more
|
||||||
configuration of the system.
|
configuration of the system.
|
||||||
|
|
||||||
@@ -116,13 +116,13 @@ The authentication and process spawning mechanisms can be replaced,
|
|||||||
which should allow plugging into a variety of authentication or process control environments.
|
which should allow plugging into a variety of authentication or process control environments.
|
||||||
Some examples, meant as illustration and testing of this concept:
|
Some examples, meant as illustration and testing of this concept:
|
||||||
|
|
||||||
- Using GitHub OAuth instead of PAM with [OAuthenticator](https://github.com/jupyter/oauthenticator)
|
- Using GitHub OAuth instead of PAM with [OAuthenticator](https://github.com/jupyterhub/oauthenticator)
|
||||||
- Spawning single-user servers with Docker, using the [DockerSpawner](https://github.com/jupyter/dockerspawner)
|
- Spawning single-user servers with Docker, using the [DockerSpawner](https://github.com/jupyterhub/dockerspawner)
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
There is a ready to go [docker image for JupyterHub](https://hub.docker.com/r/jupyter/jupyterhub/).
|
There is a ready to go [docker image for JupyterHub](https://hub.docker.com/r/jupyterhub/jupyterhub/).
|
||||||
[Note: This `jupyter/jupyterhub` docker image is only an image for running the Hub service itself.
|
[Note: This `jupyterhub/jupyterhub` docker image is only an image for running the Hub service itself.
|
||||||
It does not require the other Jupyter components, which are needed by the single-user servers.
|
It does not require the other Jupyter components, which are needed by the single-user servers.
|
||||||
To run the single-user servers, which may be on the same system as the Hub or not, installation of Jupyter Notebook ≥ 4 is required.]
|
To run the single-user servers, which may be on the same system as the Hub or not, installation of Jupyter Notebook ≥ 4 is required.]
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ We encourage you to ask questions on the mailing list:
|
|||||||
|
|
||||||
and you may participate in development discussions or get live help on Gitter:
|
and you may participate in development discussions or get live help on Gitter:
|
||||||
|
|
||||||
[](https://gitter.im/jupyter/jupyterhub?utm_source=badge&utm_medium=badge)
|
[](https://gitter.im/jupyterhub/jupyterhub?utm_source=badge&utm_medium=badge)
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
- [Project Jupyter website](https://jupyter.org)
|
- [Project Jupyter website](https://jupyter.org)
|
||||||
|
@@ -16,4 +16,9 @@ deployment:
|
|||||||
branch: master
|
branch: master
|
||||||
commands:
|
commands:
|
||||||
- docker login -u $DOCKER_USER -p $DOCKER_PASS -e unused@example.com
|
- docker login -u $DOCKER_USER -p $DOCKER_PASS -e unused@example.com
|
||||||
- docker push jupyterhub/jupyterhub-onbuild:${CIRCLE_TAG:-latest}
|
- docker push jupyterhub/jupyterhub-onbuild
|
||||||
|
release:
|
||||||
|
tag: /.*/
|
||||||
|
commands:
|
||||||
|
- docker login -u $DOCKER_USER -p $DOCKER_PASS -e unused@example.com
|
||||||
|
- docker push jupyterhub/jupyterhub-onbuild:$CIRCLE_TAG
|
||||||
|
@@ -4,6 +4,17 @@ See `git log` for a more detailed summary.
|
|||||||
|
|
||||||
## 0.6
|
## 0.6
|
||||||
|
|
||||||
|
### 0.6.1
|
||||||
|
|
||||||
|
Bugfixes on 0.6:
|
||||||
|
|
||||||
|
- statsd is an optional dependency, only needed if in use
|
||||||
|
- Notice more quickly when servers have crashed
|
||||||
|
- Better error pages for proxy errors
|
||||||
|
- Add Stop All button to admin panel for stopping all servers at once
|
||||||
|
|
||||||
|
### 0.6.0
|
||||||
|
|
||||||
- JupyterHub has moved to a new `jupyterhub` namespace on GitHub and Docker. What was `juptyer/jupyterhub` is now `jupyterhub/jupyterhub`, etc.
|
- JupyterHub has moved to a new `jupyterhub` namespace on GitHub and Docker. What was `juptyer/jupyterhub` is now `jupyterhub/jupyterhub`, etc.
|
||||||
- `jupyterhub/jupyterhub` image on DockerHub no longer loads the jupyterhub_config.py in an ONBUILD step. A new `jupyterhub/jupyterhub-onbuild` image does this
|
- `jupyterhub/jupyterhub` image on DockerHub no longer loads the jupyterhub_config.py in an ONBUILD step. A new `jupyterhub/jupyterhub-onbuild` image does this
|
||||||
- Add statsd support, via `c.JupyterHub.statsd_{host,port,prefix}`
|
- Add statsd support, via `c.JupyterHub.statsd_{host,port,prefix}`
|
||||||
|
@@ -45,7 +45,7 @@ as it resolves all of the cross-site issues.
|
|||||||
### Disabling user config
|
### Disabling user config
|
||||||
|
|
||||||
If subdomains are not available or not desirable,
|
If subdomains are not available or not desirable,
|
||||||
0.5 also adds an option `Spawner.disable_use_config`,
|
0.5 also adds an option `Spawner.disable_user_config`,
|
||||||
which you can set to prevent the user-owned configuration files from being loaded.
|
which you can set to prevent the user-owned configuration files from being loaded.
|
||||||
This leaves only package installation and PATHs as things the admin must enforce.
|
This leaves only package installation and PATHs as things the admin must enforce.
|
||||||
|
|
||||||
|
@@ -161,8 +161,9 @@ class UserServerAPIHandler(APIHandler):
|
|||||||
@admin_or_self
|
@admin_or_self
|
||||||
def post(self, name):
|
def post(self, name):
|
||||||
user = self.find_user(name)
|
user = self.find_user(name)
|
||||||
if user.spawner:
|
if user.running:
|
||||||
state = yield user.spawner.poll()
|
# include notify, so that a server that died is noticed immediately
|
||||||
|
state = yield user.spawner.poll_and_notify()
|
||||||
if state is None:
|
if state is None:
|
||||||
raise web.HTTPError(400, "%s's server is already running" % name)
|
raise web.HTTPError(400, "%s's server is already running" % name)
|
||||||
|
|
||||||
@@ -180,7 +181,8 @@ class UserServerAPIHandler(APIHandler):
|
|||||||
return
|
return
|
||||||
if not user.running:
|
if not user.running:
|
||||||
raise web.HTTPError(400, "%s's server is not running" % name)
|
raise web.HTTPError(400, "%s's server is not running" % name)
|
||||||
status = yield user.spawner.poll()
|
# include notify, so that a server that died is noticed immediately
|
||||||
|
status = yield user.spawner.poll_and_notify()
|
||||||
if status is not None:
|
if status is not None:
|
||||||
raise web.HTTPError(400, "%s's server is not running" % name)
|
raise web.HTTPError(400, "%s's server is not running" % name)
|
||||||
yield self.stop_single_user(user)
|
yield self.stop_single_user(user)
|
||||||
|
@@ -12,7 +12,6 @@ import signal
|
|||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import statsd
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from distutils.version import LooseVersion as V
|
from distutils.version import LooseVersion as V
|
||||||
from getpass import getuser
|
from getpass import getuser
|
||||||
@@ -507,22 +506,21 @@ class JupyterHub(Application):
|
|||||||
Instance(logging.Handler),
|
Instance(logging.Handler),
|
||||||
help="Extra log handlers to set on JupyterHub logger",
|
help="Extra log handlers to set on JupyterHub logger",
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
@property
|
statsd = Any(allow_none=False, help="The statsd client, if any. A mock will be used if we aren't using statsd")
|
||||||
def statsd(self):
|
@default('statsd')
|
||||||
if hasattr(self, '_statsd'):
|
def _statsd(self):
|
||||||
return self._statsd
|
|
||||||
if self.statsd_host:
|
if self.statsd_host:
|
||||||
self._statsd = statsd.StatsClient(
|
import statsd
|
||||||
|
client = statsd.StatsClient(
|
||||||
self.statsd_host,
|
self.statsd_host,
|
||||||
self.statsd_port,
|
self.statsd_port,
|
||||||
self.statsd_prefix
|
self.statsd_prefix
|
||||||
)
|
)
|
||||||
return self._statsd
|
return client
|
||||||
else:
|
else:
|
||||||
# return an empty mock object!
|
# return an empty mock object!
|
||||||
self._statsd = EmptyClass()
|
return EmptyClass()
|
||||||
return self._statsd
|
|
||||||
|
|
||||||
def init_logging(self):
|
def init_logging(self):
|
||||||
# This prevents double log messages because tornado use a root logger that
|
# This prevents double log messages because tornado use a root logger that
|
||||||
@@ -939,6 +937,7 @@ class JupyterHub(Application):
|
|||||||
'--api-ip', self.proxy.api_server.ip,
|
'--api-ip', self.proxy.api_server.ip,
|
||||||
'--api-port', str(self.proxy.api_server.port),
|
'--api-port', str(self.proxy.api_server.port),
|
||||||
'--default-target', self.hub.server.host,
|
'--default-target', self.hub.server.host,
|
||||||
|
'--error-target', url_path_join(self.hub.server.url, 'error'),
|
||||||
]
|
]
|
||||||
if self.subdomain_host:
|
if self.subdomain_host:
|
||||||
cmd.append('--host-routing')
|
cmd.append('--host-routing')
|
||||||
|
@@ -410,6 +410,7 @@ class BaseHandler(RequestHandler):
|
|||||||
"""render custom error pages"""
|
"""render custom error pages"""
|
||||||
exc_info = kwargs.get('exc_info')
|
exc_info = kwargs.get('exc_info')
|
||||||
message = ''
|
message = ''
|
||||||
|
exception = None
|
||||||
status_message = responses.get(status_code, 'Unknown HTTP Error')
|
status_message = responses.get(status_code, 'Unknown HTTP Error')
|
||||||
if exc_info:
|
if exc_info:
|
||||||
exception = exc_info[1]
|
exception = exc_info[1]
|
||||||
|
@@ -15,12 +15,12 @@ class LogoutHandler(BaseHandler):
|
|||||||
user = self.get_current_user()
|
user = self.get_current_user()
|
||||||
if user:
|
if user:
|
||||||
self.log.info("User logged out: %s", user.name)
|
self.log.info("User logged out: %s", user.name)
|
||||||
self.clear_login_cookie()
|
self.clear_login_cookie()
|
||||||
for name in user.other_user_cookies:
|
for name in user.other_user_cookies:
|
||||||
self.clear_login_cookie(name)
|
self.clear_login_cookie(name)
|
||||||
user.other_user_cookies = set([])
|
user.other_user_cookies = set([])
|
||||||
|
self.statsd.incr('logout')
|
||||||
self.redirect(self.hub.server.base_url, permanent=False)
|
self.redirect(self.hub.server.base_url, permanent=False)
|
||||||
self.statsd.incr('logout')
|
|
||||||
|
|
||||||
|
|
||||||
class LoginHandler(BaseHandler):
|
class LoginHandler(BaseHandler):
|
||||||
|
@@ -3,12 +3,14 @@
|
|||||||
# Copyright (c) Jupyter Development Team.
|
# Copyright (c) Jupyter Development Team.
|
||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
|
from http.client import responses
|
||||||
|
|
||||||
|
from jinja2 import TemplateNotFound
|
||||||
from tornado import web, gen
|
from tornado import web, gen
|
||||||
|
|
||||||
from .. import orm
|
from .. import orm
|
||||||
from ..utils import admin_only, url_path_join
|
from ..utils import admin_only, url_path_join
|
||||||
from .base import BaseHandler
|
from .base import BaseHandler
|
||||||
from urllib.parse import quote
|
|
||||||
|
|
||||||
|
|
||||||
class RootHandler(BaseHandler):
|
class RootHandler(BaseHandler):
|
||||||
@@ -41,9 +43,14 @@ class HomeHandler(BaseHandler):
|
|||||||
"""Render the user's home page."""
|
"""Render the user's home page."""
|
||||||
|
|
||||||
@web.authenticated
|
@web.authenticated
|
||||||
|
@gen.coroutine
|
||||||
def get(self):
|
def get(self):
|
||||||
|
user = self.get_current_user()
|
||||||
|
if user.running:
|
||||||
|
# trigger poll_and_notify event in case of a server that died
|
||||||
|
yield user.spawner.poll_and_notify()
|
||||||
html = self.render_template('home.html',
|
html = self.render_template('home.html',
|
||||||
user=self.get_current_user(),
|
user=user,
|
||||||
)
|
)
|
||||||
self.finish(html)
|
self.finish(html)
|
||||||
|
|
||||||
@@ -160,9 +167,43 @@ class AdminHandler(BaseHandler):
|
|||||||
self.finish(html)
|
self.finish(html)
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyErrorHandler(BaseHandler):
|
||||||
|
"""Handler for rendering proxy error pages"""
|
||||||
|
|
||||||
|
def get(self, status_code_s):
|
||||||
|
status_code = int(status_code_s)
|
||||||
|
status_message = responses.get(status_code, 'Unknown HTTP Error')
|
||||||
|
# build template namespace
|
||||||
|
|
||||||
|
hub_home = url_path_join(self.hub.server.base_url, 'home')
|
||||||
|
message_html = ''
|
||||||
|
if status_code == 503:
|
||||||
|
message_html = ' '.join([
|
||||||
|
"Your server appears to be down.",
|
||||||
|
"Try restarting it <a href='%s'>from the hub</a>" % hub_home
|
||||||
|
])
|
||||||
|
ns = dict(
|
||||||
|
status_code=status_code,
|
||||||
|
status_message=status_message,
|
||||||
|
message_html=message_html,
|
||||||
|
logo_url=hub_home,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.set_header('Content-Type', 'text/html')
|
||||||
|
# render the template
|
||||||
|
try:
|
||||||
|
html = self.render_template('%s.html' % status_code, **ns)
|
||||||
|
except TemplateNotFound:
|
||||||
|
self.log.debug("No template for %d", status_code)
|
||||||
|
html = self.render_template('error.html', **ns)
|
||||||
|
|
||||||
|
self.write(html)
|
||||||
|
|
||||||
|
|
||||||
default_handlers = [
|
default_handlers = [
|
||||||
(r'/', RootHandler),
|
(r'/', RootHandler),
|
||||||
(r'/home', HomeHandler),
|
(r'/home', HomeHandler),
|
||||||
(r'/admin', AdminHandler),
|
(r'/admin', AdminHandler),
|
||||||
(r'/spawn', SpawnHandler),
|
(r'/spawn', SpawnHandler),
|
||||||
|
(r'/error/(\d+)', ProxyErrorHandler),
|
||||||
]
|
]
|
||||||
|
@@ -15,7 +15,7 @@ from subprocess import Popen
|
|||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
|
|
||||||
from tornado import gen
|
from tornado import gen
|
||||||
from tornado.ioloop import IOLoop, PeriodicCallback
|
from tornado.ioloop import PeriodicCallback
|
||||||
|
|
||||||
from traitlets.config import LoggingConfigurable
|
from traitlets.config import LoggingConfigurable
|
||||||
from traitlets import (
|
from traitlets import (
|
||||||
@@ -335,15 +335,17 @@ class Spawner(LoggingConfigurable):
|
|||||||
|
|
||||||
self.stop_polling()
|
self.stop_polling()
|
||||||
|
|
||||||
add_callback = IOLoop.current().add_callback
|
|
||||||
for callback in self._callbacks:
|
for callback in self._callbacks:
|
||||||
add_callback(callback)
|
try:
|
||||||
|
yield gen.maybe_future(callback())
|
||||||
|
except Exception:
|
||||||
|
self.log.exception("Unhandled error in poll callback for %s", self)
|
||||||
|
return status
|
||||||
|
|
||||||
death_interval = Float(0.1)
|
death_interval = Float(0.1)
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def wait_for_death(self, timeout=10):
|
def wait_for_death(self, timeout=10):
|
||||||
"""wait for the process to die, up to timeout seconds"""
|
"""wait for the process to die, up to timeout seconds"""
|
||||||
loop = IOLoop.current()
|
|
||||||
for i in range(int(timeout / self.death_interval)):
|
for i in range(int(timeout / self.death_interval)):
|
||||||
status = yield self.poll()
|
status = yield self.poll()
|
||||||
if status is not None:
|
if status is not None:
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
version_info = (
|
version_info = (
|
||||||
0,
|
0,
|
||||||
6,
|
6,
|
||||||
0,
|
1,
|
||||||
# 'dev',
|
# 'dev',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -2,6 +2,5 @@ traitlets>=4.1
|
|||||||
tornado>=4.1
|
tornado>=4.1
|
||||||
jinja2
|
jinja2
|
||||||
pamela
|
pamela
|
||||||
statsd
|
|
||||||
sqlalchemy>=1.0
|
sqlalchemy>=1.0
|
||||||
requests
|
requests
|
||||||
|
@@ -152,15 +152,15 @@ require(["jquery", "bootstrap", "moment", "jhapi", "utils"], function ($, bs, mo
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#add-user").click(function () {
|
$("#add-users").click(function () {
|
||||||
var dialog = $("#add-user-dialog");
|
var dialog = $("#add-users-dialog");
|
||||||
dialog.find(".username-input").val('');
|
dialog.find(".username-input").val('');
|
||||||
dialog.find(".admin-checkbox").prop("checked", false);
|
dialog.find(".admin-checkbox").prop("checked", false);
|
||||||
dialog.modal();
|
dialog.modal();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#add-user-dialog").find(".save-button").click(function () {
|
$("#add-users-dialog").find(".save-button").click(function () {
|
||||||
var dialog = $("#add-user-dialog");
|
var dialog = $("#add-users-dialog");
|
||||||
var lines = dialog.find(".username-input").val().split('\n');
|
var lines = dialog.find(".username-input").val().split('\n');
|
||||||
var admin = dialog.find(".admin-checkbox").prop("checked");
|
var admin = dialog.find(".admin-checkbox").prop("checked");
|
||||||
var usernames = [];
|
var usernames = [];
|
||||||
@@ -178,6 +178,15 @@ require(["jquery", "bootstrap", "moment", "jhapi", "utils"], function ($, bs, mo
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#stop-all-servers").click(function () {
|
||||||
|
$("#stop-all-servers-dialog").modal();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#stop-all-servers-dialog").find(".stop-all-button").click(function () {
|
||||||
|
// stop all clicks all the active stop buttons
|
||||||
|
$('.stop-server').not('.hidden').click();
|
||||||
|
});
|
||||||
|
|
||||||
$("#shutdown-hub").click(function () {
|
$("#shutdown-hub").click(function () {
|
||||||
var dialog = $("#shutdown-hub-dialog");
|
var dialog = $("#shutdown-hub-dialog");
|
||||||
dialog.find("input[type=checkbox]").prop("checked", true);
|
dialog.find("input[type=checkbox]").prop("checked", true);
|
||||||
|
@@ -32,8 +32,9 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr class="user-row add-user-row">
|
<tr class="user-row add-user-row">
|
||||||
<td colspan="12">
|
<td colspan="12">
|
||||||
<a id="add-user" class="col-xs-5 btn btn-default">Add User</a>
|
<a id="add-users" class="col-xs-2 btn btn-default">Add Users</a>
|
||||||
<a id="shutdown-hub" class="col-xs-5 col-xs-offset-2 btn btn-danger">Shutdown Hub</a>
|
<a id="stop-all-servers" class="col-xs-2 col-xs-offset-5 btn btn-danger">Stop All</a>
|
||||||
|
<a id="shutdown-hub" class="col-xs-2 col-xs-offset-1 btn btn-danger">Shutdown Hub</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% for u in users %}
|
{% for u in users %}
|
||||||
@@ -71,6 +72,10 @@
|
|||||||
This operation cannot be undone.
|
This operation cannot be undone.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
|
{% call modal('Stop All Servers', btn_label='Stop All', btn_class='btn-danger stop-all-button') %}
|
||||||
|
Are you sure you want to stop all your users' servers? Kernels will be shutdown and unsaved data may be lost.
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
{% call modal('Shutdown Hub', btn_label='Shutdown', btn_class='btn-danger shutdown-button') %}
|
{% call modal('Shutdown Hub', btn_label='Shutdown', btn_class='btn-danger shutdown-button') %}
|
||||||
Are you sure you want to shutdown the Hub?
|
Are you sure you want to shutdown the Hub?
|
||||||
You can choose to leave the proxy and/or single-user servers running by unchecking the boxes below:
|
You can choose to leave the proxy and/or single-user servers running by unchecking the boxes below:
|
||||||
@@ -108,7 +113,7 @@
|
|||||||
|
|
||||||
{{ user_modal('Edit User') }}
|
{{ user_modal('Edit User') }}
|
||||||
|
|
||||||
{{ user_modal('Add User', multi=True) }}
|
{{ user_modal('Add Users', multi=True) }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@@ -17,6 +17,11 @@
|
|||||||
{{message}}
|
{{message}}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if message_html %}
|
||||||
|
<p>
|
||||||
|
{{message_html | safe}}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
{% endblock error_detail %}
|
{% endblock error_detail %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -82,7 +82,7 @@
|
|||||||
|
|
||||||
<div id="header" class="navbar navbar-static-top">
|
<div id="header" class="navbar navbar-static-top">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<span id="jupyterhub-logo" class="pull-left"><a href="{{base_url}}"><img src='{{base_url}}logo' alt='JupyterHub' class='jpy-logo' title='Home'/></a></span>
|
<span id="jupyterhub-logo" class="pull-left"><a href="{{logo_url or base_url}}"><img src='{{base_url}}logo' alt='JupyterHub' class='jpy-logo' title='Home'/></a></span>
|
||||||
|
|
||||||
{% block login_widget %}
|
{% block login_widget %}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user