mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 10:34:10 +00:00
Compare commits
159 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0e7689f277 | ||
![]() |
b677655572 | ||
![]() |
9adc871448 | ||
![]() |
29d6540333 | ||
![]() |
5a4949faa5 | ||
![]() |
f2ab23b376 | ||
![]() |
b61582420a | ||
![]() |
f11ae34b73 | ||
![]() |
e91ab50d1b | ||
![]() |
4cb3a45ce4 | ||
![]() |
4e8f9b4334 | ||
![]() |
6131f2dbaa | ||
![]() |
a9dc588454 | ||
![]() |
537b2eaff6 | ||
![]() |
7f8a981aed | ||
![]() |
bc86e4c8f5 | ||
![]() |
20f75c0018 | ||
![]() |
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 | ||
![]() |
10e7ab96e5 | ||
![]() |
40f519544f | ||
![]() |
076c14dce6 | ||
![]() |
e223ce59e1 | ||
![]() |
ad833755e1 | ||
![]() |
142978b4d8 | ||
![]() |
e3cab48039 | ||
![]() |
203f4a5855 | ||
![]() |
cfc27db43d | ||
![]() |
e2a8557083 | ||
![]() |
d5478b1f21 | ||
![]() |
cf19af6f1c | ||
![]() |
1342f00d8e | ||
![]() |
1e49b4379b | ||
![]() |
a5d563217c | ||
![]() |
b1ac3b82dc | ||
![]() |
a376f33af1 | ||
![]() |
6f8a49569b | ||
![]() |
a4c553a5c5 | ||
![]() |
75ebe40f86 | ||
![]() |
69d711929a | ||
![]() |
4c12872dbf | ||
![]() |
21cee1be31 | ||
![]() |
00c782fd40 | ||
![]() |
b3f9635ecc | ||
![]() |
8c10fb285e | ||
![]() |
8a3f5d8f2e | ||
![]() |
7b496a5b4a | ||
![]() |
41445cffb4 | ||
![]() |
64e7705053 | ||
![]() |
dafd2d67f6 | ||
![]() |
823ab58f3a | ||
![]() |
ab7883e5c3 | ||
![]() |
8fd1fb3234 | ||
![]() |
6502b50576 | ||
![]() |
861347cce0 | ||
![]() |
43d4b65250 | ||
![]() |
e53ce19fcc | ||
![]() |
e603ff8274 | ||
![]() |
22b15f0ecf | ||
![]() |
c48c5bce99 | ||
![]() |
fa11d7e3c6 | ||
![]() |
7e3f29d033 | ||
![]() |
b7827687a8 | ||
![]() |
0beb4639a3 | ||
![]() |
b010c9501e | ||
![]() |
295e92270b | ||
![]() |
e42066f1c9 | ||
![]() |
1d29fcbfb2 | ||
![]() |
bdbfbb7e32 | ||
![]() |
42314ed75b | ||
![]() |
d8141692ab | ||
![]() |
025db2f9f3 | ||
![]() |
3985140377 | ||
![]() |
6886384ca3 | ||
![]() |
4a7fe8648a | ||
![]() |
7383c0cf60 | ||
![]() |
83186e02a2 | ||
![]() |
c6b4577c0a | ||
![]() |
73b1922c17 | ||
![]() |
1430e02fa8 | ||
![]() |
9ef09a288a | ||
![]() |
4a093be938 | ||
![]() |
64a253dbef | ||
![]() |
54877025ca | ||
![]() |
555969141e | ||
![]() |
a938982bdc | ||
![]() |
60a153718d | ||
![]() |
d72a96ec17 |
@@ -1,4 +1,4 @@
|
|||||||
# dependabot.yml reference: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
# dependabot.yaml reference: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||||
#
|
#
|
||||||
# Notes:
|
# Notes:
|
||||||
# - Status and logs from dependabot are provided at
|
# - Status and logs from dependabot are provided at
|
||||||
@@ -8,8 +8,9 @@ version: 2
|
|||||||
updates:
|
updates:
|
||||||
# Maintain dependencies in our GitHub Workflows
|
# Maintain dependencies in our GitHub Workflows
|
||||||
- package-ecosystem: github-actions
|
- package-ecosystem: github-actions
|
||||||
directory: "/"
|
directory: /
|
||||||
|
labels: [ci]
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: monthly
|
||||||
time: "05:00"
|
time: "05:00"
|
||||||
timezone: "Etc/UTC"
|
timezone: Etc/UTC
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -84,7 +84,7 @@ jobs:
|
|||||||
|
|
||||||
publish-docker:
|
publish-docker:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
timeout-minutes: 30
|
timeout-minutes: 20
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# So that we can test this in PRs/branches
|
# 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.
|
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
|
close-issue: true
|
||||||
lock-issue: false
|
lock-issue: false
|
||||||
issue-lock-reason: "off-topic"
|
issue-lock-reason: "off-topic"
|
||||||
|
33
.github/workflows/test-docs.yml
vendored
33
.github/workflows/test-docs.yml
vendored
@@ -49,6 +49,11 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
# make rediraffecheckdiff requires git history to compare current
|
||||||
|
# commit with the main branch and previous releases.
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.9"
|
python-version: "3.9"
|
||||||
@@ -72,3 +77,31 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd docs
|
cd docs
|
||||||
make linkcheck
|
make linkcheck
|
||||||
|
|
||||||
|
# make rediraffecheckdiff compares files for different changesets
|
||||||
|
# these diff targets aren't always available
|
||||||
|
# - compare with base ref (usually 'main', always on 'origin') for pull requests
|
||||||
|
# - only compare with tags when running against jupyterhub/jupyterhub
|
||||||
|
# to avoid errors on forks, which often lack tags
|
||||||
|
- name: check redirects for this PR
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
run: |
|
||||||
|
cd docs
|
||||||
|
export REDIRAFFE_BRANCH=origin/${{ github.base_ref }}
|
||||||
|
make rediraffecheckdiff
|
||||||
|
|
||||||
|
# this should check currently published 'stable' links for redirects
|
||||||
|
- name: check redirects since last release
|
||||||
|
if: github.repository == 'jupyterhub/jupyterhub'
|
||||||
|
run: |
|
||||||
|
cd docs
|
||||||
|
export REDIRAFFE_BRANCH=$(git describe --tags --abbrev=0)
|
||||||
|
make rediraffecheckdiff
|
||||||
|
|
||||||
|
# longer-term redirect check (fixed version) for older links
|
||||||
|
- name: check redirects since 3.0.0
|
||||||
|
if: github.repository == 'jupyterhub/jupyterhub'
|
||||||
|
run: |
|
||||||
|
cd docs
|
||||||
|
export REDIRAFFE_BRANCH=3.0.0
|
||||||
|
make rediraffecheckdiff
|
||||||
|
23
.github/workflows/test.yml
vendored
23
.github/workflows/test.yml
vendored
@@ -99,7 +99,7 @@ jobs:
|
|||||||
noextension: noextension
|
noextension: noextension
|
||||||
subset: singleuser
|
subset: singleuser
|
||||||
- python: "3.11"
|
- python: "3.11"
|
||||||
selenium: selenium
|
browser: browser
|
||||||
- python: "3.11"
|
- python: "3.11"
|
||||||
main_dependencies: main_dependencies
|
main_dependencies: main_dependencies
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
if [ "${{ matrix.db }}" == "mysql" ]; then
|
if [ "${{ matrix.db }}" == "mysql" ]; then
|
||||||
echo "MYSQL_HOST=127.0.0.1" >> $GITHUB_ENV
|
echo "MYSQL_HOST=127.0.0.1" >> $GITHUB_ENV
|
||||||
echo "JUPYTERHUB_TEST_DB_URL=mysql+mysqlconnector://root@127.0.0.1:3306/jupyterhub" >> $GITHUB_ENV
|
echo "JUPYTERHUB_TEST_DB_URL=mysql+mysqldb://root@127.0.0.1:3306/jupyterhub" >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
if [ "${{ matrix.ssl }}" == "ssl" ]; then
|
if [ "${{ matrix.ssl }}" == "ssl" ]; then
|
||||||
echo "SSL_ENABLED=1" >> $GITHUB_ENV
|
echo "SSL_ENABLED=1" >> $GITHUB_ENV
|
||||||
@@ -164,7 +164,9 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${{ matrix.main_dependencies }}" != "" ]; then
|
if [ "${{ matrix.main_dependencies }}" != "" ]; then
|
||||||
pip install git+https://github.com/ipython/traitlets#egg=traitlets --force
|
# Tests are broken:
|
||||||
|
# https://github.com/jupyterhub/jupyterhub/issues/4418
|
||||||
|
# pip install git+https://github.com/ipython/traitlets#egg=traitlets --force
|
||||||
pip install --upgrade --pre sqlalchemy
|
pip install --upgrade --pre sqlalchemy
|
||||||
fi
|
fi
|
||||||
if [ "${{ matrix.legacy_notebook }}" != "" ]; then
|
if [ "${{ matrix.legacy_notebook }}" != "" ]; then
|
||||||
@@ -175,7 +177,7 @@ jobs:
|
|||||||
pip install "jupyter_server==${{ matrix.jupyter_server }}"
|
pip install "jupyter_server==${{ matrix.jupyter_server }}"
|
||||||
fi
|
fi
|
||||||
if [ "${{ matrix.db }}" == "mysql" ]; then
|
if [ "${{ matrix.db }}" == "mysql" ]; then
|
||||||
pip install mysql-connector-python
|
pip install mysqlclient
|
||||||
fi
|
fi
|
||||||
if [ "${{ matrix.db }}" == "postgres" ]; then
|
if [ "${{ matrix.db }}" == "postgres" ]; then
|
||||||
pip install psycopg2-binary
|
pip install psycopg2-binary
|
||||||
@@ -227,9 +229,13 @@ jobs:
|
|||||||
DB=postgres bash ci/init-db.sh
|
DB=postgres bash ci/init-db.sh
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Configure selenium tests
|
- name: Configure browser tests
|
||||||
if: matrix.selenium
|
if: matrix.browser
|
||||||
run: echo "PYTEST_ADDOPTS=$PYTEST_ADDOPTS -m selenium" >> "${GITHUB_ENV}"
|
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
|
- name: Run pytest
|
||||||
run: |
|
run: |
|
||||||
@@ -246,9 +252,8 @@ jobs:
|
|||||||
|
|
||||||
- name: build images
|
- name: build images
|
||||||
run: |
|
run: |
|
||||||
docker build -t jupyterhub/jupyterhub .
|
DOCKER_BUILDKIT=1 docker build -t jupyterhub/jupyterhub .
|
||||||
docker build -t jupyterhub/jupyterhub-onbuild onbuild
|
docker build -t jupyterhub/jupyterhub-onbuild onbuild
|
||||||
docker build -t jupyterhub/jupyterhub:alpine -f dockerfiles/Dockerfile.alpine .
|
|
||||||
docker build -t jupyterhub/singleuser singleuser
|
docker build -t jupyterhub/singleuser singleuser
|
||||||
|
|
||||||
- name: smoke test jupyterhub
|
- name: smoke test jupyterhub
|
||||||
|
@@ -16,7 +16,7 @@ ci:
|
|||||||
repos:
|
repos:
|
||||||
# Autoformat: Python code, syntax patterns are modernized
|
# Autoformat: Python code, syntax patterns are modernized
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v3.3.1
|
rev: v3.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args:
|
args:
|
||||||
@@ -24,7 +24,7 @@ repos:
|
|||||||
|
|
||||||
# Autoformat: Python code
|
# Autoformat: Python code
|
||||||
- repo: https://github.com/PyCQA/autoflake
|
- repo: https://github.com/PyCQA/autoflake
|
||||||
rev: v2.0.1
|
rev: v2.1.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: autoflake
|
- id: autoflake
|
||||||
# args ref: https://github.com/PyCQA/autoflake#advanced-usage
|
# args ref: https://github.com/PyCQA/autoflake#advanced-usage
|
||||||
@@ -39,13 +39,13 @@ repos:
|
|||||||
|
|
||||||
# Autoformat: Python code
|
# Autoformat: Python code
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.1.0
|
rev: 23.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
|
|
||||||
# Autoformat: markdown, yaml, javascript (see the file .prettierignore)
|
# Autoformat: markdown, yaml, javascript (see the file .prettierignore)
|
||||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
rev: v3.0.0-alpha.6
|
rev: v3.0.0-alpha.9-for-vscode
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
|
|
||||||
|
121
Dockerfile
121
Dockerfile
@@ -21,83 +21,116 @@
|
|||||||
# your jupyterhub_config.py will be added automatically
|
# your jupyterhub_config.py will be added automatically
|
||||||
# from your docker directory.
|
# 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
|
ARG BASE_IMAGE=ubuntu:22.04
|
||||||
FROM $BASE_IMAGE AS builder
|
|
||||||
|
|
||||||
USER root
|
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND noninteractive
|
######################################################################
|
||||||
RUN apt-get update \
|
# The JupyterHub wheel is pure Python so can be built for any platform
|
||||||
&& apt-get install -yq --no-install-recommends \
|
# on the native architecture (avoiding QEMU emulation)
|
||||||
|
FROM --platform=${BUILDPLATFORM:-linux/amd64} $BASE_IMAGE AS jupyterhub-builder
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# 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 \
|
build-essential \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
curl \
|
||||||
locales \
|
locales \
|
||||||
python3-dev \
|
python3-dev \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
python3-pycurl \
|
python3-pycurl \
|
||||||
python3-venv \
|
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 \
|
nodejs \
|
||||||
npm \
|
&& npm install --global yarn
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN python3 -m pip install --upgrade setuptools pip build wheel
|
|
||||||
RUN npm install --global yarn
|
|
||||||
|
|
||||||
|
WORKDIR /src/jupyterhub
|
||||||
# copy everything except whats in .dockerignore, its a
|
# copy everything except whats in .dockerignore, its a
|
||||||
# compromise between needing to rebuild and maintaining
|
# compromise between needing to rebuild and maintaining
|
||||||
# what needs to be part of the build
|
# what needs to be part of the build
|
||||||
COPY . /src/jupyterhub/
|
COPY . .
|
||||||
WORKDIR /src/jupyterhub
|
|
||||||
|
|
||||||
# Build client component packages (they will be copied into ./share and
|
ARG PIP_CACHE_DIR=/tmp/pip-cache
|
||||||
# packaged with the built wheel.)
|
RUN --mount=type=cache,target=${PIP_CACHE_DIR} \
|
||||||
RUN python3 -m build --wheel
|
python3 -m build --wheel
|
||||||
RUN python3 -m pip wheel --wheel-dir wheelhouse dist/*.whl
|
|
||||||
|
|
||||||
|
|
||||||
FROM $BASE_IMAGE
|
######################################################################
|
||||||
|
# All other wheels required by JupyterHub, some are platform specific
|
||||||
USER root
|
FROM $BASE_IMAGE AS wheel-builder
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update -qq \
|
||||||
&& apt-get install -yq --no-install-recommends \
|
&& apt-get install -yqq --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
gnupg \
|
|
||||||
locales \
|
locales \
|
||||||
|
python3-dev \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
python3-pycurl \
|
python3-pycurl \
|
||||||
nodejs \
|
python3-venv \
|
||||||
npm \
|
&& python3 -m pip install --no-cache-dir --upgrade setuptools pip build wheel
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
ENV SHELL=/bin/bash \
|
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
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
|
SHELL=/bin/bash \
|
||||||
LC_ALL=en_US.UTF-8 \
|
LC_ALL=en_US.UTF-8 \
|
||||||
LANG=en_US.UTF-8 \
|
LANG=en_US.UTF-8 \
|
||||||
LANGUAGE=en_US.UTF-8
|
LANGUAGE=en_US.UTF-8 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1
|
||||||
RUN locale-gen $LC_ALL
|
|
||||||
|
|
||||||
# always make sure pip is up to date!
|
|
||||||
RUN python3 -m pip install --no-cache --upgrade setuptools pip
|
|
||||||
|
|
||||||
RUN npm install -g configurable-http-proxy@^4.2.0 \
|
|
||||||
&& rm -rf ~/.npm
|
|
||||||
|
|
||||||
# install the wheels we built in the first stage
|
|
||||||
COPY --from=builder /src/jupyterhub/wheelhouse /tmp/wheelhouse
|
|
||||||
RUN python3 -m pip install --no-cache /tmp/wheelhouse/*
|
|
||||||
|
|
||||||
RUN mkdir -p /srv/jupyterhub/
|
|
||||||
WORKDIR /srv/jupyterhub/
|
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
LABEL maintainer="Jupyter Project <jupyter@googlegroups.com>"
|
LABEL maintainer="Jupyter Project <jupyter@googlegroups.com>"
|
||||||
LABEL org.jupyter.service="jupyterhub"
|
LABEL org.jupyter.service="jupyterhub"
|
||||||
|
|
||||||
|
WORKDIR /srv/jupyterhub
|
||||||
|
|
||||||
|
RUN apt-get update -qq \
|
||||||
|
&& apt-get install -yqq --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
gnupg \
|
||||||
|
locales \
|
||||||
|
python-is-python3 \
|
||||||
|
python3-pip \
|
||||||
|
python3-pycurl \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
&& 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
|
||||||
|
# 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/*
|
||||||
|
|
||||||
CMD ["jupyterhub"]
|
CMD ["jupyterhub"]
|
||||||
|
@@ -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
|
description: The REST API for JupyterHub
|
||||||
license:
|
license:
|
||||||
name: BSD-3-Clause
|
name: BSD-3-Clause
|
||||||
version: 4.0.0b2
|
version: 4.0.2
|
||||||
servers:
|
servers:
|
||||||
- url: /hub/api
|
- url: /hub/api
|
||||||
security:
|
security:
|
||||||
@@ -1202,13 +1202,13 @@ components:
|
|||||||
description: Timestamp of last-seen activity from the user
|
description: Timestamp of last-seen activity from the user
|
||||||
format: date-time
|
format: date-time
|
||||||
servers:
|
servers:
|
||||||
type: array
|
type: object
|
||||||
description: |
|
description: |
|
||||||
The servers for this user.
|
The servers for this user.
|
||||||
By default: only includes _active_ servers.
|
By default: only includes _active_ servers.
|
||||||
Changed in 3.0: if `?include_stopped_servers` parameter is specified,
|
Changed in 3.0: if `?include_stopped_servers` parameter is specified,
|
||||||
stopped servers will be included as well.
|
stopped servers will be included as well.
|
||||||
items:
|
additionalProperties:
|
||||||
$ref: "#/components/schemas/Server"
|
$ref: "#/components/schemas/Server"
|
||||||
auth_state:
|
auth_state:
|
||||||
type: object
|
type: object
|
||||||
|
@@ -201,6 +201,7 @@ intersphinx_mapping = {
|
|||||||
"python": ("https://docs.python.org/3/", None),
|
"python": ("https://docs.python.org/3/", None),
|
||||||
"tornado": ("https://www.tornadoweb.org/en/stable/", None),
|
"tornado": ("https://www.tornadoweb.org/en/stable/", None),
|
||||||
"jupyter-server": ("https://jupyter-server.readthedocs.io/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 -------------------------------------
|
# -- Options for the opengraph extension -------------------------------------
|
||||||
@@ -235,8 +236,12 @@ ogp_use_first_image = True
|
|||||||
# If you are basing changes off another branch/ commit, always change back
|
# If you are basing changes off another branch/ commit, always change back
|
||||||
# rediraffe_branch to main before pushing your changes upstream.
|
# rediraffe_branch to main before pushing your changes upstream.
|
||||||
#
|
#
|
||||||
rediraffe_branch = "main"
|
rediraffe_branch = os.environ.get("REDIRAFFE_BRANCH", "main")
|
||||||
rediraffe_redirects = "redirects.txt"
|
rediraffe_redirects = "redirects.txt"
|
||||||
|
|
||||||
|
# allow 80% match for autogenerated redirects
|
||||||
|
rediraffe_auto_redirect_perc = 80
|
||||||
|
|
||||||
# rediraffe_redirects = {
|
# rediraffe_redirects = {
|
||||||
# "old-file": "new-folder/new-file-name",
|
# "old-file": "new-folder/new-file-name",
|
||||||
# }
|
# }
|
||||||
|
@@ -130,8 +130,8 @@ configuration:
|
|||||||
jupyterhub -f testing/jupyterhub_config.py
|
jupyterhub -f testing/jupyterhub_config.py
|
||||||
```
|
```
|
||||||
|
|
||||||
The default JupyterHub [authenticator](https://jupyterhub.readthedocs.io/en/stable/reference/authenticators.html#the-default-pam-authenticator)
|
The default JupyterHub [authenticator](PAMAuthenticator)
|
||||||
& [spawner](https://jupyterhub.readthedocs.io/en/stable/api/spawner.html#localprocessspawner)
|
& [spawner](LocalProcessSpawner)
|
||||||
require your system to have user accounts for each user you want to log in to
|
require your system to have user accounts for each user you want to log in to
|
||||||
JupyterHub as.
|
JupyterHub as.
|
||||||
|
|
||||||
|
@@ -95,8 +95,14 @@ The Hub and its database are not involved in most requests to single-user server
|
|||||||
|
|
||||||
JupyterHub supports a variety of database backends via [SQLAlchemy][].
|
JupyterHub supports a variety of database backends via [SQLAlchemy][].
|
||||||
The default is sqlite, which works great for many cases, but you should be able to use many backends supported by SQLAlchemy.
|
The default is sqlite, which works great for many cases, but you should be able to use many backends supported by SQLAlchemy.
|
||||||
Usually, this will mean PostgreSQL or MySQL, both of which are well tested with JupyterHub.
|
Usually, this will mean PostgreSQL or MySQL, both of which are officially supported and well tested with JupyterHub, but others may work as well.
|
||||||
|
See [SQLAlchemy's docs][sqlalchemy-dialect] for how to connect to different database backends.
|
||||||
|
Doing so generally involves:
|
||||||
|
|
||||||
|
1. installing a Python package that provides a client implementation, and
|
||||||
|
2. setting [](JupyterHub.db_url) to connect to your database with the specified implementation
|
||||||
|
|
||||||
|
[sqlalchemy-dialect]: https://docs.sqlalchemy.org/en/20/dialects/
|
||||||
[sqlalchemy]: https://www.sqlalchemy.org
|
[sqlalchemy]: https://www.sqlalchemy.org
|
||||||
|
|
||||||
### Default backend: SQLite
|
### Default backend: SQLite
|
||||||
@@ -109,14 +115,16 @@ For production systems, SQLite has some disadvantages when used with JupyterHub:
|
|||||||
|
|
||||||
- `upgrade-db` may not always work, and you may need to start with a fresh database
|
- `upgrade-db` may not always work, and you may need to start with a fresh database
|
||||||
- `downgrade-db` **will not** work if you want to rollback to an earlier
|
- `downgrade-db` **will not** work if you want to rollback to an earlier
|
||||||
version, so backup the `jupyterhub.sqlite` file before upgrading
|
version, so backup the `jupyterhub.sqlite` file before upgrading (JupyterHub automatically creates a date-stamped backup file when upgrading sqlite)
|
||||||
|
|
||||||
The sqlite documentation provides a helpful page about [when to use SQLite and
|
The sqlite documentation provides a helpful page about [when to use SQLite and
|
||||||
where traditional RDBMS may be a better choice](https://sqlite.org/whentouse.html).
|
where traditional RDBMS may be a better choice](https://sqlite.org/whentouse.html).
|
||||||
|
|
||||||
### Picking your database backend (PostgreSQL, MySQL)
|
### Picking your database backend (PostgreSQL, MySQL)
|
||||||
|
|
||||||
When running a long term deployment or a production system, we recommend using a full-fledged relational database, such as [PostgreSQL](https://www.postgresql.org) or [MySQL](https://www.mysql.com), that supports the SQL `ALTER TABLE` statement.
|
When running a long term deployment or a production system, we recommend using a full-fledged relational database, such as [PostgreSQL](https://www.postgresql.org) or [MySQL](https://www.mysql.com), that supports the SQL `ALTER TABLE` statement, which is used in some database upgrade steps.
|
||||||
|
|
||||||
|
In general, you select your database backend with [](JupyterHub.db_url), and can further configure it (usually not necessary) with [](JupyterHub.db_kwargs).
|
||||||
|
|
||||||
## Notes and Tips
|
## Notes and Tips
|
||||||
|
|
||||||
@@ -132,14 +140,25 @@ multiple processes which might try to access the file at the same time.
|
|||||||
### PostgreSQL
|
### PostgreSQL
|
||||||
|
|
||||||
We recommend using PostgreSQL for production if you are unsure whether to use
|
We recommend using PostgreSQL for production if you are unsure whether to use
|
||||||
MySQL or PostgreSQL or if you do not have a strong preference. There is
|
MySQL or PostgreSQL or if you do not have a strong preference.
|
||||||
additional configuration required for MySQL that is not needed for PostgreSQL.
|
There is additional configuration required for MySQL that is not needed for PostgreSQL.
|
||||||
|
|
||||||
|
For example, to connect to a postgres database with psycopg2:
|
||||||
|
|
||||||
|
1. install psycopg2: `pip instal psycopg2` (or `psycopg2-binary` to avoid compilation, which is [not recommended for production][psycopg2-binary])
|
||||||
|
2. set authentication via environment variables `PGUSER` and `PGPASSWORD`
|
||||||
|
3. configure [](JupyterHub.db_url):
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.JupyterHub.db_url = "postgres+psycopg2://my-postgres-server:5432/my-db-name"
|
||||||
|
```
|
||||||
|
|
||||||
|
[psycopg2-binary]: https://www.psycopg.org/docs/install.html#psycopg-vs-psycopg-binary
|
||||||
|
|
||||||
### MySQL / MariaDB
|
### MySQL / MariaDB
|
||||||
|
|
||||||
- You should use the `pymysql` sqlalchemy provider (the other one, MySQLdb,
|
- You should probably use the `pymysql` or `mysqlclient` sqlalchemy provider, or another backend [recommended by sqlalchemy](https://docs.sqlalchemy.org/en/20/dialects/mysql.html#dialect-mysql)
|
||||||
isn't available for py3).
|
- You also need to set `pool_recycle` to some value (typically 60 - 300, JupyterHub will default to 60)
|
||||||
- You also need to set `pool_recycle` to some value (typically 60 - 300)
|
|
||||||
which depends on your MySQL setup. This is necessary since MySQL kills
|
which depends on your MySQL setup. This is necessary since MySQL kills
|
||||||
connections serverside if they've been idle for a while, and the connection
|
connections serverside if they've been idle for a while, and the connection
|
||||||
from the hub will be idle for longer than most connections. This behavior
|
from the hub will be idle for longer than most connections. This behavior
|
||||||
@@ -153,3 +172,12 @@ additional configuration required for MySQL that is not needed for PostgreSQL.
|
|||||||
correctly. Later versions of MariaDB and MySQL should set these values by
|
correctly. Later versions of MariaDB and MySQL should set these values by
|
||||||
default, as well as have a default `DYNAMIC` `row_format` and pose no trouble
|
default, as well as have a default `DYNAMIC` `row_format` and pose no trouble
|
||||||
to users.
|
to users.
|
||||||
|
|
||||||
|
For example, to connect to a mysql database with mysqlclient:
|
||||||
|
|
||||||
|
1. install mysqlclient: `pip install mysqlclient`
|
||||||
|
2. configure [](JupyterHub.db_url):
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.JupyterHub.db_url = "mysql+mysqldb://myuser:mypassword@my-sql-server:3306/my-db-name"
|
||||||
|
```
|
||||||
|
@@ -2,35 +2,75 @@
|
|||||||
|
|
||||||
## How do I share links to notebooks?
|
## 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,
|
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,
|
Your first instinct might be to copy the URL you see in the browser,
|
||||||
e.g. `hub.jupyter.org/user/yourname/notebooks/coolthing.ipynb`.
|
e.g. `jupyterhub.example/user/yourname/notebooks/coolthing.ipynb`,
|
||||||
However, let's break down what this URL means:
|
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_,
|
Unfortunately, 'share' means at least a few things to people in a JupyterHub context.
|
||||||
which means that sharing this URL is asking the person you share the link with
|
We'll cover 3 common cases here, when they are applicable, and what assumptions they make:
|
||||||
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.
|
|
||||||
|
|
||||||
**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,
|
### link to the same file on the visitor's server
|
||||||
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.
|
|
||||||
|
|
||||||
JupyterHub has a special URL that does exactly this!
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
In JupyterLab 2.0, this should also be the result of the "Copy Shareable Link"
|
**Assumption:** the same path on someone else's server is valid and points to the same file
|
||||||
action in the file browser.
|
|
||||||
|
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,
|
- **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
|
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,
|
- **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
|
- **Online communities**: Pangeo, Quantopian, mybinder.org, MathHub, Open Humans
|
||||||
- **Computing infrastructure providers**: NERSC, San Diego Supercomputing Center, Compute Canada
|
- **Computing infrastructure providers**: NERSC, San Diego Supercomputing Center, Compute Canada
|
||||||
- **Companies**: Capital One, SANDVIK code, Globus
|
- **Companies**: Capital One, SANDVIK code, Globus
|
||||||
@@ -130,7 +130,7 @@ level for several years, and makes a number of "default" security decisions that
|
|||||||
users.
|
users.
|
||||||
|
|
||||||
- For security considerations in the base JupyterHub application,
|
- 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
|
- For security considerations when deploying JupyterHub on Kubernetes, see the
|
||||||
[JupyterHub on Kubernetes security page](https://z2jh.jupyter.org/en/latest/security.html).
|
[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
|
### 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.
|
- **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.
|
- **Grading Assignments**: provide access to shared Jupyter Notebooks that may be used for management tasks such as grading assignments.
|
||||||
|
@@ -45,7 +45,7 @@ additional packages.
|
|||||||
|
|
||||||
## Configuring Jupyter and IPython
|
## Configuring Jupyter and IPython
|
||||||
|
|
||||||
[Jupyter](https://jupyter-notebook.readthedocs.io/en/stable/config_overview.html)
|
[Jupyter](https://jupyter-notebook.readthedocs.io/en/stable/configuring/config_overview.html)
|
||||||
and [IPython](https://ipython.readthedocs.io/en/stable/development/config.html)
|
and [IPython](https://ipython.readthedocs.io/en/stable/development/config.html)
|
||||||
have their own configuration systems.
|
have their own configuration systems.
|
||||||
|
|
||||||
@@ -212,13 +212,31 @@ By default, the single-user server launches JupyterLab,
|
|||||||
which is based on [Jupyter Server][].
|
which is based on [Jupyter Server][].
|
||||||
|
|
||||||
This is the default server when running JupyterHub ≥ 2.0.
|
This is the default server when running JupyterHub ≥ 2.0.
|
||||||
To switch to using the legacy Jupyter Notebook server, you can set the `JUPYTERHUB_SINGLEUSER_APP` environment variable
|
To switch to using the legacy Jupyter Notebook server (notebook < 7.0), you can set the `JUPYTERHUB_SINGLEUSER_APP` environment variable
|
||||||
(in the single-user environment) to:
|
(in the single-user environment) to:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export JUPYTERHUB_SINGLEUSER_APP='notebook.notebookapp.NotebookApp'
|
export JUPYTERHUB_SINGLEUSER_APP='notebook.notebookapp.NotebookApp'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
:::{note}
|
||||||
|
|
||||||
|
```
|
||||||
|
JUPYTERHUB_SINGLEUSER_APP='notebook.notebookapp.NotebookApp'
|
||||||
|
```
|
||||||
|
|
||||||
|
is only valid for notebook < 7. notebook v7 is based on jupyter-server,
|
||||||
|
and the default jupyter-server application must be used.
|
||||||
|
Selecting the new notebook UI is no longer a matter of selecting the server app to launch,
|
||||||
|
but only the default URL for users to visit.
|
||||||
|
To use notebook v7 with JupyterHub, leave the default singleuser app config alone (or specify `JUPYTERHUB_SINGLEUSER_APP=jupyter-server`) and set the default _URL_ for user servers:
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.Spawner.default_url = '/tree/'
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
[jupyter server]: https://jupyter-server.readthedocs.io
|
[jupyter server]: https://jupyter-server.readthedocs.io
|
||||||
[jupyter notebook]: https://jupyter-notebook.readthedocs.io
|
[jupyter notebook]: https://jupyter-notebook.readthedocs.io
|
||||||
|
|
||||||
|
@@ -33,36 +33,13 @@ such as:
|
|||||||
To send requests using the JupyterHub API, you must pass an API token with
|
To send requests using the JupyterHub API, you must pass an API token with
|
||||||
the request.
|
the request.
|
||||||
|
|
||||||
The preferred way of generating an API token is by running:
|
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,
|
||||||
```bash
|
or at the URL `/hub/token`.
|
||||||
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:
|
|
||||||
|
|
||||||
:::{figure-md}
|
:::{figure-md}
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
JupyterHub's API token page
|
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
|
## Assigning permissions to a token
|
||||||
|
|
||||||
Prior to JupyterHub 2.0, there were two levels of permissions:
|
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 |
@@ -1,23 +1,54 @@
|
|||||||
# This file contains rediraffe redirects as generated from the docs/source/conf.py file
|
# This file contains rediraffe redirects as generated from the docs/source/conf.py file
|
||||||
# For more information, see rediraffe configuration in the conf.py file.
|
# For more information, see rediraffe configuration in the conf.py file.
|
||||||
|
|
||||||
|
|
||||||
"changelog.md" "reference/changelog.md"
|
"changelog.md" "reference/changelog.md"
|
||||||
|
"contributor-list.md" "contributing/contributor-list.md"
|
||||||
|
"gallery-jhub-deployments.md" "reference/gallery-jhub-deployments.md"
|
||||||
|
"installation-basics.md" "tutorial/installation-basics.md"
|
||||||
|
"quickstart.md" "tutorial/quickstart.md"
|
||||||
|
"quickstart-docker.md" "tutorial/quickstart-docker.md"
|
||||||
|
"troubleshooting.md" "faq/troubleshooting.md"
|
||||||
|
|
||||||
|
"admin/capacity-planning.md" "explanation/capacity-planning.md"
|
||||||
|
"admin/log-messages.md" "howto/log-messages.md"
|
||||||
|
"admin/upgrading.md" "howto/upgrading.md"
|
||||||
|
|
||||||
|
"events/index.md" "reference/event-logging.md"
|
||||||
|
|
||||||
|
"getting-started/authenticators-users-basics.md" "tutorial/getting-started/authenticators-users-basics.md"
|
||||||
|
"getting-started/config-basics.md" "tutorial/getting-started/config-basics.md"
|
||||||
"getting-started/faq.md" "faq/faq.md"
|
"getting-started/faq.md" "faq/faq.md"
|
||||||
|
"getting-started/institutional-faq.md" "faq/institutional-faq.md"
|
||||||
|
"getting-started/networking-basics.md" "tutorial/getting-started/networking-basics.md"
|
||||||
|
"getting-started/services-basics.md" "tutorial/getting-started/services-basics.md"
|
||||||
|
"getting-started/spawners-basics.md" "tutorial/getting-started/spawners-basics.md"
|
||||||
|
|
||||||
"reference/api-only.md" "howto/api-only.md"
|
"reference/api-only.md" "howto/api-only.md"
|
||||||
"reference/config-ghoauth.md" "howto/configuration/config-ghoauth.md"
|
"reference/config-ghoauth.md" "howto/configuration/config-ghoauth.md"
|
||||||
"reference/config-proxy.md" "howto/configuration/config-proxy.md"
|
"reference/config-proxy.md" "howto/configuration/config-proxy.md"
|
||||||
"admin/log-messages.md" "howto/log-messages.md"
|
"reference/database.md" "explanation/database.md"
|
||||||
|
"reference/oauth.md" "explanation/oauth.md"
|
||||||
"reference/proxy.md" "howto/proxy.md"
|
"reference/proxy.md" "howto/proxy.md"
|
||||||
"reference/templates.md" "howto/templates.md"
|
"reference/templates.md" "howto/templates.md"
|
||||||
"quickstart-docker.md" "tutorial/quickstart-docker.md"
|
|
||||||
"reference/config-examples.md" "howto/index.md"
|
"reference/config-examples.md" "howto/index.md"
|
||||||
"getting-started/institutional-faq.md" "faq/institutional-faq.md"
|
|
||||||
"troubleshooting.md" "faq/troubleshooting.md"
|
|
||||||
"reference/config-sudo.md" "howto/configuration/config-sudo.md"
|
"reference/config-sudo.md" "howto/configuration/config-sudo.md"
|
||||||
"reference/config-user-env.md" "howto/configuration/config-user-env.md"
|
"reference/config-user-env.md" "howto/configuration/config-user-env.md"
|
||||||
"reference/rest.md" "howto/rest.md"
|
"reference/rest.md" "howto/rest.md"
|
||||||
"reference/separate-proxy.md" "howto/separate-proxy.md"
|
"reference/separate-proxy.md" "howto/separate-proxy.md"
|
||||||
"admin/upgrading.md" "howto/upgrading.md"
|
"reference/server-api.md" "tutorial/server-api.md"
|
||||||
"installation-basics.md" "tutorial/installation-basics.md"
|
"reference/websecurity.md" "explanation/websecurity.md"
|
||||||
"quickstart.md" "tutorial/quickstart.md"
|
|
||||||
"events/index.md" "reference/event-logging.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
|
||||||
|
# (e.g. with `make rediraffewritediff`)
|
||||||
|
@@ -273,7 +273,7 @@ c.Spawner.auth_state_hook = auth_state_hook
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
Some identity providers may have their own concept of group membership that you would like to preserve in JupyterHub.
|
Some identity providers may have their own concept of group membership that you would like to preserve in JupyterHub.
|
||||||
This is now possible with `Authenticator.managed_groups`.
|
This is now possible with `Authenticator.manage_groups`.
|
||||||
|
|
||||||
You can set the config:
|
You can set the config:
|
||||||
|
|
||||||
|
@@ -8,9 +8,92 @@ command line for details.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### 4.0 (beta) - 2023-XX-YY
|
## 4.0
|
||||||
|
|
||||||
|
### 4.0.2 - 2023-08-10
|
||||||
|
|
||||||
|
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/4.0.1...4.0.2))
|
||||||
|
|
||||||
|
#### Enhancements made
|
||||||
|
|
||||||
|
- avoid counting failed requests to not-running servers as 'activity' [#4491](https://github.com/jupyterhub/jupyterhub/pull/4491) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||||
|
- improve permission-denied errors for various cases [#4489](https://github.com/jupyterhub/jupyterhub/pull/4489) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||||
|
|
||||||
|
#### Bugs fixed
|
||||||
|
|
||||||
|
- set root_dir when using singleuser extension [#4503](https://github.com/jupyterhub/jupyterhub/pull/4503) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio), [@manics](https://github.com/manics))
|
||||||
|
- Allow setting custom log_function in tornado_settings in SingleUserServer [#4475](https://github.com/jupyterhub/jupyterhub/pull/4475) ([@grios-stratio](https://github.com/grios-stratio), [@minrk](https://github.com/minrk))
|
||||||
|
|
||||||
|
#### Documentation improvements
|
||||||
|
|
||||||
|
- doc: update notebook config URL [#4523](https://github.com/jupyterhub/jupyterhub/pull/4523) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||||
|
- document how to use notebook v7 with jupyterhub [#4522](https://github.com/jupyterhub/jupyterhub/pull/4522) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||||
|
|
||||||
|
#### 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-06-08&to=2023-08-10&type=c))
|
||||||
|
|
||||||
|
@agelosnm ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aagelosnm+updated%3A2023-06-08..2023-08-10&type=Issues)) | @consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2023-06-08..2023-08-10&type=Issues)) | @diocas ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Adiocas+updated%3A2023-06-08..2023-08-10&type=Issues)) | @grios-stratio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Agrios-stratio+updated%3A2023-06-08..2023-08-10&type=Issues)) | @jhgoebbert ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ajhgoebbert+updated%3A2023-06-08..2023-08-10&type=Issues)) | @jtpio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ajtpio+updated%3A2023-06-08..2023-08-10&type=Issues)) | @kosmonavtus ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Akosmonavtus+updated%3A2023-06-08..2023-08-10&type=Issues)) | @kreuzert ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Akreuzert+updated%3A2023-06-08..2023-08-10&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2023-06-08..2023-08-10&type=Issues)) | @martinRenou ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AmartinRenou+updated%3A2023-06-08..2023-08-10&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2023-06-08..2023-08-10&type=Issues)) | @opoplawski ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aopoplawski+updated%3A2023-06-08..2023-08-10&type=Issues)) | @Ph0tonic ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3APh0tonic+updated%3A2023-06-08..2023-08-10&type=Issues)) | @sgaist ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Asgaist+updated%3A2023-06-08..2023-08-10&type=Issues)) | @trungleduc ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Atrungleduc+updated%3A2023-06-08..2023-08-10&type=Issues)) | @yuvipanda ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ayuvipanda+updated%3A2023-06-08..2023-08-10&type=Issues))
|
||||||
|
|
||||||
|
### 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.
|
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:
|
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.
|
1. Groups can now have 'properties', editable via the admin page, which can be used by Spawners for their operations.
|
||||||
@@ -24,7 +107,7 @@ There are three major changes that _should_ be invisible to most users:
|
|||||||
|
|
||||||
In addition to these, thanks to contributions from this years Outreachy interns, we have reorganized the documentation according to [diataxis](https://diataxis.fr), improved accessibility of JupyterHub pages, and improved testing.
|
In addition to these, thanks to contributions from this years Outreachy interns, we have reorganized the documentation according to [diataxis](https://diataxis.fr), improved accessibility of JupyterHub pages, and improved testing.
|
||||||
|
|
||||||
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/3.1.0...HEAD))
|
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/3.1.0...4.0.0))
|
||||||
|
|
||||||
#### API and Breaking Changes
|
#### API and Breaking Changes
|
||||||
|
|
||||||
@@ -33,18 +116,21 @@ In addition to these, thanks to contributions from this years Outreachy interns,
|
|||||||
|
|
||||||
#### New features added
|
#### New features added
|
||||||
|
|
||||||
|
- add Spawner.server_token_scopes config [#4400](https://github.com/jupyterhub/jupyterhub/pull/4400) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||||
- Make singleuser server-extension default [#4354](https://github.com/jupyterhub/jupyterhub/pull/4354) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
- Make singleuser server-extension default [#4354](https://github.com/jupyterhub/jupyterhub/pull/4354) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||||
- singleuser auth as server extension [#3888](https://github.com/jupyterhub/jupyterhub/pull/3888) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
- singleuser auth as server extension [#3888](https://github.com/jupyterhub/jupyterhub/pull/3888) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||||
- Dynamic table for changing customizable properties of groups [#3651](https://github.com/jupyterhub/jupyterhub/pull/3651) ([@vladfreeze](https://github.com/vladfreeze), [@minrk](https://github.com/minrk), [@naatebarber](https://github.com/naatebarber), [@manics](https://github.com/manics))
|
- Dynamic table for changing customizable properties of groups [#3651](https://github.com/jupyterhub/jupyterhub/pull/3651) ([@vladfreeze](https://github.com/vladfreeze), [@minrk](https://github.com/minrk), [@naatebarber](https://github.com/naatebarber), [@manics](https://github.com/manics))
|
||||||
|
|
||||||
#### Enhancements made
|
#### Enhancements made
|
||||||
|
|
||||||
|
- admin page: improve display of long lists (groups, etc.) [#4417](https://github.com/jupyterhub/jupyterhub/pull/4417) ([@manics](https://github.com/manics), [@minrk](https://github.com/minrk), [@ryanlovett](https://github.com/ryanlovett))
|
||||||
- add a few more buckets for server_spawn_duration_seconds [#4352](https://github.com/jupyterhub/jupyterhub/pull/4352) ([@shaneknapp](https://github.com/shaneknapp), [@yuvipanda](https://github.com/yuvipanda))
|
- add a few more buckets for server_spawn_duration_seconds [#4352](https://github.com/jupyterhub/jupyterhub/pull/4352) ([@shaneknapp](https://github.com/shaneknapp), [@yuvipanda](https://github.com/yuvipanda))
|
||||||
- Improve contrast on muted text [#4326](https://github.com/jupyterhub/jupyterhub/pull/4326) ([@bl-aire](https://github.com/bl-aire), [@minrk](https://github.com/minrk))
|
- Improve contrast on muted text [#4326](https://github.com/jupyterhub/jupyterhub/pull/4326) ([@bl-aire](https://github.com/bl-aire), [@minrk](https://github.com/minrk))
|
||||||
- Standardize styling on input fields by moving common form CSS to page.less [#4294](https://github.com/jupyterhub/jupyterhub/pull/4294) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
- Standardize styling on input fields by moving common form CSS to page.less [#4294](https://github.com/jupyterhub/jupyterhub/pull/4294) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||||
|
|
||||||
#### Bugs fixed
|
#### Bugs fixed
|
||||||
|
|
||||||
|
- make sure named server URLs include trailing slash [#4402](https://github.com/jupyterhub/jupyterhub/pull/4402) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||||
- fix inclusion of singleuser/templates/page.html in wheel [#4387](https://github.com/jupyterhub/jupyterhub/pull/4387) ([@consideRatio](https://github.com/consideRatio), [@minrk](https://github.com/minrk))
|
- fix inclusion of singleuser/templates/page.html in wheel [#4387](https://github.com/jupyterhub/jupyterhub/pull/4387) ([@consideRatio](https://github.com/consideRatio), [@minrk](https://github.com/minrk))
|
||||||
- exponential_backoff: preserve jitter when max_wait is reached [#4383](https://github.com/jupyterhub/jupyterhub/pull/4383) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
- exponential_backoff: preserve jitter when max_wait is reached [#4383](https://github.com/jupyterhub/jupyterhub/pull/4383) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||||
- admin panel: fix condition for start/stop buttons on user servers [#4365](https://github.com/jupyterhub/jupyterhub/pull/4365) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
- admin panel: fix condition for start/stop buttons on user servers [#4365](https://github.com/jupyterhub/jupyterhub/pull/4365) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||||
@@ -57,6 +143,13 @@ In addition to these, thanks to contributions from this years Outreachy interns,
|
|||||||
|
|
||||||
#### Maintenance and upkeep improvements
|
#### Maintenance and upkeep improvements
|
||||||
|
|
||||||
|
- add remaining redirects for docs reorg [#4423](https://github.com/jupyterhub/jupyterhub/pull/4423) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||||
|
- Disable dev traitlets [#4419](https://github.com/jupyterhub/jupyterhub/pull/4419) ([@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio))
|
||||||
|
- dependabot: rename to .yaml [#4409](https://github.com/jupyterhub/jupyterhub/pull/4409) ([@consideRatio](https://github.com/consideRatio))
|
||||||
|
- dependabot: fix syntax error of not using quotes for ##:## [#4408](https://github.com/jupyterhub/jupyterhub/pull/4408) ([@consideRatio](https://github.com/consideRatio))
|
||||||
|
- dependabot: monthly updates of github actions [#4403](https://github.com/jupyterhub/jupyterhub/pull/4403) ([@consideRatio](https://github.com/consideRatio), [@minrk](https://github.com/minrk))
|
||||||
|
- Refresh 4.0 changelog [#4396](https://github.com/jupyterhub/jupyterhub/pull/4396) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||||
|
- Reduce size of jupyterhub image [#4394](https://github.com/jupyterhub/jupyterhub/pull/4394) ([@alekseyolg](https://github.com/alekseyolg), [@minrk](https://github.com/minrk))
|
||||||
- Selenium: updating test_oauth_page [#4393](https://github.com/jupyterhub/jupyterhub/pull/4393) ([@mouse1203](https://github.com/mouse1203), [@minrk](https://github.com/minrk))
|
- Selenium: updating test_oauth_page [#4393](https://github.com/jupyterhub/jupyterhub/pull/4393) ([@mouse1203](https://github.com/mouse1203), [@minrk](https://github.com/minrk))
|
||||||
- avoid warning on engine_connect listener [#4392](https://github.com/jupyterhub/jupyterhub/pull/4392) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio), [@manics](https://github.com/manics))
|
- avoid warning on engine_connect listener [#4392](https://github.com/jupyterhub/jupyterhub/pull/4392) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio), [@manics](https://github.com/manics))
|
||||||
- remove pin from singleuser [#4379](https://github.com/jupyterhub/jupyterhub/pull/4379) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio), [@mathbunnyru](https://github.com/mathbunnyru))
|
- remove pin from singleuser [#4379](https://github.com/jupyterhub/jupyterhub/pull/4379) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio), [@mathbunnyru](https://github.com/mathbunnyru))
|
||||||
@@ -81,6 +174,9 @@ In addition to these, thanks to contributions from this years Outreachy interns,
|
|||||||
|
|
||||||
#### Documentation improvements
|
#### Documentation improvements
|
||||||
|
|
||||||
|
- Fix variable spelling. [#4398](https://github.com/jupyterhub/jupyterhub/pull/4398) ([@ryanlovett](https://github.com/ryanlovett), [@manics](https://github.com/manics))
|
||||||
|
- Remove bracket around link text without address [#4416](https://github.com/jupyterhub/jupyterhub/pull/4416) ([@crazytan](https://github.com/crazytan), [@minrk](https://github.com/minrk))
|
||||||
|
- add some more detail and examples to database doc [#4399](https://github.com/jupyterhub/jupyterhub/pull/4399) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||||
- Add emphasis about role loading and hub restarts. [#4390](https://github.com/jupyterhub/jupyterhub/pull/4390) ([@ryanlovett](https://github.com/ryanlovett), [@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
- Add emphasis about role loading and hub restarts. [#4390](https://github.com/jupyterhub/jupyterhub/pull/4390) ([@ryanlovett](https://github.com/ryanlovett), [@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||||
- Re-enable links to REST API [#4386](https://github.com/jupyterhub/jupyterhub/pull/4386) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
- Re-enable links to REST API [#4386](https://github.com/jupyterhub/jupyterhub/pull/4386) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||||
- reduce nested hierarchy in docs organization [#4377](https://github.com/jupyterhub/jupyterhub/pull/4377) ([@alwasega](https://github.com/alwasega), [@minrk](https://github.com/minrk))
|
- reduce nested hierarchy in docs organization [#4377](https://github.com/jupyterhub/jupyterhub/pull/4377) ([@alwasega](https://github.com/alwasega), [@minrk](https://github.com/minrk))
|
||||||
@@ -119,9 +215,9 @@ In addition to these, thanks to contributions from this years Outreachy interns,
|
|||||||
The following people contributed discussions, new ideas, code and documentation contributions, and review.
|
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).
|
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=2022-12-05&to=2023-03-15&type=c))
|
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2022-12-05&to=2023-04-20&type=c))
|
||||||
|
|
||||||
@3coins ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3A3coins+updated%3A2022-12-05..2023-03-15&type=Issues)) | @ajcollett ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aajcollett+updated%3A2022-12-05..2023-03-15&type=Issues)) | @ajpower ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aajpower+updated%3A2022-12-05..2023-03-15&type=Issues)) | @alwasega ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aalwasega+updated%3A2022-12-05..2023-03-15&type=Issues)) | @betatim ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Abetatim+updated%3A2022-12-05..2023-03-15&type=Issues)) | @bl-aire ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Abl-aire+updated%3A2022-12-05..2023-03-15&type=Issues)) | @choldgraf ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Acholdgraf+updated%3A2022-12-05..2023-03-15&type=Issues)) | @consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2022-12-05..2023-03-15&type=Issues)) | @dependabot ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Adependabot+updated%3A2022-12-05..2023-03-15&type=Issues)) | @fperez ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Afperez+updated%3A2022-12-05..2023-03-15&type=Issues)) | @GeorgianaElena ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AGeorgianaElena+updated%3A2022-12-05..2023-03-15&type=Issues)) | @julietKiloRomeo ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AjulietKiloRomeo+updated%3A2022-12-05..2023-03-15&type=Issues)) | @ktaletsk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aktaletsk+updated%3A2022-12-05..2023-03-15&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2022-12-05..2023-03-15&type=Issues)) | @mathbunnyru ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amathbunnyru+updated%3A2022-12-05..2023-03-15&type=Issues)) | @meeseeksdev ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ameeseeksdev+updated%3A2022-12-05..2023-03-15&type=Issues)) | @meeseeksmachine ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ameeseeksmachine+updated%3A2022-12-05..2023-03-15&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2022-12-05..2023-03-15&type=Issues)) | @mouse1203 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amouse1203+updated%3A2022-12-05..2023-03-15&type=Issues)) | @naatebarber ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Anaatebarber+updated%3A2022-12-05..2023-03-15&type=Issues)) | @pnasrat ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Apnasrat+updated%3A2022-12-05..2023-03-15&type=Issues)) | @pre-commit-ci ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Apre-commit-ci+updated%3A2022-12-05..2023-03-15&type=Issues)) | @ryanlovett ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aryanlovett+updated%3A2022-12-05..2023-03-15&type=Issues)) | @sgibson91 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Asgibson91+updated%3A2022-12-05..2023-03-15&type=Issues)) | @shaneknapp ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ashaneknapp+updated%3A2022-12-05..2023-03-15&type=Issues)) | @Sheila-nk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3ASheila-nk+updated%3A2022-12-05..2023-03-15&type=Issues)) | @stevejpurves ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Astevejpurves+updated%3A2022-12-05..2023-03-15&type=Issues)) | @TaofeeqatDev ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3ATaofeeqatDev+updated%3A2022-12-05..2023-03-15&type=Issues)) | @vladfreeze ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Avladfreeze+updated%3A2022-12-05..2023-03-15&type=Issues)) | @yuvipanda ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ayuvipanda+updated%3A2022-12-05..2023-03-15&type=Issues))
|
@3coins ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3A3coins+updated%3A2022-12-05..2023-04-20&type=Issues)) | @ajcollett ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aajcollett+updated%3A2022-12-05..2023-04-20&type=Issues)) | @ajpower ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aajpower+updated%3A2022-12-05..2023-04-20&type=Issues)) | @alekseyolg ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aalekseyolg+updated%3A2022-12-05..2023-04-20&type=Issues)) | @alwasega ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aalwasega+updated%3A2022-12-05..2023-04-20&type=Issues)) | @betatim ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Abetatim+updated%3A2022-12-05..2023-04-20&type=Issues)) | @bl-aire ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Abl-aire+updated%3A2022-12-05..2023-04-20&type=Issues)) | @choldgraf ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Acholdgraf+updated%3A2022-12-05..2023-04-20&type=Issues)) | @consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2022-12-05..2023-04-20&type=Issues)) | @crazytan ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Acrazytan+updated%3A2022-12-05..2023-04-20&type=Issues)) | @dependabot ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Adependabot+updated%3A2022-12-05..2023-04-20&type=Issues)) | @fperez ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Afperez+updated%3A2022-12-05..2023-04-20&type=Issues)) | @GeorgianaElena ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AGeorgianaElena+updated%3A2022-12-05..2023-04-20&type=Issues)) | @julietKiloRomeo ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AjulietKiloRomeo+updated%3A2022-12-05..2023-04-20&type=Issues)) | @ktaletsk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aktaletsk+updated%3A2022-12-05..2023-04-20&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2022-12-05..2023-04-20&type=Issues)) | @mathbunnyru ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amathbunnyru+updated%3A2022-12-05..2023-04-20&type=Issues)) | @meeseeksdev ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ameeseeksdev+updated%3A2022-12-05..2023-04-20&type=Issues)) | @meeseeksmachine ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ameeseeksmachine+updated%3A2022-12-05..2023-04-20&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2022-12-05..2023-04-20&type=Issues)) | @mouse1203 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amouse1203+updated%3A2022-12-05..2023-04-20&type=Issues)) | @naatebarber ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Anaatebarber+updated%3A2022-12-05..2023-04-20&type=Issues)) | @pnasrat ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Apnasrat+updated%3A2022-12-05..2023-04-20&type=Issues)) | @pre-commit-ci ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Apre-commit-ci+updated%3A2022-12-05..2023-04-20&type=Issues)) | @ryanlovett ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aryanlovett+updated%3A2022-12-05..2023-04-20&type=Issues)) | @sgibson91 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Asgibson91+updated%3A2022-12-05..2023-04-20&type=Issues)) | @shaneknapp ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ashaneknapp+updated%3A2022-12-05..2023-04-20&type=Issues)) | @Sheila-nk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3ASheila-nk+updated%3A2022-12-05..2023-04-20&type=Issues)) | @stevejpurves ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Astevejpurves+updated%3A2022-12-05..2023-04-20&type=Issues)) | @TaofeeqatDev ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3ATaofeeqatDev+updated%3A2022-12-05..2023-04-20&type=Issues)) | @vladfreeze ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Avladfreeze+updated%3A2022-12-05..2023-04-20&type=Issues)) | @yuvipanda ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ayuvipanda+updated%3A2022-12-05..2023-04-20&type=Issues))
|
||||||
|
|
||||||
## 3.1
|
## 3.1
|
||||||
|
|
||||||
@@ -1357,7 +1453,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))
|
- `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))
|
- 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))
|
- `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))
|
- 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))
|
- 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))
|
- 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
|
> 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.
|
The following contains the output of that command for reference.
|
||||||
|
|
||||||
```{eval-rst}
|
```{eval-rst}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# Event logging and telemetry
|
# Event logging and telemetry
|
||||||
|
|
||||||
JupyterHub can be configured to record structured events from a running server using Jupyter's [Telemetry System]. The types of events that JupyterHub emits are defined by [JSON schemas] listed at the bottom of this [page].
|
JupyterHub can be configured to record structured events from a running server using Jupyter's [Telemetry System]. The types of events that JupyterHub emits are defined by [JSON schemas] listed at the bottom of this page.
|
||||||
|
|
||||||
## How to emit events
|
## How to emit events
|
||||||
|
|
||||||
|
@@ -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
|
- [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
|
||||||
|
|
||||||
[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.
|
[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
|
In [version 0.8.0](changelog), a TOKEN request page for
|
||||||
generating an API token is available from the JupyterHub user interface:
|
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.
|
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,51 +25,49 @@
|
|||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
|
"\\.(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"
|
"\\.(css|less)$": "identity-obj-proxy"
|
||||||
}
|
},
|
||||||
|
"testEnvironment": "jsdom"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "^4.5.3",
|
"bootstrap": "^5.2.3",
|
||||||
"history": "^5.0.0",
|
"history": "^5.3.0",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash": "^4.17.21",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.2",
|
||||||
"react-bootstrap": "^2.1.1",
|
"react-bootstrap": "^2.7.4",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.2",
|
||||||
"react-icons": "^4.1.0",
|
"react-icons": "^4.8.0",
|
||||||
"react-multi-select-component": "^3.0.7",
|
"react-multi-select-component": "^4.3.4",
|
||||||
"react-object-table-viewer": "^1.0.7",
|
"react-redux": "^7.2.8",
|
||||||
"react-redux": "^7.2.2",
|
"react-router-dom": "^5.3.4",
|
||||||
"react-router": "^5.2.0",
|
"recompose": "npm:react-recompose@^0.33.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"redux": "^4.2.1",
|
||||||
"recompose": "npm:react-recompose@^0.31.2",
|
"regenerator-runtime": "^0.13.11"
|
||||||
"redux": "^4.0.5",
|
|
||||||
"regenerator-runtime": "^0.13.9"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.3",
|
"@babel/core": "^7.21.4",
|
||||||
"@babel/preset-env": "^7.12.11",
|
"@babel/preset-env": "^7.21.4",
|
||||||
"@babel/preset-react": "^7.12.10",
|
"@babel/preset-react": "^7.18.6",
|
||||||
"@testing-library/jest-dom": "^5.15.1",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^12.1.2",
|
"@testing-library/react": "^12.1.5",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@webpack-cli/serve": "^1.7.0",
|
"@webpack-cli/serve": "^2.0.1",
|
||||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.5",
|
"babel-jest": "^29.5.0",
|
||||||
"babel-jest": "^26.6.3",
|
"babel-loader": "^9.1.2",
|
||||||
"babel-loader": "^8.2.1",
|
"css-loader": "^6.7.3",
|
||||||
"css-loader": "^5.0.1",
|
"eslint": "^8.38.0",
|
||||||
"enzyme": "^3.11.0",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint": "^7.18.0",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
"eslint-plugin-prettier": "^3.3.1",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"eslint-plugin-react": "^7.22.0",
|
|
||||||
"eslint-plugin-unused-imports": "^1.1.1",
|
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^26.6.3",
|
"jest": "^29.5.0",
|
||||||
"prettier": "^2.2.1",
|
"jest-environment-jsdom": "^29.5.0",
|
||||||
"sinon": "^13.0.1",
|
"prettier": "^2.8.7",
|
||||||
"style-loader": "^2.0.0",
|
"sinon": "^15.0.3",
|
||||||
"webpack": "^5.76.0",
|
"style-loader": "^3.3.2",
|
||||||
"webpack-cli": "^4.10.0",
|
"webpack": "^5.79.0",
|
||||||
"webpack-dev-server": "^4.9.3"
|
"webpack-cli": "^5.0.1",
|
||||||
|
"webpack-dev-server": "^4.13.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
jsx/src/components/ReactObjectTableViewer/LICENSE
Normal file
21
jsx/src/components/ReactObjectTableViewer/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2021 jinkwon.lee<uzmystic@gmail.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
@@ -0,0 +1,65 @@
|
|||||||
|
// Originally copied from
|
||||||
|
// https://github.com/jinkwon/react-object-table-viewer/blob/f29827028fad547a0a17e044567cf1486849fb7a/src/ReactObjectTableViewer.tsx
|
||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
const ReactObjectTableViewer = (props) => {
|
||||||
|
const opt = props;
|
||||||
|
|
||||||
|
const data = opt.data;
|
||||||
|
const keys = Object.keys(data || {}) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table
|
||||||
|
className={opt.className}
|
||||||
|
style={{
|
||||||
|
...opt.style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
{keys.map((k, key) => {
|
||||||
|
const val = data[k];
|
||||||
|
const isObject = typeof val === "object";
|
||||||
|
const isElement = React.isValidElement(val);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={key}>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
...opt.keyStyle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{k}
|
||||||
|
</th>
|
||||||
|
{isObject && (
|
||||||
|
<td>
|
||||||
|
{isElement && val}
|
||||||
|
{!isElement && <ReactObjectTableViewer {...opt} data={val} />}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
{!isObject && (
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
...opt.valueStyle,
|
||||||
|
}}
|
||||||
|
>{`${val}`}</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ReactObjectTableViewer.propTypes = {
|
||||||
|
data: PropTypes.object,
|
||||||
|
style: PropTypes.objectOf(PropTypes.string),
|
||||||
|
keyStyle: PropTypes.objectOf(PropTypes.string),
|
||||||
|
valueStyle: PropTypes.objectOf(PropTypes.string),
|
||||||
|
className: PropTypes.string,
|
||||||
|
layout: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReactObjectTableViewer;
|
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState, Fragment } from "react";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
CardGroup,
|
CardGroup,
|
||||||
Collapse,
|
Collapse,
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
import ReactObjectTableViewer from "react-object-table-viewer";
|
import ReactObjectTableViewer from "../ReactObjectTableViewer/ReactObjectTableViewer";
|
||||||
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
|
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
|
||||||
@@ -29,6 +29,13 @@ const AccessServerButton = ({ url }) => (
|
|||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const RowListItem = ({ text }) => (
|
||||||
|
<span className="server-dashboard-row-list-item">{text}</span>
|
||||||
|
);
|
||||||
|
RowListItem.propTypes = {
|
||||||
|
text: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
const ServerDashboard = (props) => {
|
const ServerDashboard = (props) => {
|
||||||
let base_url = window.base_url || "/";
|
let base_url = window.base_url || "/";
|
||||||
// sort methods
|
// sort methods
|
||||||
@@ -67,6 +74,7 @@ const ServerDashboard = (props) => {
|
|||||||
shutdownHub,
|
shutdownHub,
|
||||||
startServer,
|
startServer,
|
||||||
stopServer,
|
stopServer,
|
||||||
|
deleteServer,
|
||||||
startAll,
|
startAll,
|
||||||
stopAll,
|
stopAll,
|
||||||
history,
|
history,
|
||||||
@@ -160,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 }) => {
|
const StartServerButton = ({ serverName, userName }) => {
|
||||||
var [isDisabled, setIsDisabled] = useState(false);
|
var [isDisabled, setIsDisabled] = useState(false);
|
||||||
return (
|
return (
|
||||||
@@ -236,8 +288,13 @@ const ServerDashboard = (props) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
// cast arrays (e.g. roles, groups) to string
|
value = (
|
||||||
value = value.sort().join(", ");
|
<Fragment>
|
||||||
|
{value.sort().flatMap((v) => (
|
||||||
|
<RowListItem text={v} />
|
||||||
|
))}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
result[key] = value;
|
result[key] = value;
|
||||||
return result;
|
return result;
|
||||||
@@ -266,7 +323,11 @@ const ServerDashboard = (props) => {
|
|||||||
const userServerName = user.name + serverNameDash;
|
const userServerName = user.name + serverNameDash;
|
||||||
const open = collapseStates[userServerName] || false;
|
const open = collapseStates[userServerName] || false;
|
||||||
return [
|
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">
|
<td data-testid="user-row-name">
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
@@ -312,6 +373,10 @@ const ServerDashboard = (props) => {
|
|||||||
userName={user.name}
|
userName={user.name}
|
||||||
style={{ marginRight: 20 }}
|
style={{ marginRight: 20 }}
|
||||||
/>
|
/>
|
||||||
|
<DeleteServerButton
|
||||||
|
serverName={server.name}
|
||||||
|
userName={user.name}
|
||||||
|
/>
|
||||||
<a
|
<a
|
||||||
href={`${base_url}spawn/${user.name}${
|
href={`${base_url}spawn/${user.name}${
|
||||||
server.name ? "/" + server.name : ""
|
server.name ? "/" + server.name : ""
|
||||||
@@ -570,6 +635,7 @@ ServerDashboard.propTypes = {
|
|||||||
shutdownHub: PropTypes.func,
|
shutdownHub: PropTypes.func,
|
||||||
startServer: PropTypes.func,
|
startServer: PropTypes.func,
|
||||||
stopServer: PropTypes.func,
|
stopServer: PropTypes.func,
|
||||||
|
deleteServer: PropTypes.func,
|
||||||
startAll: PropTypes.func,
|
startAll: PropTypes.func,
|
||||||
stopAll: PropTypes.func,
|
stopAll: PropTypes.func,
|
||||||
dispatch: PropTypes.func,
|
dispatch: PropTypes.func,
|
||||||
|
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { act } from "react-dom/test-utils";
|
import { act } from "react-dom/test-utils";
|
||||||
import userEvent from "@testing-library/user-event";
|
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 { HashRouter, Switch } from "react-router-dom";
|
||||||
import { Provider, useSelector } from "react-redux";
|
import { Provider, useSelector } from "react-redux";
|
||||||
import { createStore } from "redux";
|
import { createStore } from "redux";
|
||||||
@@ -43,6 +43,31 @@ var mockAsync = (data) =>
|
|||||||
var mockAsyncRejection = () =>
|
var mockAsyncRejection = () =>
|
||||||
jest.fn().mockImplementation(() => Promise.reject());
|
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 = () =>
|
var mockAppState = () =>
|
||||||
Object.assign({}, initialState, {
|
Object.assign({}, initialState, {
|
||||||
user_data: [
|
user_data: [
|
||||||
@@ -78,19 +103,7 @@ var mockAppState = () =>
|
|||||||
pending: null,
|
pending: null,
|
||||||
created: "2020-12-07T18:46:27.115528Z",
|
created: "2020-12-07T18:46:27.115528Z",
|
||||||
last_activity: "2020-12-07T20:43:51.013613Z",
|
last_activity: "2020-12-07T20:43:51.013613Z",
|
||||||
servers: {
|
servers: 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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
user_page: {
|
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 foo = screen.getByTestId("user-name-div-foo");
|
||||||
let bar = screen.getByTestId("user-name-div-bar");
|
let bar = screen.getByTestId("user-name-div-bar");
|
||||||
|
let bar_server = screen.getByTestId("user-name-div-bar-servername");
|
||||||
|
|
||||||
expect(foo).toBeVisible();
|
expect(foo).toBeVisible();
|
||||||
expect(bar).toBeVisible();
|
expect(bar).toBeVisible();
|
||||||
|
expect(bar_server).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Renders correctly the status of a single-user server", async () => {
|
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));
|
render(serverDashboardJsx(callbackSpy));
|
||||||
});
|
});
|
||||||
|
|
||||||
let start = screen.getByText("Start Server");
|
let start_elems = screen.getAllByText("Start Server");
|
||||||
let stop = screen.getByText("Stop Server");
|
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
|
||||||
|
start_elems.forEach((start) => {
|
||||||
|
expect(start).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
expect(start).toBeVisible();
|
let stop = screen.getByText("Stop Server");
|
||||||
expect(stop).toBeVisible();
|
expect(stop).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -176,9 +194,12 @@ test("Renders spawn page link", async () => {
|
|||||||
render(serverDashboardJsx(callbackSpy));
|
render(serverDashboardJsx(callbackSpy));
|
||||||
});
|
});
|
||||||
|
|
||||||
let link = screen.getByText("Spawn Page").closest("a");
|
for (let server in bar_servers) {
|
||||||
let url = new URL(link.href);
|
let row = screen.getByTestId(`user-row-bar${server ? "-" + server : ""}`);
|
||||||
expect(url.pathname).toEqual("/spawn/bar");
|
let link = getByText(row, "Spawn Page").closest("a");
|
||||||
|
let url = new URL(link.href);
|
||||||
|
expect(url.pathname).toEqual("/spawn/bar" + (server ? "/" + server : ""));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Invokes the startServer event on button click", async () => {
|
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));
|
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 () => {
|
await act(async () => {
|
||||||
fireEvent.click(start);
|
fireEvent.click(start_elems[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(callbackSpy).toHaveBeenCalled();
|
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 () => {
|
await act(async () => {
|
||||||
fireEvent.click(start);
|
fireEvent.click(start_elems[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
let errorDialog = screen.getByText("Failed to start server.");
|
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 () => {
|
await act(async () => {
|
||||||
fireEvent.click(start);
|
fireEvent.click(start_elems[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
let errorDialog = screen.getByText("Failed to start server.");
|
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.mock.calls).toHaveLength(2);
|
||||||
// expect(callbackSpy).toHaveBeenCalledWith(2, 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@@ -30,3 +30,11 @@
|
|||||||
tr.noborder > td {
|
tr.noborder > td {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.server-dashboard-row-list-item {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 5px;
|
||||||
|
margin: 2px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
@@ -18,6 +18,12 @@ const withAPI = withProps(() => ({
|
|||||||
jhapiRequest("/users/" + name + "/servers/" + (serverName || ""), "POST"),
|
jhapiRequest("/users/" + name + "/servers/" + (serverName || ""), "POST"),
|
||||||
stopServer: (name, serverName = "") =>
|
stopServer: (name, serverName = "") =>
|
||||||
jhapiRequest("/users/" + name + "/servers/" + (serverName || ""), "DELETE"),
|
jhapiRequest("/users/" + name + "/servers/" + (serverName || ""), "DELETE"),
|
||||||
|
deleteServer: (name, serverName = "") =>
|
||||||
|
jhapiRequest(
|
||||||
|
"/users/" + name + "/servers/" + (serverName || ""),
|
||||||
|
"DELETE",
|
||||||
|
{ remove: true },
|
||||||
|
),
|
||||||
startAll: (names) =>
|
startAll: (names) =>
|
||||||
names.map((e) => jhapiRequest("/users/" + e + "/server", "POST")),
|
names.map((e) => jhapiRequest("/users/" + e + "/server", "POST")),
|
||||||
stopAll: (names) =>
|
stopAll: (names) =>
|
||||||
|
17029
jsx/yarn.lock
17029
jsx/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
|||||||
# 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.
|
||||||
# version_info updated by running `tbump`
|
# version_info updated by running `tbump`
|
||||||
version_info = (4, 0, 0, "b2", "")
|
version_info = (4, 0, 2, "", "")
|
||||||
|
|
||||||
# pep 440 version: no dot before beta/rc, but before .dev
|
# pep 440 version: no dot before beta/rc, but before .dev
|
||||||
# 0.1.0rc1
|
# 0.1.0rc1
|
||||||
|
@@ -89,6 +89,11 @@ class APIHandler(BaseHandler):
|
|||||||
if not hasattr(self, '_jupyterhub_user'):
|
if not hasattr(self, '_jupyterhub_user'):
|
||||||
# called too early to check if we're token-authenticated
|
# called too early to check if we're token-authenticated
|
||||||
return
|
return
|
||||||
|
if self._jupyterhub_user is None and 'Origin' not in self.request.headers:
|
||||||
|
# don't raise xsrf if auth failed
|
||||||
|
# don't apply this shortcut to actual cross-site requests, which have an 'Origin' header,
|
||||||
|
# which would reveal if there are credentials present
|
||||||
|
return
|
||||||
if getattr(self, '_token_authenticated', False):
|
if getattr(self, '_token_authenticated', False):
|
||||||
# if token-authenticated, ignore XSRF
|
# if token-authenticated, ignore XSRF
|
||||||
return
|
return
|
||||||
|
@@ -9,6 +9,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
|
import shlex
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
import ssl
|
import ssl
|
||||||
@@ -2840,6 +2841,10 @@ class JupyterHub(Application):
|
|||||||
super().initialize(*args, **kwargs)
|
super().initialize(*args, **kwargs)
|
||||||
if self.generate_config or self.generate_certs or self.subapp:
|
if self.generate_config or self.generate_certs or self.subapp:
|
||||||
return
|
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()
|
self._start_future = asyncio.Future()
|
||||||
|
|
||||||
def record_start(f):
|
def record_start(f):
|
||||||
|
@@ -236,11 +236,13 @@ class BaseHandler(RequestHandler):
|
|||||||
def check_xsrf_cookie(self):
|
def check_xsrf_cookie(self):
|
||||||
try:
|
try:
|
||||||
return super().check_xsrf_cookie()
|
return super().check_xsrf_cookie()
|
||||||
except Exception as e:
|
except web.HTTPError as e:
|
||||||
# ensure _juptyerhub_user is defined on rejected requests
|
# ensure _jupyterhub_user is defined on rejected requests
|
||||||
if not hasattr(self, "_jupyterhub_user"):
|
if not hasattr(self, "_jupyterhub_user"):
|
||||||
self._jupyterhub_user = None
|
self._jupyterhub_user = None
|
||||||
self._resolve_roles_and_scopes()
|
self._resolve_roles_and_scopes()
|
||||||
|
# rewrite message because we use this on methods other than POST
|
||||||
|
e.log_message = e.log_message.replace("POST", self.request.method)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1431,6 +1433,12 @@ class UserUrlHandler(BaseHandler):
|
|||||||
# accept token auth for API requests that are probably to non-running servers
|
# accept token auth for API requests that are probably to non-running servers
|
||||||
_accept_token_auth = True
|
_accept_token_auth = True
|
||||||
|
|
||||||
|
# don't consider these redirects 'activity'
|
||||||
|
# if the redirect is followed and the subsequent action taken,
|
||||||
|
# _that_ is activity
|
||||||
|
def _record_activity(self, obj, timestamp=None):
|
||||||
|
return False
|
||||||
|
|
||||||
def _fail_api_request(self, user_name='', server_name=''):
|
def _fail_api_request(self, user_name='', server_name=''):
|
||||||
"""Fail an API request to a not-running server"""
|
"""Fail an API request to a not-running server"""
|
||||||
self.log.warning(
|
self.log.warning(
|
||||||
|
@@ -105,6 +105,7 @@ class LoginHandler(BaseHandler):
|
|||||||
'next': self.get_argument('next', ''),
|
'next': self.get_argument('next', ''),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
"xsrf": self.xsrf_token.decode('ascii'),
|
||||||
}
|
}
|
||||||
custom_html = Template(
|
custom_html = Template(
|
||||||
self.authenticator.get_custom_html(self.hub.base_url)
|
self.authenticator.get_custom_html(self.hub.base_url)
|
||||||
|
@@ -52,7 +52,7 @@ def get_default_roles():
|
|||||||
'description': 'Post activity only',
|
'description': 'Post activity only',
|
||||||
'scopes': [
|
'scopes': [
|
||||||
'users:activity!user',
|
'users:activity!user',
|
||||||
'access:servers!user',
|
'access:servers!server',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -845,6 +845,15 @@ def needs_scope(*scopes):
|
|||||||
def scope_decorator(func):
|
def scope_decorator(func):
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def _auth_func(self, *args, **kwargs):
|
def _auth_func(self, *args, **kwargs):
|
||||||
|
if not self.current_user:
|
||||||
|
# not authenticated at all, fail with more generic message
|
||||||
|
# this is the most likely permission error - missing or mis-specified credentials,
|
||||||
|
# don't indicate that they have insufficient permissions.
|
||||||
|
raise web.HTTPError(
|
||||||
|
403,
|
||||||
|
"Missing or invalid credentials.",
|
||||||
|
)
|
||||||
|
|
||||||
sig = inspect.signature(func)
|
sig = inspect.signature(func)
|
||||||
bound_sig = sig.bind(self, *args, **kwargs)
|
bound_sig = sig.bind(self, *args, **kwargs)
|
||||||
bound_sig.apply_defaults()
|
bound_sig.apply_defaults()
|
||||||
@@ -853,6 +862,11 @@ def needs_scope(*scopes):
|
|||||||
self.expanded_scopes = {}
|
self.expanded_scopes = {}
|
||||||
self.parsed_scopes = {}
|
self.parsed_scopes = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
end_point = self.request.path
|
||||||
|
except AttributeError:
|
||||||
|
end_point = self.__name__
|
||||||
|
|
||||||
s_kwargs = {}
|
s_kwargs = {}
|
||||||
for resource in {'user', 'server', 'group', 'service'}:
|
for resource in {'user', 'server', 'group', 'service'}:
|
||||||
resource_name = resource + '_name'
|
resource_name = resource + '_name'
|
||||||
@@ -860,14 +874,10 @@ def needs_scope(*scopes):
|
|||||||
resource_value = bound_sig.arguments[resource_name]
|
resource_value = bound_sig.arguments[resource_name]
|
||||||
s_kwargs[resource] = resource_value
|
s_kwargs[resource] = resource_value
|
||||||
for scope in scopes:
|
for scope in scopes:
|
||||||
app_log.debug("Checking access via scope %s", scope)
|
app_log.debug("Checking access to %s via scope %s", end_point, scope)
|
||||||
has_access = _check_scope_access(self, scope, **s_kwargs)
|
has_access = _check_scope_access(self, scope, **s_kwargs)
|
||||||
if has_access:
|
if has_access:
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
try:
|
|
||||||
end_point = self.request.path
|
|
||||||
except AttributeError:
|
|
||||||
end_point = self.__name__
|
|
||||||
app_log.warning(
|
app_log.warning(
|
||||||
"Not authorizing access to {}. Requires any of [{}], not derived from scopes [{}]".format(
|
"Not authorizing access to {}. Requires any of [{}], not derived from scopes [{}]".format(
|
||||||
end_point, ", ".join(scopes), ", ".join(self.expanded_scopes)
|
end_point, ", ".join(scopes), ", ".join(self.expanded_scopes)
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
.. versionchanged:: 2.0
|
.. versionchanged:: 2.0
|
||||||
|
|
||||||
Default app changed to launch `jupyter labhub`.
|
Default app changed to launch `jupyter labhub`.
|
||||||
Use JUPYTERHUB_SINGLEUSER_APP=notebook.notebookapp.NotebookApp for the legacy 'classic' notebook server.
|
Use JUPYTERHUB_SINGLEUSER_APP='notebook' for the legacy 'classic' notebook server (requires notebook<7).
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -27,7 +27,25 @@ JUPYTERHUB_SINGLEUSER_APP = _app_shortcuts.get(
|
|||||||
JUPYTERHUB_SINGLEUSER_APP.replace("_", "-"), JUPYTERHUB_SINGLEUSER_APP
|
JUPYTERHUB_SINGLEUSER_APP.replace("_", "-"), JUPYTERHUB_SINGLEUSER_APP
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if JUPYTERHUB_SINGLEUSER_APP:
|
if JUPYTERHUB_SINGLEUSER_APP:
|
||||||
|
if JUPYTERHUB_SINGLEUSER_APP in {"notebook", _app_shortcuts["notebook"]}:
|
||||||
|
# better error for notebook v7, which uses jupyter-server
|
||||||
|
# when the legacy notebook server is requested
|
||||||
|
try:
|
||||||
|
from notebook import __version__
|
||||||
|
except ImportError:
|
||||||
|
# will raise later
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# check if this failed because of notebook v7
|
||||||
|
_notebook_major_version = int(__version__.split(".", 1)[0])
|
||||||
|
if _notebook_major_version >= 7:
|
||||||
|
raise ImportError(
|
||||||
|
f"JUPYTERHUB_SINGLEUSER_APP={JUPYTERHUB_SINGLEUSER_APP} is not valid with notebook>=7 (have notebook=={__version__}).\n"
|
||||||
|
f"Leave $JUPYTERHUB_SINGLEUSER_APP unspecified (or use the default JUPYTERHUB_SINGLEUSER_APP=jupyter-server), "
|
||||||
|
'and set `c.Spawner.default_url = "/tree"` to make notebook v7 the default UI.'
|
||||||
|
)
|
||||||
App = import_item(JUPYTERHUB_SINGLEUSER_APP)
|
App = import_item(JUPYTERHUB_SINGLEUSER_APP)
|
||||||
else:
|
else:
|
||||||
App = None
|
App = None
|
||||||
|
@@ -483,6 +483,11 @@ class JupyterHubSingleUser(ExtensionApp):
|
|||||||
cfg.answer_yes = True
|
cfg.answer_yes = True
|
||||||
self.config.FileContentsManager.delete_to_trash = False
|
self.config.FileContentsManager.delete_to_trash = False
|
||||||
|
|
||||||
|
# load Spawner.notebook_dir configuration, if given
|
||||||
|
root_dir = os.getenv("JUPYTERHUB_ROOT_DIR", None)
|
||||||
|
if root_dir:
|
||||||
|
cfg.root_dir = os.path.expanduser(root_dir)
|
||||||
|
|
||||||
# load http server config from environment
|
# load http server config from environment
|
||||||
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
|
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
|
||||||
if url.port:
|
if url.port:
|
||||||
@@ -511,16 +516,25 @@ class JupyterHubSingleUser(ExtensionApp):
|
|||||||
|
|
||||||
# Jupyter Server default: config files have higher priority than extensions,
|
# Jupyter Server default: config files have higher priority than extensions,
|
||||||
# by:
|
# by:
|
||||||
# 1. load config files
|
# 1. load config files and CLI
|
||||||
# 2. load extension config
|
# 2. load extension config
|
||||||
# 3. merge file config into extension config
|
# 3. merge file config into extension config
|
||||||
|
|
||||||
# we invert that by merging our extension config into server config before
|
# we invert that by merging our extension config into server config before
|
||||||
# they get merged the other way
|
# they get merged the other way
|
||||||
# this way config from this extension should always have highest priority
|
# 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)
|
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)
|
self.config.NotebookApp.extra_template_paths.append(SINGLEUSER_TEMPLATES_DIR)
|
||||||
|
|
||||||
@default("default_url")
|
@default("default_url")
|
||||||
@@ -608,7 +622,9 @@ class JupyterHubSingleUser(ExtensionApp):
|
|||||||
app.web_app.settings[
|
app.web_app.settings[
|
||||||
"page_config_hook"
|
"page_config_hook"
|
||||||
] = app.identity_provider.page_config_hook
|
] = app.identity_provider.page_config_hook
|
||||||
app.web_app.settings["log_function"] = log_request
|
# if the user has configured a log function in the tornado settings, do not override it
|
||||||
|
if not 'log_function' in app.config.ServerApp.get('tornado_settings', {}):
|
||||||
|
app.web_app.settings["log_function"] = log_request
|
||||||
# add jupyterhub version header
|
# add jupyterhub version header
|
||||||
headers = app.web_app.settings.setdefault("headers", {})
|
headers = app.web_app.settings.setdefault("headers", {})
|
||||||
headers["X-JupyterHub-Version"] = __version__
|
headers["X-JupyterHub-Version"] = __version__
|
||||||
|
@@ -669,7 +669,8 @@ class SingleUserNotebookAppMixin(Configurable):
|
|||||||
# load the hub-related settings into the tornado settings dict
|
# load the hub-related settings into the tornado settings dict
|
||||||
self.init_hub_auth()
|
self.init_hub_auth()
|
||||||
s = self.tornado_settings
|
s = self.tornado_settings
|
||||||
s['log_function'] = log_request
|
# if the user has configured a log function in the tornado settings, do not override it
|
||||||
|
s.setdefault('log_function', log_request)
|
||||||
s['user'] = self.user
|
s['user'] = self.user
|
||||||
s['group'] = self.group
|
s['group'] = self.group
|
||||||
s['hub_prefix'] = self.hub_prefix
|
s['hub_prefix'] = self.hub_prefix
|
||||||
|
@@ -382,6 +382,23 @@ class Spawner(LoggingConfigurable):
|
|||||||
scopes.append(f"access:servers!server={self.user.name}/{self.name}")
|
scopes.append(f"access:servers!server={self.user.name}/{self.name}")
|
||||||
return sorted(set(scopes))
|
return sorted(set(scopes))
|
||||||
|
|
||||||
|
server_token_scopes = Union(
|
||||||
|
[List(Unicode()), Callable()],
|
||||||
|
help="""The list of scopes to request for $JUPYTERHUB_API_TOKEN
|
||||||
|
|
||||||
|
If not specified, the scopes in the `server` role will be used
|
||||||
|
(unchanged from pre-4.0).
|
||||||
|
|
||||||
|
If callable, will be called with the Spawner instance as its sole argument
|
||||||
|
(JupyterHub user available as spawner.user).
|
||||||
|
|
||||||
|
JUPYTERHUB_API_TOKEN will be assigned the _subset_ of these scopes
|
||||||
|
that are held by the user (as in oauth_client_allowed_scopes).
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
""",
|
||||||
|
).tag(config=True)
|
||||||
|
|
||||||
will_resume = Bool(
|
will_resume = Bool(
|
||||||
False,
|
False,
|
||||||
help="""Whether the Spawner will resume on next start
|
help="""Whether the Spawner will resume on next start
|
||||||
|
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
@@ -30,6 +30,7 @@ class JupyterHubTestHandler(JupyterHandler):
|
|||||||
info = {
|
info = {
|
||||||
"current_user": self.current_user,
|
"current_user": self.current_user,
|
||||||
"config": self.app.config,
|
"config": self.app.config,
|
||||||
|
"root_dir": self.contents_manager.root_dir,
|
||||||
"disable_user_config": getattr(self.app, "disable_user_config", None),
|
"disable_user_config": getattr(self.app, "disable_user_config", None),
|
||||||
"settings": self.settings,
|
"settings": self.settings,
|
||||||
"config_file_paths": self.app.config_file_paths,
|
"config_file_paths": self.app.config_file_paths,
|
||||||
|
@@ -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
@@ -122,6 +122,41 @@ async def test_xsrf_check(app, username, method, path, xsrf_in_url):
|
|||||||
assert r.status_code == 403
|
assert r.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@mark.parametrize(
|
||||||
|
"auth, expected_message",
|
||||||
|
[
|
||||||
|
("", "Missing or invalid credentials"),
|
||||||
|
("cookie_no_xsrf", "'_xsrf' argument missing from GET"),
|
||||||
|
("cookie_xsrf_mismatch", "XSRF cookie does not match GET argument"),
|
||||||
|
("token_no_scope", "requires any of [list:users]"),
|
||||||
|
("cookie_no_scope", "requires any of [list:users]"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_permission_error_messages(app, user, auth, expected_message):
|
||||||
|
# 1. no credentials, should be 403 and not mention xsrf
|
||||||
|
|
||||||
|
url = public_url(app, path="hub/api/users")
|
||||||
|
|
||||||
|
kwargs = {}
|
||||||
|
kwargs["headers"] = headers = {}
|
||||||
|
kwargs["params"] = params = {}
|
||||||
|
if auth == "token_no_scope":
|
||||||
|
token = user.new_api_token()
|
||||||
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
|
elif "cookie" in auth:
|
||||||
|
cookies = kwargs["cookies"] = await app.login_user(user.name)
|
||||||
|
if auth == "cookie_no_scope":
|
||||||
|
params["_xsrf"] = cookies["_xsrf"]
|
||||||
|
if auth == "cookie_xsrf_mismatch":
|
||||||
|
params["_xsrf"] = "somethingelse"
|
||||||
|
|
||||||
|
r = await async_requests.get(url, **kwargs)
|
||||||
|
assert r.status_code == 403
|
||||||
|
response = r.json()
|
||||||
|
message = response["message"]
|
||||||
|
assert expected_message in message
|
||||||
|
|
||||||
|
|
||||||
# --------------
|
# --------------
|
||||||
# User API tests
|
# User API tests
|
||||||
# --------------
|
# --------------
|
||||||
|
@@ -35,7 +35,7 @@ def generate_old_db(env_dir, hub_version, db_url):
|
|||||||
pkgs.append('sqlalchemy<2')
|
pkgs.append('sqlalchemy<2')
|
||||||
|
|
||||||
if 'mysql' in db_url:
|
if 'mysql' in db_url:
|
||||||
pkgs.append('mysql-connector-python')
|
pkgs.append('mysqlclient')
|
||||||
elif 'postgres' in db_url:
|
elif 'postgres' in db_url:
|
||||||
pkgs.append('psycopg2-binary')
|
pkgs.append('psycopg2-binary')
|
||||||
check_call([env_pip, 'install'] + pkgs)
|
check_call([env_pip, 'install'] + pkgs)
|
||||||
|
@@ -848,8 +848,12 @@ async def test_server_token_role(app):
|
|||||||
orm_server_token = orm.APIToken.find(app.db, server_token)
|
orm_server_token = orm.APIToken.find(app.db, server_token)
|
||||||
assert orm_server_token
|
assert orm_server_token
|
||||||
|
|
||||||
server_role = orm.Role.find(app.db, 'server')
|
# resolve `!server` filter in server role
|
||||||
assert set(server_role.scopes) == set(orm_server_token.scopes)
|
server_role_scopes = {
|
||||||
|
s.replace("!server", f"!server={user.name}/")
|
||||||
|
for s in orm.Role.find(app.db, "server").scopes
|
||||||
|
}
|
||||||
|
assert set(orm_server_token.scopes) == server_role_scopes
|
||||||
|
|
||||||
assert orm_server_token.user.name == user.name
|
assert orm_server_token.user.name == user.name
|
||||||
assert user.api_tokens == [orm_server_token]
|
assert user.api_tokens == [orm_server_token]
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
|
from pprint import pprint
|
||||||
from subprocess import CalledProcessError, check_output
|
from subprocess import CalledProcessError, check_output
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from urllib.parse import urlencode, urlparse
|
from urllib.parse import urlencode, urlparse
|
||||||
@@ -171,9 +172,7 @@ async def test_disable_user_config(request, app, tmpdir, full_spawn):
|
|||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
info = r.json()
|
info = r.json()
|
||||||
import pprint
|
pprint(info)
|
||||||
|
|
||||||
pprint.pprint(info)
|
|
||||||
assert info['disable_user_config']
|
assert info['disable_user_config']
|
||||||
server_config = info['config']
|
server_config = info['config']
|
||||||
settings = info['settings']
|
settings = info['settings']
|
||||||
@@ -198,6 +197,79 @@ async def test_disable_user_config(request, app, tmpdir, full_spawn):
|
|||||||
assert_not_in_home(path, key)
|
assert_not_in_home(path, key)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("extension", [True, False])
|
||||||
|
@pytest.mark.parametrize("notebook_dir", ["", "~", "~/sub", "ABS"])
|
||||||
|
async def test_notebook_dir(
|
||||||
|
request, app, tmpdir, user, full_spawn, extension, notebook_dir
|
||||||
|
):
|
||||||
|
if extension:
|
||||||
|
try:
|
||||||
|
import jupyter_server # noqa
|
||||||
|
except ImportError:
|
||||||
|
pytest.skip("needs jupyter-server 2")
|
||||||
|
else:
|
||||||
|
if jupyter_server.version_info < (2,):
|
||||||
|
pytest.skip("needs jupyter-server 2")
|
||||||
|
|
||||||
|
token = user.new_api_token(scopes=["access:servers!user"])
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
spawner = user.spawner
|
||||||
|
if extension:
|
||||||
|
user.spawner.environment["JUPYTERHUB_SINGLEUSER_EXTENSION"] = "1"
|
||||||
|
else:
|
||||||
|
user.spawner.environment["JUPYTERHUB_SINGLEUSER_EXTENSION"] = "0"
|
||||||
|
|
||||||
|
home_dir = tmpdir.join("home").mkdir()
|
||||||
|
sub_dir = home_dir.join("sub").mkdir()
|
||||||
|
with sub_dir.join("subfile.txt").open("w") as f:
|
||||||
|
f.write("txt\n")
|
||||||
|
abs_dir = tmpdir.join("abs").mkdir()
|
||||||
|
with abs_dir.join("absfile.txt").open("w") as f:
|
||||||
|
f.write("absfile\n")
|
||||||
|
|
||||||
|
if notebook_dir:
|
||||||
|
expected_root_dir = notebook_dir.replace("ABS", str(abs_dir)).replace(
|
||||||
|
"~", str(home_dir)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
expected_root_dir = str(home_dir)
|
||||||
|
|
||||||
|
spawner.notebook_dir = notebook_dir.replace("ABS", str(abs_dir))
|
||||||
|
|
||||||
|
# home_dir is defined on SimpleSpawner
|
||||||
|
user.spawner.home_dir = home = str(home_dir)
|
||||||
|
spawner.environment["HOME"] = home
|
||||||
|
await user.spawn()
|
||||||
|
await app.proxy.add_user(user)
|
||||||
|
url = public_url(app, user)
|
||||||
|
r = await async_requests.get(
|
||||||
|
url_path_join(public_url(app, user), 'jupyterhub-test-info'), headers=headers
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
info = r.json()
|
||||||
|
pprint(info)
|
||||||
|
|
||||||
|
assert info["root_dir"] == expected_root_dir
|
||||||
|
# secondary check: make sure it has the intended effect on root_dir
|
||||||
|
r = await async_requests.get(
|
||||||
|
url_path_join(public_url(app, user), 'api/contents/'), headers=headers
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
root_contents = sorted(item['name'] for item in r.json()['content'])
|
||||||
|
|
||||||
|
# check contents
|
||||||
|
if not notebook_dir or notebook_dir == "~":
|
||||||
|
# use any to avoid counting possible automatically created files in $HOME
|
||||||
|
assert 'sub' in root_contents
|
||||||
|
elif notebook_dir == "ABS":
|
||||||
|
assert 'absfile.txt' in root_contents
|
||||||
|
elif notebook_dir == "~/sub":
|
||||||
|
assert 'subfile.txt' in root_contents
|
||||||
|
else:
|
||||||
|
raise ValueError(f"No contents check for {notebook_dir}")
|
||||||
|
|
||||||
|
|
||||||
def test_help_output():
|
def test_help_output():
|
||||||
out = check_output(
|
out = check_output(
|
||||||
[sys.executable, '-m', 'jupyterhub.singleuser', '--help-all']
|
[sys.executable, '-m', 'jupyterhub.singleuser', '--help-all']
|
||||||
|
@@ -20,7 +20,7 @@ from ..objects import Hub, Server
|
|||||||
from ..scopes import access_scopes
|
from ..scopes import access_scopes
|
||||||
from ..spawner import LocalProcessSpawner, Spawner
|
from ..spawner import LocalProcessSpawner, Spawner
|
||||||
from ..user import User
|
from ..user import User
|
||||||
from ..utils import AnyTimeoutError, new_token, url_path_join
|
from ..utils import AnyTimeoutError, maybe_future, new_token, url_path_join
|
||||||
from .mocking import public_url
|
from .mocking import public_url
|
||||||
from .test_api import add_user
|
from .test_api import add_user
|
||||||
from .utils import async_requests
|
from .utils import async_requests
|
||||||
@@ -336,6 +336,12 @@ async def test_spawner_insert_api_token(app):
|
|||||||
assert found
|
assert found
|
||||||
assert found.user.name == user.name
|
assert found.user.name == user.name
|
||||||
assert user.api_tokens == [found]
|
assert user.api_tokens == [found]
|
||||||
|
# resolve `!server` filter in server role
|
||||||
|
server_role_scopes = {
|
||||||
|
s.replace("!server", f"!server={user.name}/")
|
||||||
|
for s in orm.Role.find(app.db, "server").scopes
|
||||||
|
}
|
||||||
|
assert set(found.scopes) == server_role_scopes
|
||||||
await user.stop()
|
await user.stop()
|
||||||
|
|
||||||
|
|
||||||
@@ -361,6 +367,58 @@ async def test_spawner_bad_api_token(app):
|
|||||||
assert other_user.api_tokens == []
|
assert other_user.api_tokens == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"have_scopes, request_scopes, expected_scopes",
|
||||||
|
[
|
||||||
|
(["self"], ["inherit"], ["inherit"]),
|
||||||
|
(["self"], [], ["access:servers!server=USER/", "users:activity!user"]),
|
||||||
|
(
|
||||||
|
["self"],
|
||||||
|
["admin:groups", "read:servers!server"],
|
||||||
|
["users:activity!user", "read:servers!server=USER/"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
["self", "read:groups!group=x", "users:activity"],
|
||||||
|
["admin:groups", "users:activity"],
|
||||||
|
["read:groups!group=x", "read:groups:name!group=x", "users:activity"],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_server_token_scopes(
|
||||||
|
app, username, create_user_with_scopes, have_scopes, request_scopes, expected_scopes
|
||||||
|
):
|
||||||
|
"""Token provided by spawner is not in the db
|
||||||
|
|
||||||
|
Insert token into db as a user-provided token.
|
||||||
|
"""
|
||||||
|
db = app.db
|
||||||
|
|
||||||
|
# apply templating
|
||||||
|
def _format_scopes(scopes):
|
||||||
|
if callable(scopes):
|
||||||
|
|
||||||
|
async def get_scopes(*args):
|
||||||
|
return _format_scopes(await maybe_future(scopes(*args)))
|
||||||
|
|
||||||
|
return get_scopes
|
||||||
|
|
||||||
|
return [s.replace("USER", username) for s in scopes]
|
||||||
|
|
||||||
|
have_scopes = _format_scopes(have_scopes)
|
||||||
|
request_scopes = _format_scopes(request_scopes)
|
||||||
|
expected_scopes = _format_scopes(expected_scopes)
|
||||||
|
|
||||||
|
user = create_user_with_scopes(*have_scopes, name=username)
|
||||||
|
spawner = user.spawner
|
||||||
|
spawner.server_token_scopes = request_scopes
|
||||||
|
|
||||||
|
await user.spawn()
|
||||||
|
orm_token = orm.APIToken.find(db, spawner.api_token)
|
||||||
|
assert orm_token
|
||||||
|
assert set(orm_token.scopes) == set(expected_scopes)
|
||||||
|
await user.stop()
|
||||||
|
|
||||||
|
|
||||||
async def test_spawner_delete_server(app):
|
async def test_spawner_delete_server(app):
|
||||||
"""Test deleting spawner.server
|
"""Test deleting spawner.server
|
||||||
|
|
||||||
|
@@ -53,3 +53,16 @@ def test_sync_groups(app, user, group_names):
|
|||||||
assert user.orm_user in group.users
|
assert user.orm_user in group.users
|
||||||
else:
|
else:
|
||||||
assert user.orm_user not in group.users
|
assert user.orm_user not in group.users
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"server_name, path",
|
||||||
|
[
|
||||||
|
("", ""),
|
||||||
|
("name", "name/"),
|
||||||
|
("næme", "n%C3%A6me/"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_server_url(app, user, server_name, path):
|
||||||
|
user_url = user.url
|
||||||
|
assert user.server_url(server_name) == user_url + path
|
||||||
|
@@ -13,7 +13,7 @@ from tornado import gen, web
|
|||||||
from tornado.httputil import urlencode
|
from tornado.httputil import urlencode
|
||||||
from tornado.log import app_log
|
from tornado.log import app_log
|
||||||
|
|
||||||
from . import orm
|
from . import orm, roles, scopes
|
||||||
from ._version import __version__, _check_version
|
from ._version import __version__, _check_version
|
||||||
from .crypto import CryptKeeper, EncryptionUnavailable, InvalidToken, decrypt, encrypt
|
from .crypto import CryptKeeper, EncryptionUnavailable, InvalidToken, decrypt, encrypt
|
||||||
from .metrics import RUNNING_SERVERS, TOTAL_USERS
|
from .metrics import RUNNING_SERVERS, TOTAL_USERS
|
||||||
@@ -588,7 +588,7 @@ class User:
|
|||||||
if not server_name:
|
if not server_name:
|
||||||
return self.url
|
return self.url
|
||||||
else:
|
else:
|
||||||
return url_path_join(self.url, url_escape_path(server_name))
|
return url_path_join(self.url, url_escape_path(server_name), "/")
|
||||||
|
|
||||||
def progress_url(self, server_name=''):
|
def progress_url(self, server_name=''):
|
||||||
"""API URL for progress endpoint for a server with a given name"""
|
"""API URL for progress endpoint for a server with a given name"""
|
||||||
@@ -673,13 +673,63 @@ class User:
|
|||||||
orm_server = orm.Server(base_url=base_url)
|
orm_server = orm.Server(base_url=base_url)
|
||||||
db.add(orm_server)
|
db.add(orm_server)
|
||||||
note = "Server at %s" % base_url
|
note = "Server at %s" % base_url
|
||||||
api_token = self.new_api_token(note=note, roles=['server'])
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
spawner = self.get_spawner(server_name, replace_failed=True)
|
spawner = self.get_spawner(server_name, replace_failed=True)
|
||||||
spawner.server = server = Server(orm_server=orm_server)
|
spawner.server = server = Server(orm_server=orm_server)
|
||||||
assert spawner.orm_spawner.server is orm_server
|
assert spawner.orm_spawner.server is orm_server
|
||||||
|
|
||||||
|
requested_scopes = spawner.server_token_scopes
|
||||||
|
if callable(requested_scopes):
|
||||||
|
requested_scopes = await maybe_future(requested_scopes(spawner))
|
||||||
|
if not requested_scopes:
|
||||||
|
# nothing requested, default to 'server' role
|
||||||
|
requested_scopes = orm.Role.find(db, "server").scopes
|
||||||
|
requested_scopes = set(requested_scopes)
|
||||||
|
# resolve !server filter, which won't resolve elsewhere,
|
||||||
|
# because this token is not owned by the server's own oauth client
|
||||||
|
server_filter = f"={self.name}/{server_name}"
|
||||||
|
requested_scopes = {
|
||||||
|
scope + server_filter if scope.endswith("!server") else scope
|
||||||
|
for scope in requested_scopes
|
||||||
|
}
|
||||||
|
# ensure activity scope is requested, since activity doesn't work without
|
||||||
|
activity_scope = "users:activity!user"
|
||||||
|
if not {activity_scope, "users:activity", "inherit"}.intersection(
|
||||||
|
requested_scopes
|
||||||
|
):
|
||||||
|
self.log.warning(
|
||||||
|
f"Adding required scope {activity_scope} to server token, missing from Spawner.server_token_scopes. Please make sure to add it!"
|
||||||
|
)
|
||||||
|
requested_scopes |= {activity_scope}
|
||||||
|
|
||||||
|
have_scopes = roles.roles_to_scopes(roles.get_roles_for(self.orm_user))
|
||||||
|
have_scopes |= {"inherit"}
|
||||||
|
jupyterhub_client = (
|
||||||
|
db.query(orm.OAuthClient)
|
||||||
|
.filter_by(
|
||||||
|
identifier="jupyterhub",
|
||||||
|
)
|
||||||
|
.one()
|
||||||
|
)
|
||||||
|
|
||||||
|
resolved_scopes, excluded_scopes = scopes._resolve_requested_scopes(
|
||||||
|
requested_scopes, have_scopes, self.orm_user, jupyterhub_client, db
|
||||||
|
)
|
||||||
|
if excluded_scopes:
|
||||||
|
# what level should this be?
|
||||||
|
# for admins-get-more use case, this is going to happen for most users
|
||||||
|
# but for misconfiguration, folks will want to know!
|
||||||
|
self.log.debug(
|
||||||
|
"Not assigning requested scopes for %s: requested=%s, assigned=%s, excluded=%s",
|
||||||
|
spawner._log_name,
|
||||||
|
requested_scopes,
|
||||||
|
resolved_scopes,
|
||||||
|
excluded_scopes,
|
||||||
|
)
|
||||||
|
|
||||||
|
api_token = self.new_api_token(note=note, scopes=resolved_scopes)
|
||||||
|
|
||||||
# pass requesting handler to the spawner
|
# pass requesting handler to the spawner
|
||||||
# e.g. for processing GET params
|
# e.g. for processing GET params
|
||||||
spawner.handler = handler
|
spawner.handler = handler
|
||||||
@@ -808,6 +858,7 @@ class User:
|
|||||||
spawner.api_token,
|
spawner.api_token,
|
||||||
generated=False,
|
generated=False,
|
||||||
note="retrieved from spawner %s" % server_name,
|
note="retrieved from spawner %s" % server_name,
|
||||||
|
scopes=resolved_scopes,
|
||||||
)
|
)
|
||||||
# update OAuth client secret with updated API token
|
# update OAuth client secret with updated API token
|
||||||
if oauth_provider:
|
if oauth_provider:
|
||||||
|
@@ -43,7 +43,7 @@ target_version = [
|
|||||||
github_url = "https://github.com/jupyterhub/jupyterhub"
|
github_url = "https://github.com/jupyterhub/jupyterhub"
|
||||||
|
|
||||||
[tool.tbump.version]
|
[tool.tbump.version]
|
||||||
current = "4.0.0b2"
|
current = "4.0.2"
|
||||||
|
|
||||||
# Example of a semver regexp.
|
# Example of a semver regexp.
|
||||||
# Make sure this matches current_version before
|
# Make sure this matches current_version before
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
asyncio_mode = auto
|
asyncio_mode = auto
|
||||||
|
|
||||||
# jupyter_server plugin is incompatible with notebook imports
|
# 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
|
python_files = test_*.py
|
||||||
markers =
|
markers =
|
||||||
@@ -17,7 +17,7 @@ markers =
|
|||||||
user: mark as a test for a user
|
user: mark as a test for a user
|
||||||
slow: mark a test as slow
|
slow: mark a test as slow
|
||||||
role: mark as a test for roles
|
role: mark as a test for roles
|
||||||
selenium: web tests that run with selenium
|
browser: web tests that run with playwright
|
||||||
|
|
||||||
filterwarnings =
|
filterwarnings =
|
||||||
ignore:.*The new signature is "def engine_connect\(conn\)"*:sqlalchemy.exc.SADeprecationWarning
|
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-asyncio>=0.17",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"requests-mock",
|
"requests-mock",
|
||||||
"selenium",
|
"playwright",
|
||||||
"virtualenv",
|
"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
|
// common form display
|
||||||
|
|
||||||
.form-control:focus {
|
.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;
|
border-color: @jupyter-orange;
|
||||||
outline-color: @jupyter-orange;
|
outline-color: @jupyter-orange;
|
||||||
}
|
}
|
||||||
|
@@ -17,10 +17,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% 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.
|
This note will help you keep track of what your tokens are for.
|
||||||
</small>
|
</small>
|
||||||
<br><br>
|
<br><br>
|
||||||
<label for="token-expiration-seconds">Token expires</label>
|
<label for="token-expiration-seconds">Token expires in</label>
|
||||||
{% block expiration_options %}
|
{% block expiration_options %}
|
||||||
<select id="token-expiration-seconds"
|
<select id="token-expiration-seconds"
|
||||||
class="form-control">
|
class="form-control">
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
</select>
|
</select>
|
||||||
{% endblock expiration_options %}
|
{% endblock expiration_options %}
|
||||||
<small id="note-expires-at" class="form-text text-muted">
|
<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>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -62,8 +62,8 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<h2>API Tokens</h2>
|
<h2>API Tokens</h2>
|
||||||
<p>
|
<p>
|
||||||
These are tokens with full access to the JupyterHub API.
|
These are tokens with access to the JupyterHub API.
|
||||||
Anything you can do with JupyterHub can be done with these tokens.
|
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.
|
Revoking the API token for a running server will require restarting that server.
|
||||||
</p>
|
</p>
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
<th>Note</th>
|
<th>Note</th>
|
||||||
<th>Last used</th>
|
<th>Last used</th>
|
||||||
<th>Created</th>
|
<th>Created</th>
|
||||||
<th>Expires at</th>
|
<th>Expires</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
Reference in New Issue
Block a user