mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 10:34:10 +00:00
Compare commits
81 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
689dc5ba24 | ||
![]() |
d42a7261a4 | ||
![]() |
bcbf136de2 | ||
![]() |
55e9a0f5b5 | ||
![]() |
d64d916abc | ||
![]() |
da668b5e9a | ||
![]() |
d54442ecbf | ||
![]() |
c930d6bf6a | ||
![]() |
2ce263d45f | ||
![]() |
68f81fdc30 | ||
![]() |
e7ab18a720 | ||
![]() |
582467642c | ||
![]() |
d65e2daa15 | ||
![]() |
4eaa7c5eb3 | ||
![]() |
02de44e551 | ||
![]() |
4cdf0a65cd | ||
![]() |
b0367c21f3 | ||
![]() |
9d68107722 | ||
![]() |
ad61c23873 | ||
![]() |
c359221ef3 | ||
![]() |
cc94d290ab | ||
![]() |
da0a58cb9c | ||
![]() |
7ddd3b0589 | ||
![]() |
ff71d09fd1 | ||
![]() |
1eb0b1b073 | ||
![]() |
9ea9902c76 | ||
![]() |
6494017ce2 | ||
![]() |
b0cd9eebe9 | ||
![]() |
c3d4885521 | ||
![]() |
2919aaae79 | ||
![]() |
1986ba71c1 | ||
![]() |
a2c39a4dbc | ||
![]() |
1e847c8710 | ||
![]() |
83a8552a63 | ||
![]() |
f60c633320 | ||
![]() |
a5c7384228 | ||
![]() |
27de930978 | ||
![]() |
98e76d52bc | ||
![]() |
729aac9bd1 | ||
![]() |
bc85c445ab | ||
![]() |
9f708fa10c | ||
![]() |
d26c7cd6fc | ||
![]() |
0174083439 | ||
![]() |
e6fc2aee4a | ||
![]() |
47513cfbd0 | ||
![]() |
4e7147a495 | ||
![]() |
5cfc0db0d5 | ||
![]() |
eb862e2cbb | ||
![]() |
98799e4227 | ||
![]() |
ea6a0e53cc | ||
![]() |
f2b42a50c8 | ||
![]() |
43336f5b07 | ||
![]() |
bf2d948366 | ||
![]() |
271fd35bce | ||
![]() |
1d70986c25 | ||
![]() |
ec017d1f1d | ||
![]() |
a8c804de5b | ||
![]() |
3578001fab | ||
![]() |
b199110276 | ||
![]() |
b69bba5a7d | ||
![]() |
efdad701df | ||
![]() |
8a074b12b5 | ||
![]() |
b5e5fe630d | ||
![]() |
5d23bf6da3 | ||
![]() |
e5a8939481 | ||
![]() |
0eca901c65 | ||
![]() |
4a1964f881 | ||
![]() |
131094b5ff | ||
![]() |
4544a98fb9 | ||
![]() |
cbacdecb1e | ||
![]() |
64d8b2adc9 | ||
![]() |
9c83c15f67 | ||
![]() |
d2a545a01e | ||
![]() |
a376f33af1 | ||
![]() |
6f8a49569b | ||
![]() |
a4c553a5c5 | ||
![]() |
41445cffb4 | ||
![]() |
dafd2d67f6 | ||
![]() |
823ab58f3a | ||
![]() |
ab7883e5c3 | ||
![]() |
8fd1fb3234 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -84,7 +84,7 @@ jobs:
|
||||
|
||||
publish-docker:
|
||||
runs-on: ubuntu-20.04
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 20
|
||||
|
||||
services:
|
||||
# So that we can test this in PRs/branches
|
||||
|
2
.github/workflows/support-bot.yml
vendored
2
.github/workflows/support-bot.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
Our goal is to sustain a positive experience for both users and developers. We use GitHub issues for specific discussions related to changing a repository's content, and let the forum be where we can more generally help and inspire each other.
|
||||
|
||||
Thanks you for being an active member of our community! :heart:
|
||||
Thank you for being an active member of our community! :heart:
|
||||
close-issue: true
|
||||
lock-issue: false
|
||||
issue-lock-reason: "off-topic"
|
||||
|
13
.github/workflows/test.yml
vendored
13
.github/workflows/test.yml
vendored
@@ -99,7 +99,7 @@ jobs:
|
||||
noextension: noextension
|
||||
subset: singleuser
|
||||
- python: "3.11"
|
||||
selenium: selenium
|
||||
browser: browser
|
||||
- python: "3.11"
|
||||
main_dependencies: main_dependencies
|
||||
|
||||
@@ -229,9 +229,13 @@ jobs:
|
||||
DB=postgres bash ci/init-db.sh
|
||||
fi
|
||||
|
||||
- name: Configure selenium tests
|
||||
if: matrix.selenium
|
||||
run: echo "PYTEST_ADDOPTS=$PYTEST_ADDOPTS -m selenium" >> "${GITHUB_ENV}"
|
||||
- name: Configure browser tests
|
||||
if: matrix.browser
|
||||
run: echo "PYTEST_ADDOPTS=$PYTEST_ADDOPTS -m browser" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Ensure browsers are installed for playwright
|
||||
if: matrix.browser
|
||||
run: python -m playwright install --with-deps
|
||||
|
||||
- name: Run pytest
|
||||
run: |
|
||||
@@ -250,7 +254,6 @@ jobs:
|
||||
run: |
|
||||
DOCKER_BUILDKIT=1 docker build -t jupyterhub/jupyterhub .
|
||||
docker build -t jupyterhub/jupyterhub-onbuild onbuild
|
||||
docker build -t jupyterhub/jupyterhub:alpine -f dockerfiles/Dockerfile.alpine .
|
||||
docker build -t jupyterhub/singleuser singleuser
|
||||
|
||||
- name: smoke test jupyterhub
|
||||
|
@@ -16,7 +16,7 @@ ci:
|
||||
repos:
|
||||
# Autoformat: Python code, syntax patterns are modernized
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.3.1
|
||||
rev: v3.4.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args:
|
||||
@@ -24,7 +24,7 @@ repos:
|
||||
|
||||
# Autoformat: Python code
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.0.2
|
||||
rev: v2.1.1
|
||||
hooks:
|
||||
- id: autoflake
|
||||
# args ref: https://github.com/PyCQA/autoflake#advanced-usage
|
||||
@@ -45,7 +45,7 @@ repos:
|
||||
|
||||
# Autoformat: markdown, yaml, javascript (see the file .prettierignore)
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v3.0.0-alpha.6
|
||||
rev: v3.0.0-alpha.9-for-vscode
|
||||
hooks:
|
||||
- id: prettier
|
||||
|
||||
|
79
Dockerfile
79
Dockerfile
@@ -21,37 +21,83 @@
|
||||
# your jupyterhub_config.py will be added automatically
|
||||
# from your docker directory.
|
||||
|
||||
######################################################################
|
||||
# This Dockerfile uses multi-stage builds with optimisations to build
|
||||
# the JupyterHub wheel on the native architecture only
|
||||
# https://www.docker.com/blog/faster-multi-platform-builds-dockerfile-cross-compilation-guide/
|
||||
|
||||
ARG BASE_IMAGE=ubuntu:22.04
|
||||
FROM $BASE_IMAGE AS builder
|
||||
|
||||
|
||||
######################################################################
|
||||
# The JupyterHub wheel is pure Python so can be built for any platform
|
||||
# on the native architecture (avoiding QEMU emulation)
|
||||
FROM --platform=${BUILDPLATFORM:-linux/amd64} $BASE_IMAGE AS jupyterhub-builder
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
WORKDIR /src/jupyterhub
|
||||
|
||||
RUN apt update -q \
|
||||
&& apt install -yq --no-install-recommends \
|
||||
# Don't clear apt cache, and don't combine RUN commands, so that cached layers can
|
||||
# be reused in other stages
|
||||
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install -yqq --no-install-recommends \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
locales \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
python3-pycurl \
|
||||
python3-venv \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade setuptools pip build wheel
|
||||
# Ubuntu 22.04 comes with Nodejs 12 which is too old for building JupyterHub JS
|
||||
# It's fine at runtime though (used only by configurable-http-proxy)
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||
&& apt-get install -yqq --no-install-recommends \
|
||||
nodejs \
|
||||
npm \
|
||||
&& apt clean \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade setuptools pip build wheel \
|
||||
&& npm install --global yarn
|
||||
|
||||
WORKDIR /src/jupyterhub
|
||||
# copy everything except whats in .dockerignore, its a
|
||||
# compromise between needing to rebuild and maintaining
|
||||
# what needs to be part of the build
|
||||
COPY . .
|
||||
|
||||
ARG PIP_CACHE_DIR=/tmp/pip-cache
|
||||
RUN --mount=type=cache,target=${PIP_CACHE_DIR} \
|
||||
python3 -m build --wheel \
|
||||
&& python3 -m pip wheel --wheel-dir wheelhouse dist/*.whl
|
||||
python3 -m build --wheel
|
||||
|
||||
|
||||
######################################################################
|
||||
# All other wheels required by JupyterHub, some are platform specific
|
||||
FROM $BASE_IMAGE AS wheel-builder
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install -yqq --no-install-recommends \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
locales \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
python3-pycurl \
|
||||
python3-venv \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade setuptools pip build wheel
|
||||
|
||||
WORKDIR /src/jupyterhub
|
||||
|
||||
COPY --from=jupyterhub-builder /src/jupyterhub/dist/*.whl /src/jupyterhub/dist/
|
||||
ARG PIP_CACHE_DIR=/tmp/pip-cache
|
||||
RUN --mount=type=cache,target=${PIP_CACHE_DIR} \
|
||||
python3 -m pip wheel --wheel-dir wheelhouse dist/*.whl
|
||||
|
||||
|
||||
######################################################################
|
||||
# The final JupyterHub image, platform specific
|
||||
FROM $BASE_IMAGE AS jupyterhub
|
||||
|
||||
FROM $BASE_IMAGE
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
SHELL=/bin/bash \
|
||||
LC_ALL=en_US.UTF-8 \
|
||||
@@ -66,8 +112,8 @@ LABEL org.jupyter.service="jupyterhub"
|
||||
|
||||
WORKDIR /srv/jupyterhub
|
||||
|
||||
RUN apt update -q \
|
||||
&& apt install -yq --no-install-recommends \
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install -yqq --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
@@ -80,10 +126,9 @@ RUN apt update -q \
|
||||
&& locale-gen $LC_ALL \
|
||||
&& npm install -g configurable-http-proxy@^4.2.0 \
|
||||
# clean cache and logs
|
||||
&& rm -rf /var/lib/apt/lists/* /var/log/* /var/tmp/* ~/.npm \
|
||||
&& find / -type d -name '__pycache__' -prune -exec rm -rf {} \;
|
||||
# install the wheels we built in the first stage
|
||||
RUN --mount=type=cache,from=builder,source=/src/jupyterhub/wheelhouse,target=/tmp/wheelhouse \
|
||||
&& rm -rf /var/lib/apt/lists/* /var/log/* /var/tmp/* ~/.npm
|
||||
# install the wheels we built in the previous stage
|
||||
RUN --mount=type=cache,from=wheel-builder,source=/src/jupyterhub/wheelhouse,target=/tmp/wheelhouse \
|
||||
# always make sure pip is up to date!
|
||||
python3 -m pip install --no-compile --no-cache-dir --upgrade setuptools pip \
|
||||
&& python3 -m pip install --no-compile --no-cache-dir /tmp/wheelhouse/*
|
||||
|
@@ -1,14 +0,0 @@
|
||||
FROM alpine:3.13
|
||||
ENV LANG=en_US.UTF-8
|
||||
RUN apk add --no-cache \
|
||||
python3 \
|
||||
py3-pip \
|
||||
py3-ruamel.yaml \
|
||||
py3-cryptography \
|
||||
py3-sqlalchemy
|
||||
|
||||
ARG JUPYTERHUB_VERSION=1.3.0
|
||||
RUN pip3 install --no-cache jupyterhub==${JUPYTERHUB_VERSION}
|
||||
|
||||
USER nobody
|
||||
CMD ["jupyterhub"]
|
@@ -1,22 +0,0 @@
|
||||
## What is Dockerfile.alpine
|
||||
|
||||
Dockerfile.alpine contains the base image for jupyterhub. It does not work independently, but only as part of a full jupyterhub cluster
|
||||
|
||||
## How to use it?
|
||||
|
||||
You will need:
|
||||
|
||||
1. A running configurable-http-proxy, whose API is accessible.
|
||||
2. A jupyterhub_config file.
|
||||
3. Authentication and other libraries required by the specific jupyterhub_config file.
|
||||
|
||||
## Steps to test it outside a cluster
|
||||
|
||||
- start configurable-http-proxy in another container
|
||||
- specify CONFIGPROXY_AUTH_TOKEN env in both containers
|
||||
- put both containers on the same network (e.g. docker network create jupyterhub; docker run ... --net jupyterhub)
|
||||
- tell jupyterhub where CHP is (e.g. c.ConfigurableHTTPProxy.api_url = 'http://chp:8001')
|
||||
- tell jupyterhub not to start the proxy itself (c.ConfigurableHTTPProxy.should_start = False)
|
||||
- Use a dummy authenticator for ease of testing. Update following in jupyterhub_config file
|
||||
- c.JupyterHub.authenticator_class = 'dummyauthenticator.DummyAuthenticator'
|
||||
- c.DummyAuthenticator.password = "your strong password"
|
@@ -6,7 +6,7 @@ info:
|
||||
description: The REST API for JupyterHub
|
||||
license:
|
||||
name: BSD-3-Clause
|
||||
version: 4.0.0
|
||||
version: 4.0.1
|
||||
servers:
|
||||
- url: /hub/api
|
||||
security:
|
||||
@@ -1202,13 +1202,13 @@ components:
|
||||
description: Timestamp of last-seen activity from the user
|
||||
format: date-time
|
||||
servers:
|
||||
type: array
|
||||
type: object
|
||||
description: |
|
||||
The servers for this user.
|
||||
By default: only includes _active_ servers.
|
||||
Changed in 3.0: if `?include_stopped_servers` parameter is specified,
|
||||
stopped servers will be included as well.
|
||||
items:
|
||||
additionalProperties:
|
||||
$ref: "#/components/schemas/Server"
|
||||
auth_state:
|
||||
type: object
|
||||
|
@@ -201,6 +201,7 @@ intersphinx_mapping = {
|
||||
"python": ("https://docs.python.org/3/", None),
|
||||
"tornado": ("https://www.tornadoweb.org/en/stable/", None),
|
||||
"jupyter-server": ("https://jupyter-server.readthedocs.io/en/stable/", None),
|
||||
"nbgitpuller": ("https://nbgitpuller.readthedocs.io/en/latest", None),
|
||||
}
|
||||
|
||||
# -- Options for the opengraph extension -------------------------------------
|
||||
|
@@ -130,8 +130,8 @@ configuration:
|
||||
jupyterhub -f testing/jupyterhub_config.py
|
||||
```
|
||||
|
||||
The default JupyterHub [authenticator](https://jupyterhub.readthedocs.io/en/stable/reference/authenticators.html#the-default-pam-authenticator)
|
||||
& [spawner](https://jupyterhub.readthedocs.io/en/stable/api/spawner.html#localprocessspawner)
|
||||
The default JupyterHub [authenticator](PAMAuthenticator)
|
||||
& [spawner](LocalProcessSpawner)
|
||||
require your system to have user accounts for each user you want to log in to
|
||||
JupyterHub as.
|
||||
|
||||
|
@@ -2,35 +2,75 @@
|
||||
|
||||
## How do I share links to notebooks?
|
||||
|
||||
In short, where you see `/user/name/notebooks/foo.ipynb` use `/hub/user-redirect/notebooks/foo.ipynb` (replace `/user/name` with `/hub/user-redirect`).
|
||||
|
||||
Sharing links to notebooks is a common activity,
|
||||
and can look different based on what you mean.
|
||||
and can look different depending on what you mean by 'share.'
|
||||
Your first instinct might be to copy the URL you see in the browser,
|
||||
e.g. `hub.jupyter.org/user/yourname/notebooks/coolthing.ipynb`.
|
||||
However, let's break down what this URL means:
|
||||
e.g. `jupyterhub.example/user/yourname/notebooks/coolthing.ipynb`,
|
||||
but this usually won't work, depending on the permissions of the person you share the link with.
|
||||
|
||||
`hub.jupyter.org/user/yourname/` is the URL prefix handled by _your server_,
|
||||
which means that sharing this URL is asking the person you share the link with
|
||||
to come to _your server_ and look at the exact same file.
|
||||
In most circumstances, this is forbidden by permissions because the person you share with does not have access to your server.
|
||||
What actually happens when someone visits this URL will depend on whether your server is running and other factors.
|
||||
Unfortunately, 'share' means at least a few things to people in a JupyterHub context.
|
||||
We'll cover 3 common cases here, when they are applicable, and what assumptions they make:
|
||||
|
||||
**But what is our actual goal?**
|
||||
1. sharing links that will open the same file on the visitor's own server
|
||||
2. sharing links that will bring the visitor to _your_ server (e.g. for real-time collaboration, or RTC)
|
||||
3. publishing notebooks and sharing links that will download the notebook into the user's server
|
||||
|
||||
A typical situation is that you have some shared or common filesystem,
|
||||
such that the same path corresponds to the same document
|
||||
(either the exact same document or another copy of it).
|
||||
Typically, what folks want when they do sharing like this
|
||||
is for each visitor to open the same file _on their own server_,
|
||||
so Breq would open `/user/breq/notebooks/foo.ipynb` and
|
||||
Seivarden would open `/user/seivarden/notebooks/foo.ipynb`, etc.
|
||||
### link to the same file on the visitor's server
|
||||
|
||||
JupyterHub has a special URL that does exactly this!
|
||||
It's called `/hub/user-redirect/...`.
|
||||
So if you replace `/user/yourname` in your URL bar
|
||||
with `/hub/user-redirect` any visitor should get the same
|
||||
URL on their own server, rather than visiting yours.
|
||||
This is for the case where you have JupyterHub on a shared (or sufficiently similar) filesystem, where you want to share a link that will cause users to login and start their _own_ server, to view or edit the file.
|
||||
|
||||
In JupyterLab 2.0, this should also be the result of the "Copy Shareable Link"
|
||||
action in the file browser.
|
||||
**Assumption:** the same path on someone else's server is valid and points to the same file
|
||||
|
||||
This is useful in e.g. classes where you know students have certain files in certain locations, or collaborations where you know you have a shared filesystem where everyone has access to the same files.
|
||||
|
||||
A link should look like `https://jupyterhub.example/hub/user-redirect/lab/tree/foo.ipynb`.
|
||||
You can hand-craft these URLs from the URL you are looking at, where you see `/user/name/lab/tree/foo.ipynb` use `/hub/user-redirect/lab/tree/foo.ipynb` (replace `/user/name/` with `/hub/user-redirect/`).
|
||||
Or you can use JupyterLab's "copy shareable link" in the context menu in the file browser:
|
||||
|
||||

|
||||
|
||||
which will produce a correct URL with `/hub/user-redirect/` in it.
|
||||
|
||||
### link to the file on your server
|
||||
|
||||
This is for the case where you want to both be using _your_ server, e.g. for real-time collaboration (RTC).
|
||||
|
||||
**Assumption:** the user has (or should have) access to your server.
|
||||
|
||||
**Assumption:** your server is running _or_ the user has permission to start it.
|
||||
|
||||
By default, JupyterHub users don't have access to each other's servers, but JupyterHub 2.0 administrators can grant users limited access permissions to each other's servers.
|
||||
If the visitor doesn't have access to the server, these links will result in a 403 Permission Denied error.
|
||||
|
||||
In many cases, for this situation you can copy the link in your URL bar (`/user/yourname/lab`), or you can add `/tree/path/to/specific/notebook.ipynb` to open a specific file.
|
||||
|
||||
The [jupyterlab-link-share] JupyterLab extension generates these links, and even can _grant_ other users access to your server.
|
||||
|
||||
[jupyterlab-link-share]: https://github.com/jupyterlab-contrib/jupyterlab-link-share
|
||||
|
||||
:::{warning}
|
||||
Note that the way the extension _grants_ access is handing over credentials to allow the other user to **_BECOME YOU_**.
|
||||
This is usually not appropriate in JupyterHub.
|
||||
:::
|
||||
|
||||
### link to a published copy
|
||||
|
||||
Another way to 'share' notebooks is to publish copies, e.g. pushing the notebook to a git repository and sharing a download link.
|
||||
This way is especially useful for course materials,
|
||||
where no assumptions are necessary about the user's environment,
|
||||
except for having one package installed.
|
||||
|
||||
**Assumption:** The [nbgitpuller](inv:nbgitpuller#index) server extension is installed
|
||||
|
||||
Unlike the other two methods, nbgitpuller doesn't provide an extension to copy a shareable link for the document you're currently looking at,
|
||||
but it does provide a [link generator](inv:nbgitpuller#link),
|
||||
which uses the `user-redirect` approach above.
|
||||
|
||||
When visiting an nbgitpuller link:
|
||||
|
||||
- The visitor will be directed to their own server
|
||||
- Your repo will be cloned (or updated if it's already been cloned)
|
||||
- and then the file opened when it's ready
|
||||
|
||||
[nbgitpuller]: https://nbgitpuller.readthedocs.io
|
||||
[nbgitpuller-link]: https://nbgitpuller.readthedocs.io/en/latest/link.html
|
||||
|
@@ -66,7 +66,7 @@ Here is a sample of organizations that use JupyterHub:
|
||||
- **Universities and colleges**: UC Berkeley, UC San Diego, Cal Poly SLO, Harvard University, University of Chicago,
|
||||
University of Oslo, University of Sheffield, Université Paris Sud, University of Versailles
|
||||
- **Research laboratories**: NASA, NCAR, NOAA, the Large Synoptic Survey Telescope, Brookhaven National Lab,
|
||||
Minnesota Supercomputing Institute, ALCF, CERN, Lawrence Livermore National Laboratory
|
||||
Minnesota Supercomputing Institute, ALCF, CERN, Lawrence Livermore National Laboratory, HUNT
|
||||
- **Online communities**: Pangeo, Quantopian, mybinder.org, MathHub, Open Humans
|
||||
- **Computing infrastructure providers**: NERSC, San Diego Supercomputing Center, Compute Canada
|
||||
- **Companies**: Capital One, SANDVIK code, Globus
|
||||
@@ -130,7 +130,7 @@ level for several years, and makes a number of "default" security decisions that
|
||||
users.
|
||||
|
||||
- For security considerations in the base JupyterHub application,
|
||||
[see the JupyterHub security page](https://jupyterhub.readthedocs.io/en/stable/reference/websecurity.html).
|
||||
[see the JupyterHub security page](web-security).
|
||||
- For security considerations when deploying JupyterHub on Kubernetes, see the
|
||||
[JupyterHub on Kubernetes security page](https://z2jh.jupyter.org/en/latest/security.html).
|
||||
|
||||
|
@@ -167,7 +167,7 @@ When your whole JupyterHub sits behind an organization proxy (_not_ a reverse pr
|
||||
|
||||
### Launching Jupyter Notebooks to run as an externally managed JupyterHub service with the `jupyterhub-singleuser` command returns a `JUPYTERHUB_API_TOKEN` error
|
||||
|
||||
[JupyterHub services](https://jupyterhub.readthedocs.io/en/stable/reference/services.html) allow processes to interact with JupyterHub's REST API. Example use-cases include:
|
||||
{ref}`services` allow processes to interact with JupyterHub's REST API. Example use-cases include:
|
||||
|
||||
- **Secure Testing**: provide a canonical Jupyter Notebook for testing production data to reduce the number of entry points into production systems.
|
||||
- **Grading Assignments**: provide access to shared Jupyter Notebooks that may be used for management tasks such as grading assignments.
|
||||
|
@@ -33,36 +33,13 @@ such as:
|
||||
To send requests using the JupyterHub API, you must pass an API token with
|
||||
the request.
|
||||
|
||||
The preferred way of generating an API token is by running:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
This `openssl` command generates a potential token that can then be
|
||||
added to JupyterHub using `.api_tokens` configuration setting in
|
||||
`jupyterhub_config.py`.
|
||||
|
||||
```{note}
|
||||
The api_tokens configuration has been softly deprecated since the introduction of services.
|
||||
```
|
||||
|
||||
Alternatively, you can use the `jupyterhub token` command to generate a token
|
||||
for a specific hub user by passing the **username**:
|
||||
|
||||
```bash
|
||||
jupyterhub token <username>
|
||||
```
|
||||
|
||||
This command generates a random string to use as a token and registers
|
||||
it for the given user with the Hub's database.
|
||||
|
||||
In [version 0.8.0](changelog), a token request page for
|
||||
generating an API token is available from the JupyterHub user interface:
|
||||
While JupyterHub is running, any JupyterHub user can request a token via the `token` page.
|
||||
This is accessible via a `token` link in the top nav bar from the JupyterHub home page,
|
||||
or at the URL `/hub/token`.
|
||||
|
||||
:::{figure-md}
|
||||
|
||||

|
||||

|
||||
|
||||
JupyterHub's API token page
|
||||
:::
|
||||
@@ -74,6 +51,40 @@ JupyterHub's token page after successfully requesting a token.
|
||||
|
||||
:::
|
||||
|
||||
### Register API tokens via configuration
|
||||
|
||||
Sometimes, you'll want to pre-generate a token for access to JupyterHub,
|
||||
typically for use by external services,
|
||||
so that both JupyterHub and the service have access to the same value.
|
||||
|
||||
First, you need to generate a good random secret.
|
||||
A good way of generating an API token is by running:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
This `openssl` command generates a random token that can be added to the JupyterHub configuration in `jupyterhub_config.py`.
|
||||
|
||||
For external services, this would be registered with JupyterHub via configuration:
|
||||
|
||||
```python
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
"name": "my-service",
|
||||
"api_token": the_secret_value,
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
At this point, requests authenticated with the token will be associated with The service `my-service`.
|
||||
|
||||
```{note}
|
||||
You can also load additional tokens for users via the `JupyterHub.api_tokens` configuration.
|
||||
|
||||
However, this option has been deprecated since the introduction of services.
|
||||
```
|
||||
|
||||
## Assigning permissions to a token
|
||||
|
||||
Prior to JupyterHub 2.0, there were two levels of permissions:
|
||||
|
BIN
docs/source/images/shareable_link.webp
Normal file
BIN
docs/source/images/shareable_link.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 137 KiB |
Binary file not shown.
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 99 KiB |
Binary file not shown.
Before Width: | Height: | Size: 52 KiB |
@@ -39,6 +39,15 @@
|
||||
"reference/server-api.md" "tutorial/server-api.md"
|
||||
"reference/websecurity.md" "explanation/websecurity.md"
|
||||
|
||||
"api/app.md" "reference/api/app.md"
|
||||
"api/auth.md" "reference/api/auth.md"
|
||||
"api/index.md" "reference/api/index.md"
|
||||
"api/proxy.md" "reference/api/proxy.md"
|
||||
"api/service.md" "reference/api/service.md"
|
||||
"api/services.auth.md" "reference/api/services.auth.md"
|
||||
"api/spawner.md" "reference/api/spawner.md"
|
||||
"api/user.md" "reference/api/user.md"
|
||||
|
||||
# -- JupyterHub 4.0 --
|
||||
# redirects above are up-to-date as of JupyterHub 4.0
|
||||
# add future redirects below
|
||||
|
@@ -10,9 +10,62 @@ command line for details.
|
||||
|
||||
## 4.0
|
||||
|
||||
### 4.0.1 - 2023-06-08
|
||||
|
||||
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/4.0.0...4.0.1))
|
||||
|
||||
#### Enhancements made
|
||||
|
||||
- Delete server button on admin page [#4457](https://github.com/jupyterhub/jupyterhub/pull/4457) ([@diocas](https://github.com/diocas), [@minrk](https://github.com/minrk))
|
||||
|
||||
#### Bugs fixed
|
||||
|
||||
- Abort informatively on unrecognized CLI options [#4467](https://github.com/jupyterhub/jupyterhub/pull/4467) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Add xsrf to custom_html template context [#4464](https://github.com/jupyterhub/jupyterhub/pull/4464) ([@opoplawski](https://github.com/opoplawski), [@minrk](https://github.com/minrk))
|
||||
- preserve CLI > env priority config in jupyterhub-singleuser extension [#4451](https://github.com/jupyterhub/jupyterhub/pull/4451) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio), [@timeu](https://github.com/timeu), [@rcthomas](https://github.com/rcthomas))
|
||||
|
||||
#### Maintenance and upkeep improvements
|
||||
|
||||
- Fix link to collaboration accounts doc in example [#4448](https://github.com/jupyterhub/jupyterhub/pull/4448) ([@minrk](https://github.com/minrk))
|
||||
- Remove Dockerfile.alpine [#4444](https://github.com/jupyterhub/jupyterhub/pull/4444) ([@manics](https://github.com/manics), [@minrk](https://github.com/minrk))
|
||||
- Update jsx dependencies as much as possible [#4443](https://github.com/jupyterhub/jupyterhub/pull/4443) ([@manics](https://github.com/manics), [@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Remove unused admin JS code [#4438](https://github.com/jupyterhub/jupyterhub/pull/4438) ([@yuvipanda](https://github.com/yuvipanda), [@minrk](https://github.com/minrk))
|
||||
- Finish migrating browser tests from selenium to playwright [#4435](https://github.com/jupyterhub/jupyterhub/pull/4435) ([@mouse1203](https://github.com/mouse1203), [@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Migrate some tests from selenium to playwright [#4431](https://github.com/jupyterhub/jupyterhub/pull/4431) ([@mouse1203](https://github.com/mouse1203), [@minrk](https://github.com/minrk))
|
||||
- Begin setup of playwright tests [#4420](https://github.com/jupyterhub/jupyterhub/pull/4420) ([@mouse1203](https://github.com/mouse1203), [@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||
|
||||
#### Documentation improvements
|
||||
|
||||
- Reorder token request docs [#4463](https://github.com/jupyterhub/jupyterhub/pull/4463) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||
- 'servers' should be a dict of dicts, not a list of dicts in rest-api.yml [#4458](https://github.com/jupyterhub/jupyterhub/pull/4458) ([@tfmark](https://github.com/tfmark), [@minrk](https://github.com/minrk))
|
||||
- Config reference: link to nicer(?) API docs first [#4456](https://github.com/jupyterhub/jupyterhub/pull/4456) ([@manics](https://github.com/manics), [@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Add CERN to Gallery of JupyterHub Deployments [#4454](https://github.com/jupyterhub/jupyterhub/pull/4454) ([@goseind](https://github.com/goseind), [@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Fix "Thanks" typo. [#4441](https://github.com/jupyterhub/jupyterhub/pull/4441) ([@ryanlovett](https://github.com/ryanlovett), [@minrk](https://github.com/minrk))
|
||||
- add HUNT into research institutions [#4432](https://github.com/jupyterhub/jupyterhub/pull/4432) ([@matuskosut](https://github.com/matuskosut), [@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||
- docs: fix missing redirects for api to reference/api [#4429](https://github.com/jupyterhub/jupyterhub/pull/4429) ([@consideRatio](https://github.com/consideRatio), [@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||
- update sharing faq for 2023 [#4428](https://github.com/jupyterhub/jupyterhub/pull/4428) ([@minrk](https://github.com/minrk))
|
||||
- Fix some public URL links within the docs [#4427](https://github.com/jupyterhub/jupyterhub/pull/4427) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- add upgrade note for 4.0 to changelog [#4426](https://github.com/jupyterhub/jupyterhub/pull/4426) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
|
||||
#### Contributors to this release
|
||||
|
||||
The following people contributed discussions, new ideas, code and documentation contributions, and review.
|
||||
See [our definition of contributors](https://github-activity.readthedocs.io/en/latest/#how-does-this-tool-define-contributions-in-the-reports).
|
||||
|
||||
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2023-04-20&to=2023-06-07&type=c))
|
||||
|
||||
@consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2023-04-20..2023-06-07&type=Issues)) | @diocas ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Adiocas+updated%3A2023-04-20..2023-06-07&type=Issues)) | @echarles ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aecharles+updated%3A2023-04-20..2023-06-07&type=Issues)) | @goseind ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Agoseind+updated%3A2023-04-20..2023-06-07&type=Issues)) | @hsadia538 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ahsadia538+updated%3A2023-04-20..2023-06-07&type=Issues)) | @mahamtariq58 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amahamtariq58+updated%3A2023-04-20..2023-06-07&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2023-04-20..2023-06-07&type=Issues)) | @matuskosut ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amatuskosut+updated%3A2023-04-20..2023-06-07&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2023-04-20..2023-06-07&type=Issues)) | @mouse1203 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amouse1203+updated%3A2023-04-20..2023-06-07&type=Issues)) | @opoplawski ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aopoplawski+updated%3A2023-04-20..2023-06-07&type=Issues)) | @rcthomas ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Arcthomas+updated%3A2023-04-20..2023-06-07&type=Issues)) | @ryanlovett ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aryanlovett+updated%3A2023-04-20..2023-06-07&type=Issues)) | @tfmark ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Atfmark+updated%3A2023-04-20..2023-06-07&type=Issues)) | @timeu ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Atimeu+updated%3A2023-04-20..2023-06-07&type=Issues)) | @yuvipanda ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ayuvipanda+updated%3A2023-04-20..2023-06-07&type=Issues))
|
||||
|
||||
### 4.0.0 - 2023-04-20
|
||||
|
||||
4.0 is a major release, but a small one.
|
||||
|
||||
:::{admonition} Upgrade note
|
||||
|
||||
Upgrading from 3.1 to 4.0 should require no additional action beyond running `jupyterhub --upgrade-db` to upgrade the database schema after upgrading the package version.
|
||||
It is otherwise a regular jupyterhub [upgrade](upgrading-jupyterhub).
|
||||
:::
|
||||
|
||||
There are three major changes that _should_ be invisible to most users:
|
||||
|
||||
1. Groups can now have 'properties', editable via the admin page, which can be used by Spawners for their operations.
|
||||
@@ -1372,7 +1425,7 @@ Thanks to everyone who has contributed to this release!
|
||||
- `JupyterHub.init_spawners_timeout` is introduced to combat slow startups on large JupyterHub deployments [#2721](https://github.com/jupyterhub/jupyterhub/pull/2721) ([@minrk](https://github.com/minrk))
|
||||
- The configuration `uids` for local authenticators is added to consistently assign users UNIX id's between installations [#2687](https://github.com/jupyterhub/jupyterhub/pull/2687) ([@rgerkin](https://github.com/rgerkin))
|
||||
- `JupyterHub.activity_resolution` is introduced with a default value of 30s improving performance by not updating the database with user activity too often [#2605](https://github.com/jupyterhub/jupyterhub/pull/2605) ([@minrk](https://github.com/minrk))
|
||||
- [HubAuth](https://jupyterhub.readthedocs.io/en/stable/api/services.auth.html#jupyterhub.services.auth.HubAuth)'s SSL configuration can now be set through environment variables [#2588](https://github.com/jupyterhub/jupyterhub/pull/2588) ([@cmd-ntrf](https://github.com/cmd-ntrf))
|
||||
- [HubAuth](jupyterhub.services.auth.HubAuth)'s SSL configuration can now be set through environment variables [#2588](https://github.com/jupyterhub/jupyterhub/pull/2588) ([@cmd-ntrf](https://github.com/cmd-ntrf))
|
||||
- Expose spawner.user_options in REST API. [#2755](https://github.com/jupyterhub/jupyterhub/pull/2755) ([@danielballan](https://github.com/danielballan))
|
||||
- add block for scripts included in head [#2828](https://github.com/jupyterhub/jupyterhub/pull/2828) ([@bitnik](https://github.com/bitnik))
|
||||
- Instrument JupyterHub to record events with jupyter_telemetry [Part II] [#2698](https://github.com/jupyterhub/jupyterhub/pull/2698) ([@Zsailer](https://github.com/Zsailer))
|
||||
|
@@ -14,6 +14,12 @@ section, the `jupyterhub_config.py` can be automatically generated via
|
||||
> jupyterhub --generate-config
|
||||
> ```
|
||||
|
||||
Most of this information is available in a nicer format in:
|
||||
|
||||
- [](./api/app.md)
|
||||
- [](./api/auth.md)
|
||||
- [](./api/spawner.md)
|
||||
|
||||
The following contains the output of that command for reference.
|
||||
|
||||
```{eval-rst}
|
||||
|
@@ -63,6 +63,15 @@ easy to do with RStudio too.
|
||||
|
||||
- [jupyterhub-deploy-teaching](https://github.com/jupyterhub/jupyterhub-deploy-teaching) based on work by Brian Granger for Cal Poly's Data Science 301 Course
|
||||
|
||||
### CERN
|
||||
|
||||
[CERN](https://home.cern/), also known as the European Organization for Nuclear Research, is a world-renowned scientific research centre and the home of the Large Hadron Collider (LHC).
|
||||
|
||||
Within CERN, there are two noteworthy JupyterHub deployments in operation:
|
||||
|
||||
- [SWAN](https://swan.web.cern.ch/swan/), which stands for Service for Web based Analysis, serves as an interactive data analysis platform primarily utilized at CERN.
|
||||
- [VRE](https://vre-hub.github.io/), which stands for Virtual Research Environment, is an analysis platform developed within the [EOSC Project](https://eoscfuture.eu/) to cater to the needs of scientific communities involved in European projects.
|
||||
|
||||
### Chameleon
|
||||
|
||||
[Chameleon](https://www.chameleoncloud.org) is a NSF-funded configurable experimental environment for large-scale computer science systems research with [bare metal reconfigurability](https://chameleoncloud.readthedocs.io/en/latest/technical/baremetal.html). Chameleon users utilize JupyterHub to document and reproduce their complex CISE and networking experiments.
|
||||
|
@@ -39,7 +39,7 @@ openssl rand -hex 32
|
||||
In [version 0.8.0](changelog), a TOKEN request page for
|
||||
generating an API token is available from the JupyterHub user interface:
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
|
@@ -2,4 +2,4 @@
|
||||
|
||||
An example of enabling real-time collaboration with dedicated accounts for collaborations.
|
||||
|
||||
See [collaboration account docs](docs/source/tutorial/collaboration-accounts.md) for details.
|
||||
See [collaboration account docs](../../docs/source/tutorial/collaboration-users.md) for details.
|
||||
|
@@ -25,50 +25,49 @@
|
||||
"moduleNameMapper": {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
|
||||
"\\.(css|less)$": "identity-obj-proxy"
|
||||
}
|
||||
},
|
||||
"testEnvironment": "jsdom"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^4.5.3",
|
||||
"history": "^5.0.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^17.0.1",
|
||||
"react-bootstrap": "^2.1.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-icons": "^4.1.0",
|
||||
"react-multi-select-component": "^3.0.7",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"recompose": "npm:react-recompose@^0.31.2",
|
||||
"redux": "^4.0.5",
|
||||
"regenerator-runtime": "^0.13.9"
|
||||
"bootstrap": "^5.2.3",
|
||||
"history": "^5.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap": "^2.7.4",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-multi-select-component": "^4.3.4",
|
||||
"react-redux": "^7.2.8",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"recompose": "npm:react-recompose@^0.33.0",
|
||||
"redux": "^4.2.1",
|
||||
"regenerator-runtime": "^0.13.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.3",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-react": "^7.12.10",
|
||||
"@testing-library/jest-dom": "^5.15.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@babel/core": "^7.21.4",
|
||||
"@babel/preset-env": "^7.21.4",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@webpack-cli/serve": "^1.7.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.5",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babel-loader": "^8.2.1",
|
||||
"css-loader": "^5.0.1",
|
||||
"enzyme": "^3.11.0",
|
||||
"eslint": "^7.18.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"eslint-plugin-unused-imports": "^1.1.1",
|
||||
"@webpack-cli/serve": "^2.0.1",
|
||||
"babel-jest": "^29.5.0",
|
||||
"babel-loader": "^9.1.2",
|
||||
"css-loader": "^6.7.3",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"prettier": "^2.2.1",
|
||||
"sinon": "^13.0.1",
|
||||
"style-loader": "^2.0.0",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.9.3"
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"prettier": "^2.8.7",
|
||||
"sinon": "^15.0.3",
|
||||
"style-loader": "^3.3.2",
|
||||
"webpack": "^5.79.0",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.13.3"
|
||||
}
|
||||
}
|
||||
|
@@ -74,6 +74,7 @@ const ServerDashboard = (props) => {
|
||||
shutdownHub,
|
||||
startServer,
|
||||
stopServer,
|
||||
deleteServer,
|
||||
startAll,
|
||||
stopAll,
|
||||
history,
|
||||
@@ -167,6 +168,50 @@ const ServerDashboard = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const DeleteServerButton = ({ serverName, userName }) => {
|
||||
if (serverName === "") {
|
||||
return null;
|
||||
}
|
||||
var [isDisabled, setIsDisabled] = useState(false);
|
||||
return (
|
||||
<button
|
||||
className="btn btn-danger btn-xs stop-button"
|
||||
// It's not possible to delete unnamed servers
|
||||
disabled={isDisabled}
|
||||
onClick={() => {
|
||||
setIsDisabled(true);
|
||||
deleteServer(userName, serverName)
|
||||
.then((res) => {
|
||||
if (res.status < 300) {
|
||||
updateUsers(...slice)
|
||||
.then((data) => {
|
||||
dispatchPageUpdate(
|
||||
data.items,
|
||||
data._pagination,
|
||||
name_filter,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
setIsDisabled(false);
|
||||
setErrorAlert(`Failed to update users list.`);
|
||||
});
|
||||
} else {
|
||||
setErrorAlert(`Failed to delete server.`);
|
||||
setIsDisabled(false);
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.catch(() => {
|
||||
setErrorAlert(`Failed to delete server.`);
|
||||
setIsDisabled(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete Server
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const StartServerButton = ({ serverName, userName }) => {
|
||||
var [isDisabled, setIsDisabled] = useState(false);
|
||||
return (
|
||||
@@ -278,7 +323,11 @@ const ServerDashboard = (props) => {
|
||||
const userServerName = user.name + serverNameDash;
|
||||
const open = collapseStates[userServerName] || false;
|
||||
return [
|
||||
<tr key={`${userServerName}-row`} className="user-row">
|
||||
<tr
|
||||
key={`${userServerName}-row`}
|
||||
data-testid={`user-row-${userServerName}`}
|
||||
className="user-row"
|
||||
>
|
||||
<td data-testid="user-row-name">
|
||||
<span>
|
||||
<Button
|
||||
@@ -324,6 +373,10 @@ const ServerDashboard = (props) => {
|
||||
userName={user.name}
|
||||
style={{ marginRight: 20 }}
|
||||
/>
|
||||
<DeleteServerButton
|
||||
serverName={server.name}
|
||||
userName={user.name}
|
||||
/>
|
||||
<a
|
||||
href={`${base_url}spawn/${user.name}${
|
||||
server.name ? "/" + server.name : ""
|
||||
@@ -582,6 +635,7 @@ ServerDashboard.propTypes = {
|
||||
shutdownHub: PropTypes.func,
|
||||
startServer: PropTypes.func,
|
||||
stopServer: PropTypes.func,
|
||||
deleteServer: PropTypes.func,
|
||||
startAll: PropTypes.func,
|
||||
stopAll: PropTypes.func,
|
||||
dispatch: PropTypes.func,
|
||||
|
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { render, screen, fireEvent, getByText } from "@testing-library/react";
|
||||
import { HashRouter, Switch } from "react-router-dom";
|
||||
import { Provider, useSelector } from "react-redux";
|
||||
import { createStore } from "redux";
|
||||
@@ -43,6 +43,31 @@ var mockAsync = (data) =>
|
||||
var mockAsyncRejection = () =>
|
||||
jest.fn().mockImplementation(() => Promise.reject());
|
||||
|
||||
var bar_servers = {
|
||||
"": {
|
||||
name: "",
|
||||
last_activity: "2020-12-07T20:58:02.437408Z",
|
||||
started: "2020-12-07T20:58:01.508266Z",
|
||||
pending: null,
|
||||
ready: false,
|
||||
state: { pid: 12345 },
|
||||
url: "/user/bar/",
|
||||
user_options: {},
|
||||
progress_url: "/hub/api/users/bar/progress",
|
||||
},
|
||||
servername: {
|
||||
name: "servername",
|
||||
last_activity: "2020-12-07T20:58:02.437408Z",
|
||||
started: "2020-12-07T20:58:01.508266Z",
|
||||
pending: null,
|
||||
ready: false,
|
||||
state: { pid: 12345 },
|
||||
url: "/user/bar/servername",
|
||||
user_options: {},
|
||||
progress_url: "/hub/api/users/bar/servername/progress",
|
||||
},
|
||||
};
|
||||
|
||||
var mockAppState = () =>
|
||||
Object.assign({}, initialState, {
|
||||
user_data: [
|
||||
@@ -78,19 +103,7 @@ var mockAppState = () =>
|
||||
pending: null,
|
||||
created: "2020-12-07T18:46:27.115528Z",
|
||||
last_activity: "2020-12-07T20:43:51.013613Z",
|
||||
servers: {
|
||||
"": {
|
||||
name: "",
|
||||
last_activity: "2020-12-07T20:58:02.437408Z",
|
||||
started: "2020-12-07T20:58:01.508266Z",
|
||||
pending: null,
|
||||
ready: false,
|
||||
state: { pid: 12345 },
|
||||
url: "/user/bar/",
|
||||
user_options: {},
|
||||
progress_url: "/hub/api/users/bar/progress",
|
||||
},
|
||||
},
|
||||
servers: bar_servers,
|
||||
},
|
||||
],
|
||||
user_page: {
|
||||
@@ -150,9 +163,11 @@ test("Renders users from props.user_data into table", async () => {
|
||||
|
||||
let foo = screen.getByTestId("user-name-div-foo");
|
||||
let bar = screen.getByTestId("user-name-div-bar");
|
||||
let bar_server = screen.getByTestId("user-name-div-bar-servername");
|
||||
|
||||
expect(foo).toBeVisible();
|
||||
expect(bar).toBeVisible();
|
||||
expect(bar_server).toBeVisible();
|
||||
});
|
||||
|
||||
test("Renders correctly the status of a single-user server", async () => {
|
||||
@@ -162,10 +177,13 @@ test("Renders correctly the status of a single-user server", async () => {
|
||||
render(serverDashboardJsx(callbackSpy));
|
||||
});
|
||||
|
||||
let start = screen.getByText("Start Server");
|
||||
let stop = screen.getByText("Stop Server");
|
||||
|
||||
let start_elems = screen.getAllByText("Start Server");
|
||||
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
|
||||
start_elems.forEach((start) => {
|
||||
expect(start).toBeVisible();
|
||||
});
|
||||
|
||||
let stop = screen.getByText("Stop Server");
|
||||
expect(stop).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -176,9 +194,12 @@ test("Renders spawn page link", async () => {
|
||||
render(serverDashboardJsx(callbackSpy));
|
||||
});
|
||||
|
||||
let link = screen.getByText("Spawn Page").closest("a");
|
||||
for (let server in bar_servers) {
|
||||
let row = screen.getByTestId(`user-row-bar${server ? "-" + server : ""}`);
|
||||
let link = getByText(row, "Spawn Page").closest("a");
|
||||
let url = new URL(link.href);
|
||||
expect(url.pathname).toEqual("/spawn/bar");
|
||||
expect(url.pathname).toEqual("/spawn/bar" + (server ? "/" + server : ""));
|
||||
}
|
||||
});
|
||||
|
||||
test("Invokes the startServer event on button click", async () => {
|
||||
@@ -188,10 +209,11 @@ test("Invokes the startServer event on button click", async () => {
|
||||
render(serverDashboardJsx(callbackSpy));
|
||||
});
|
||||
|
||||
let start = screen.getByText("Start Server");
|
||||
let start_elems = screen.getAllByText("Start Server");
|
||||
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(start);
|
||||
fireEvent.click(start_elems[0]);
|
||||
});
|
||||
|
||||
expect(callbackSpy).toHaveBeenCalled();
|
||||
@@ -453,10 +475,11 @@ test("Shows a UI error dialogue when start user server fails", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
let start = screen.getByText("Start Server");
|
||||
let start_elems = screen.getAllByText("Start Server");
|
||||
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(start);
|
||||
fireEvent.click(start_elems[0]);
|
||||
});
|
||||
|
||||
let errorDialog = screen.getByText("Failed to start server.");
|
||||
@@ -487,10 +510,11 @@ test("Shows a UI error dialogue when start user server returns an improper statu
|
||||
);
|
||||
});
|
||||
|
||||
let start = screen.getByText("Start Server");
|
||||
let start_elems = screen.getAllByText("Start Server");
|
||||
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(start);
|
||||
fireEvent.click(start_elems[0]);
|
||||
});
|
||||
|
||||
let errorDialog = screen.getByText("Failed to start server.");
|
||||
@@ -656,3 +680,20 @@ test("Interacting with PaginationFooter causes state update and refresh via useE
|
||||
// expect(callbackSpy.mock.calls).toHaveLength(2);
|
||||
// expect(callbackSpy).toHaveBeenCalledWith(2, 2, "");
|
||||
});
|
||||
|
||||
test("Server delete button exists for named servers", async () => {
|
||||
let callbackSpy = mockAsync();
|
||||
|
||||
await act(async () => {
|
||||
render(serverDashboardJsx(callbackSpy));
|
||||
});
|
||||
|
||||
for (let server in bar_servers) {
|
||||
if (server === "") {
|
||||
continue;
|
||||
}
|
||||
let row = screen.getByTestId(`user-row-bar-${server}`);
|
||||
let delete_button = getByText(row, "Delete Server");
|
||||
expect(delete_button).toBeEnabled();
|
||||
}
|
||||
});
|
||||
|
@@ -18,6 +18,12 @@ const withAPI = withProps(() => ({
|
||||
jhapiRequest("/users/" + name + "/servers/" + (serverName || ""), "POST"),
|
||||
stopServer: (name, serverName = "") =>
|
||||
jhapiRequest("/users/" + name + "/servers/" + (serverName || ""), "DELETE"),
|
||||
deleteServer: (name, serverName = "") =>
|
||||
jhapiRequest(
|
||||
"/users/" + name + "/servers/" + (serverName || ""),
|
||||
"DELETE",
|
||||
{ remove: true },
|
||||
),
|
||||
startAll: (names) =>
|
||||
names.map((e) => jhapiRequest("/users/" + e + "/server", "POST")),
|
||||
stopAll: (names) =>
|
||||
|
17021
jsx/yarn.lock
17021
jsx/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
# version_info updated by running `tbump`
|
||||
version_info = (4, 0, 0, "", "")
|
||||
version_info = (4, 0, 1, "", "")
|
||||
|
||||
# pep 440 version: no dot before beta/rc, but before .dev
|
||||
# 0.1.0rc1
|
||||
|
@@ -9,6 +9,7 @@ import logging
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import shlex
|
||||
import signal
|
||||
import socket
|
||||
import ssl
|
||||
@@ -2840,6 +2841,10 @@ class JupyterHub(Application):
|
||||
super().initialize(*args, **kwargs)
|
||||
if self.generate_config or self.generate_certs or self.subapp:
|
||||
return
|
||||
if self.extra_args:
|
||||
self.exit(
|
||||
f"Unrecognized command-line arguments: {' '.join(shlex.quote(arg) for arg in self.extra_args)!r}"
|
||||
)
|
||||
self._start_future = asyncio.Future()
|
||||
|
||||
def record_start(f):
|
||||
|
@@ -105,6 +105,7 @@ class LoginHandler(BaseHandler):
|
||||
'next': self.get_argument('next', ''),
|
||||
},
|
||||
),
|
||||
"xsrf": self.xsrf_token.decode('ascii'),
|
||||
}
|
||||
custom_html = Template(
|
||||
self.authenticator.get_custom_html(self.hub.base_url)
|
||||
|
@@ -511,16 +511,25 @@ class JupyterHubSingleUser(ExtensionApp):
|
||||
|
||||
# Jupyter Server default: config files have higher priority than extensions,
|
||||
# by:
|
||||
# 1. load config files
|
||||
# 1. load config files and CLI
|
||||
# 2. load extension config
|
||||
# 3. merge file config into extension config
|
||||
|
||||
# we invert that by merging our extension config into server config before
|
||||
# they get merged the other way
|
||||
# this way config from this extension should always have highest priority
|
||||
|
||||
# but this also puts our config above _CLI_ options,
|
||||
# and CLI should come before env,
|
||||
# so merge that into _our_ config before loading
|
||||
if self.serverapp.cli_config:
|
||||
for cls_name, cls_config in self.serverapp.cli_config.items():
|
||||
if cls_name in self.config:
|
||||
self.config[cls_name].merge(cls_config)
|
||||
|
||||
self.serverapp.update_config(self.config)
|
||||
|
||||
# add our custom templates
|
||||
# config below here has _lower_ priority than user config
|
||||
self.config.NotebookApp.extra_template_paths.append(SINGLEUSER_TEMPLATES_DIR)
|
||||
|
||||
@default("default_url")
|
||||
|
14
jupyterhub/tests/browser/conftest.py
Normal file
14
jupyterhub/tests/browser/conftest.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
async def browser():
|
||||
# browser_type in ["chromium", "firefox", "webkit"]
|
||||
async with async_playwright() as playwright:
|
||||
browser = await playwright.firefox.launch(headless=True)
|
||||
context = await browser.new_context()
|
||||
page = await context.new_page()
|
||||
yield page
|
||||
await context.clear_cookies()
|
||||
await browser.close()
|
1070
jupyterhub/tests/browser/test_browser.py
Normal file
1070
jupyterhub/tests/browser/test_browser.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,23 +0,0 @@
|
||||
import pytest
|
||||
from selenium import webdriver
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def browser_session():
|
||||
"""Re-use one browser instance for the test session"""
|
||||
options = webdriver.FirefoxOptions()
|
||||
options.add_argument("-headless")
|
||||
driver = webdriver.Firefox(options=options)
|
||||
yield driver
|
||||
driver.close()
|
||||
driver.quit()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def browser(browser_session, cleanup_after):
|
||||
"""Get the browser session for one test
|
||||
|
||||
cookies are cleared after each test
|
||||
"""
|
||||
yield browser_session
|
||||
browser_session.delete_all_cookies()
|
@@ -1,100 +0,0 @@
|
||||
"""Using for testing via the Selenium WebDriver for elements localization"""
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
|
||||
class BarLocators:
|
||||
"""class for handling the Menu bar page locators"""
|
||||
|
||||
LINK_HOME_BAR = (By.CSS_SELECTOR, "div.container-fluid a")
|
||||
USER_NAME = (By.CLASS_NAME, 'navbar-text')
|
||||
|
||||
|
||||
class LoginPageLocators:
|
||||
"""class for handling the login page locators"""
|
||||
|
||||
FORM_LOGIN = (By.XPATH, '//*[@id="login-main"]/form')
|
||||
ACCOUNT = (By.ID, "username_input")
|
||||
PASSWORD = (By.ID, "password_input")
|
||||
ERROR_INVALID_CREDANTIALS = (By.CSS_SELECTOR, "p.login_error")
|
||||
|
||||
|
||||
class SpawningPageLocators:
|
||||
"""class for handling the Spawning page locators"""
|
||||
|
||||
BUTTONS_SERVER = (By.CSS_SELECTOR, "div.text-center a")
|
||||
TEXT_SERVER_TITLE = (By.CSS_SELECTOR, "div.text-center h1")
|
||||
|
||||
TEXT_SERVER = (By.CSS_SELECTOR, "div.text-center p")
|
||||
TEXT_SERVER_NOT_RUN_YET = "Server not running"
|
||||
TEXT_SERVER_NOT_RUNNING = "Your server is not running. Would you like to start it?"
|
||||
|
||||
TEXT_SERVER_STARTING = "Your server is starting up."
|
||||
TEXT_SERVER_REDIRECT = (
|
||||
"You will be redirected automatically when it's ready for you."
|
||||
)
|
||||
PROGRESS_MESSAGE = (By.ID, "progress-message")
|
||||
PROGRESS_PRO = (By.ID, "sr-progress")
|
||||
PROGRESS_STATUS = (By.CLASS_NAME, "sr-only")
|
||||
TEXT = (By.ID, "starting")
|
||||
|
||||
|
||||
class HomePageLocators:
|
||||
"""class for handling the home page locators"""
|
||||
|
||||
BUTTONS_SERVER = (By.CSS_SELECTOR, "div.text-center a")
|
||||
TEXT_SERVER = (By.CSS_SELECTOR, "div.text-center p")
|
||||
TEXT_SERVER_STARTING = "Your server is starting up."
|
||||
TEXT_SERVER_REDIRECT = (
|
||||
"You will be redirected automatically when it's ready for you."
|
||||
)
|
||||
|
||||
|
||||
class TokenPageLocators:
|
||||
"""class for handling the Token page locators"""
|
||||
|
||||
BUTTON_API_REQ = (
|
||||
By.XPATH,
|
||||
'//form[@id="request-token-form"]//button[@type="submit"]',
|
||||
)
|
||||
LIST_EXP_TOKEN_FIELD = (By.ID, "token-expiration-seconds")
|
||||
LIST_EXP_TOKEN_OPT = (By.XPATH, '//option')
|
||||
""" 1 Hour,1 Day,1 Week, Never """
|
||||
|
||||
LIST_EXP_TOKEN_OPT_DICT = {
|
||||
'1 Hour': '3600',
|
||||
'1 Day': '86400',
|
||||
'1 Week': '604800',
|
||||
'Never': '',
|
||||
}
|
||||
"""'1 Hour': '3600','1 Day': '86400','1 Week': '604800','Never': ''
|
||||
displayed options: the values in sec"""
|
||||
|
||||
NEVER_EXP = (By.XPATH, '//*[@id="token-expiration-seconds"]/option[4]')
|
||||
TEXT = "Copy this token. You won't be able to see it again, but you can always come back here to get a new one."
|
||||
|
||||
# API Tokens table
|
||||
TOKEN_TABLE = (By.XPATH, '//h2[text()="API Tokens"]//following::table')
|
||||
TOKEN_TABLE_HEADER = (By.XPATH, '//h2[text()="API Tokens"]//following::table/thead')
|
||||
TOKEN_TABLE_HEAD_LIST = ['Note', 'Last used', 'Created', 'Expires at']
|
||||
TOKEN_TABLE_BODY = (By.TAG_NAME, 'tbody')
|
||||
TOKEN_TABLE_ROWS_BY_CLASS = (
|
||||
By.XPATH,
|
||||
'//h2[text()="API Tokens"]//following::table//tr[@class="token-row"]',
|
||||
)
|
||||
BUTTON_REVOKE_TOKEN = (By.XPATH, '//tr/td[5]/button')
|
||||
|
||||
# Authorized Applications
|
||||
AUTH_TABLE = (By.XPATH, '//h2[text()="Authorized Applications"]//following::table')
|
||||
AUTH_TABLE_HEAD_LIST = ['Application', 'Last used', 'First authorized']
|
||||
AUTH_TABLE_HEADER = (
|
||||
By.XPATH,
|
||||
'//h2[text()="Authorized Applications"]//following::table/thead',
|
||||
)
|
||||
AUTH_TABLE_HEAD = (By.TAG_NAME, 'thead')
|
||||
AUTH_TABLE_BODY = (By.TAG_NAME, 'tbody')
|
||||
AUTH_TABLE_ROWS_BY_CLASS = (
|
||||
By.XPATH,
|
||||
'//h2[text()="Authorized Applications"]//following::table//tr[@class="token-row"]',
|
||||
)
|
||||
BUTTON_REVOKE_AUTH = (By.XPATH, '//tr/td[4]/button')
|
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,7 @@ target_version = [
|
||||
github_url = "https://github.com/jupyterhub/jupyterhub"
|
||||
|
||||
[tool.tbump.version]
|
||||
current = "4.0.0"
|
||||
current = "4.0.1"
|
||||
|
||||
# Example of a semver regexp.
|
||||
# Make sure this matches current_version before
|
||||
|
@@ -7,7 +7,7 @@
|
||||
asyncio_mode = auto
|
||||
|
||||
# jupyter_server plugin is incompatible with notebook imports
|
||||
addopts = -p no:jupyter_server -m 'not selenium' --color yes --durations 10 --verbose
|
||||
addopts = -p no:jupyter_server -m 'not browser' --color yes --durations 10 --verbose
|
||||
|
||||
python_files = test_*.py
|
||||
markers =
|
||||
@@ -17,7 +17,7 @@ markers =
|
||||
user: mark as a test for a user
|
||||
slow: mark a test as slow
|
||||
role: mark as a test for roles
|
||||
selenium: web tests that run with selenium
|
||||
browser: web tests that run with playwright
|
||||
|
||||
filterwarnings =
|
||||
ignore:.*The new signature is "def engine_connect\(conn\)"*:sqlalchemy.exc.SADeprecationWarning
|
||||
|
2
setup.py
2
setup.py
@@ -144,7 +144,7 @@ setup_args = dict(
|
||||
"pytest-asyncio>=0.17",
|
||||
"pytest-cov",
|
||||
"requests-mock",
|
||||
"selenium",
|
||||
"playwright",
|
||||
"virtualenv",
|
||||
],
|
||||
},
|
||||
|
@@ -1,299 +0,0 @@
|
||||
// Copyright (c) Jupyter Development Team.
|
||||
// Distributed under the terms of the Modified BSD License.
|
||||
|
||||
require(["jquery", "moment", "jhapi", "utils"], function (
|
||||
$,
|
||||
moment,
|
||||
JHAPI,
|
||||
utils,
|
||||
) {
|
||||
"use strict";
|
||||
|
||||
var base_url = window.jhdata.base_url;
|
||||
var prefix = window.jhdata.prefix;
|
||||
var admin_access = window.jhdata.admin_access;
|
||||
var options_form = window.jhdata.options_form;
|
||||
|
||||
var api = new JHAPI(base_url);
|
||||
|
||||
function getRow(element) {
|
||||
var original = element;
|
||||
while (!element.hasClass("server-row")) {
|
||||
element = element.parent();
|
||||
if (element[0].tagName === "BODY") {
|
||||
console.error("Couldn't find row for", original);
|
||||
throw new Error("No server-row found");
|
||||
}
|
||||
}
|
||||
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");
|
||||
if (!col || col.length === 0) {
|
||||
return;
|
||||
}
|
||||
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
|
||||
el = $(el);
|
||||
var m = moment(new Date(el.text().trim()));
|
||||
el.text(m.isValid() ? m.fromNow() : "Never");
|
||||
});
|
||||
|
||||
$(".stop-server").click(function () {
|
||||
var el = $(this);
|
||||
var row = getRow(el);
|
||||
var serverName = row.data("server-name");
|
||||
var user = row.data("user");
|
||||
el.text("stopping...");
|
||||
var stop = function (options) {
|
||||
return api.stop_server(user, options);
|
||||
};
|
||||
if (serverName !== "") {
|
||||
stop = function (options) {
|
||||
return api.stop_named_server(user, serverName, options);
|
||||
};
|
||||
}
|
||||
stop({
|
||||
success: function () {
|
||||
el.text("stop " + serverName).addClass("hidden");
|
||||
row.find(".access-server").addClass("hidden");
|
||||
row.find(".start-server").removeClass("hidden");
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
$(".delete-server").click(function () {
|
||||
var el = $(this);
|
||||
var row = getRow(el);
|
||||
var serverName = row.data("server-name");
|
||||
var user = row.data("user");
|
||||
el.text("deleting...");
|
||||
api.delete_named_server(user, serverName, {
|
||||
success: function () {
|
||||
row.remove();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
$(".access-server").map(function (i, el) {
|
||||
el = $(el);
|
||||
var row = getRow(el);
|
||||
var user = row.data("user");
|
||||
var serverName = row.data("server-name");
|
||||
el.attr(
|
||||
"href",
|
||||
utils.url_path_join(prefix, "user", user, serverName) + "/",
|
||||
);
|
||||
});
|
||||
|
||||
if (admin_access && options_form) {
|
||||
// if admin access and options form are enabled
|
||||
// link to spawn page instead of making API requests
|
||||
$(".start-server").map(function (i, el) {
|
||||
el = $(el);
|
||||
var row = getRow(el);
|
||||
var user = row.data("user");
|
||||
var serverName = row.data("server-name");
|
||||
el.attr(
|
||||
"href",
|
||||
utils.url_path_join(prefix, "hub/spawn", user, serverName),
|
||||
);
|
||||
});
|
||||
// cannot start all servers in this case
|
||||
// since it would mean opening a bunch of tabs
|
||||
$("#start-all-servers").addClass("hidden");
|
||||
} else {
|
||||
$(".start-server").click(function () {
|
||||
var el = $(this);
|
||||
var row = getRow(el);
|
||||
var user = row.data("user");
|
||||
var serverName = row.data("server-name");
|
||||
el.text("starting...");
|
||||
var start = function (options) {
|
||||
return api.start_server(user, options);
|
||||
};
|
||||
if (serverName !== "") {
|
||||
start = function (options) {
|
||||
return api.start_named_server(user, serverName, options);
|
||||
};
|
||||
}
|
||||
start({
|
||||
success: function () {
|
||||
el.text("start " + serverName).addClass("hidden");
|
||||
row.find(".stop-server").removeClass("hidden");
|
||||
row.find(".access-server").removeClass("hidden");
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(".edit-user").click(function () {
|
||||
var el = $(this);
|
||||
var row = getRow(el);
|
||||
var user = row.data("user");
|
||||
var admin = row.data("admin");
|
||||
var dialog = $("#edit-user-dialog");
|
||||
dialog.data("user", user);
|
||||
dialog.find(".username-input").val(user);
|
||||
dialog.find(".admin-checkbox").attr("checked", admin === "True");
|
||||
dialog.modal();
|
||||
});
|
||||
|
||||
$("#edit-user-dialog")
|
||||
.find(".save-button")
|
||||
.click(function () {
|
||||
var dialog = $("#edit-user-dialog");
|
||||
var user = dialog.data("user");
|
||||
var name = dialog.find(".username-input").val();
|
||||
var admin = dialog.find(".admin-checkbox").prop("checked");
|
||||
api.edit_user(
|
||||
user,
|
||||
{
|
||||
admin: admin,
|
||||
name: name,
|
||||
},
|
||||
{
|
||||
success: function () {
|
||||
window.location.reload();
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
$(".delete-user").click(function () {
|
||||
var el = $(this);
|
||||
var row = getRow(el);
|
||||
var user = row.data("user");
|
||||
var dialog = $("#delete-user-dialog");
|
||||
dialog.find(".delete-username").text(user);
|
||||
dialog.modal();
|
||||
});
|
||||
|
||||
$("#delete-user-dialog")
|
||||
.find(".delete-button")
|
||||
.click(function () {
|
||||
var dialog = $("#delete-user-dialog");
|
||||
var username = dialog.find(".delete-username").text();
|
||||
console.log("deleting", username);
|
||||
api.delete_user(username, {
|
||||
success: function () {
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
$("#add-users").click(function () {
|
||||
var dialog = $("#add-users-dialog");
|
||||
dialog.find(".username-input").val("");
|
||||
dialog.find(".admin-checkbox").prop("checked", false);
|
||||
dialog.modal();
|
||||
});
|
||||
|
||||
$("#add-users-dialog")
|
||||
.find(".save-button")
|
||||
.click(function () {
|
||||
var dialog = $("#add-users-dialog");
|
||||
var lines = dialog.find(".username-input").val().split("\n");
|
||||
var admin = dialog.find(".admin-checkbox").prop("checked");
|
||||
var usernames = [];
|
||||
lines.map(function (line) {
|
||||
var username = line.trim();
|
||||
if (username.length) {
|
||||
usernames.push(username);
|
||||
}
|
||||
});
|
||||
|
||||
api.add_users(
|
||||
usernames,
|
||||
{ admin: admin },
|
||||
{
|
||||
success: function () {
|
||||
window.location.reload();
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
$("#stop-all-servers").click(function () {
|
||||
$("#stop-all-servers-dialog").modal();
|
||||
});
|
||||
|
||||
$("#start-all-servers").click(function () {
|
||||
$("#start-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();
|
||||
});
|
||||
|
||||
function start(el) {
|
||||
return function () {
|
||||
$(el).click();
|
||||
};
|
||||
}
|
||||
|
||||
$("#start-all-servers-dialog")
|
||||
.find(".start-all-button")
|
||||
.click(function () {
|
||||
$(".start-server")
|
||||
.not(".hidden")
|
||||
.each(function (i) {
|
||||
setTimeout(start(this), i * 500);
|
||||
});
|
||||
});
|
||||
|
||||
$("#shutdown-hub").click(function () {
|
||||
var dialog = $("#shutdown-hub-dialog");
|
||||
dialog.find("input[type=checkbox]").prop("checked", true);
|
||||
dialog.modal();
|
||||
});
|
||||
|
||||
$("#shutdown-hub-dialog")
|
||||
.find(".shutdown-button")
|
||||
.click(function () {
|
||||
var dialog = $("#shutdown-hub-dialog");
|
||||
var servers = dialog.find(".shutdown-servers-checkbox").prop("checked");
|
||||
var proxy = dialog.find(".shutdown-proxy-checkbox").prop("checked");
|
||||
api.shutdown_hub({
|
||||
proxy: proxy,
|
||||
servers: servers,
|
||||
});
|
||||
});
|
||||
|
||||
// signal that page has finished loading
|
||||
window._jupyterhub_page_loaded = true;
|
||||
});
|
@@ -66,7 +66,9 @@
|
||||
// common form display
|
||||
|
||||
.form-control:focus {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px @jupyter-orange;
|
||||
box-shadow:
|
||||
inset 0 1px 1px rgba(0, 0, 0, 0.075),
|
||||
0 0 8px @jupyter-orange;
|
||||
border-color: @jupyter-orange;
|
||||
outline-color: @jupyter-orange;
|
||||
}
|
||||
|
@@ -17,10 +17,3 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
<script type="text/javascript">
|
||||
require(["admin"]);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@@ -21,7 +21,7 @@
|
||||
This note will help you keep track of what your tokens are for.
|
||||
</small>
|
||||
<br><br>
|
||||
<label for="token-expiration-seconds">Token expires</label>
|
||||
<label for="token-expiration-seconds">Token expires in</label>
|
||||
{% block expiration_options %}
|
||||
<select id="token-expiration-seconds"
|
||||
class="form-control">
|
||||
@@ -33,7 +33,7 @@
|
||||
</select>
|
||||
{% endblock expiration_options %}
|
||||
<small id="note-expires-at" class="form-text text-muted">
|
||||
You can configure when your token will be expired.
|
||||
You can configure when your token will expire.
|
||||
</small>
|
||||
</div>
|
||||
</form>
|
||||
@@ -62,8 +62,8 @@
|
||||
<div class="row">
|
||||
<h2>API Tokens</h2>
|
||||
<p>
|
||||
These are tokens with full access to the JupyterHub API.
|
||||
Anything you can do with JupyterHub can be done with these tokens.
|
||||
These are tokens with access to the JupyterHub API.
|
||||
Permissions for each token may be viewed via the JupyterHub tokens API.
|
||||
Revoking the API token for a running server will require restarting that server.
|
||||
</p>
|
||||
<table class="table table-striped">
|
||||
@@ -72,7 +72,7 @@
|
||||
<th>Note</th>
|
||||
<th>Last used</th>
|
||||
<th>Created</th>
|
||||
<th>Expires at</th>
|
||||
<th>Expires</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
Reference in New Issue
Block a user