Compare commits

...

159 Commits

Author SHA1 Message Date
Min RK
0e7689f277 Bump to 4.0.2 2023-08-10 11:27:56 +02:00
Min RK
b677655572 Merge pull request #4535 from meeseeksmachine/auto-backport-of-pr-4534-on-4.x
Backport PR #4534 on branch 4.x (Changelog for 4.0.2)
2023-08-10 11:26:50 +02:00
Min RK
9adc871448 Backport PR #4534: Changelog for 4.0.2 2023-08-10 09:19:16 +00:00
Min RK
29d6540333 Merge pull request #4533 from meeseeksmachine/auto-backport-of-pr-4489-on-4.x
Backport PR #4489 on branch 4.x (improve permission-denied errors for various cases)
2023-08-10 11:01:08 +02:00
Erik Sundell
5a4949faa5 Backport PR #4489: improve permission-denied errors for various cases 2023-08-10 08:13:44 +00:00
Min RK
f2ab23b376 Merge pull request #4531 from meeseeksmachine/auto-backport-of-pr-4475-on-4.x
Backport PR #4475 on branch 4.x (Allow setting custom log_function in tornado_settings in SingleUserServer)
2023-08-09 15:24:45 +02:00
Min RK
b61582420a Merge pull request #4532 from meeseeksmachine/auto-backport-of-pr-4522-on-4.x
Backport PR #4522 on branch 4.x (document how to use notebook v7 with jupyterhub)
2023-08-09 15:24:34 +02:00
Simon Li
f11ae34b73 Backport PR #4522: document how to use notebook v7 with jupyterhub 2023-08-09 11:12:50 +00:00
Min RK
e91ab50d1b Backport PR #4475: Allow setting custom log_function in tornado_settings in SingleUserServer 2023-08-09 11:03:55 +00:00
Min RK
4cb3a45ce4 Merge pull request #4529 from meeseeksmachine/auto-backport-of-pr-4523-on-4.x
Backport PR #4523 on branch 4.x (doc: update notebook config URL)
2023-08-09 12:40:11 +02:00
Min RK
4e8f9b4334 Merge pull request #4528 from meeseeksmachine/auto-backport-of-pr-4503-on-4.x
Backport PR #4503 on branch 4.x (set root_dir when using singleuser extension)
2023-08-09 12:31:33 +02:00
Min RK
6131f2dbaa Merge pull request #4530 from meeseeksmachine/auto-backport-of-pr-4491-on-4.x
Backport PR #4491 on branch 4.x (avoid counting failed requests to not-running servers as 'activity')
2023-08-09 12:31:07 +02:00
Min RK
a9dc588454 can't use f"{name=}" in Python 3.7 2023-08-09 11:56:03 +02:00
Erik Sundell
537b2eaff6 Backport PR #4491: avoid counting failed requests to not-running servers as 'activity' 2023-08-09 09:53:20 +00:00
Simon Li
7f8a981aed Backport PR #4523: doc: update notebook config URL 2023-08-09 09:52:41 +00:00
Erik Sundell
bc86e4c8f5 Backport PR #4503: set root_dir when using singleuser extension 2023-08-09 09:50:48 +00:00
Min RK
20f75c0018 Bump to 4.1.0.dev 2023-06-12 15:29:13 +02:00
Min RK
689dc5ba24 Bump to 4.0.1 2023-06-08 10:38:00 +02:00
Min RK
d42a7261a4 Merge pull request #4472 from minrk/401-cl
changelog for 4.0.1
2023-06-08 10:37:12 +02:00
Min RK
bcbf136de2 set date for 4.0.1
Co-authored-by: Erik Sundell <erik.i.sundell@gmail.com>
2023-06-08 09:58:21 +02:00
Min RK
55e9a0f5b5 changelog for 4.0.1 2023-06-07 15:41:22 +02:00
Min RK
d64d916abc Merge pull request #4470 from jupyterhub/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-06-06 09:05:29 +02:00
pre-commit-ci[bot]
da668b5e9a [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.3.2 → v3.4.0](https://github.com/asottile/pyupgrade/compare/v3.3.2...v3.4.0)
2023-06-06 04:12:25 +00:00
Erik Sundell
d54442ecbf Merge pull request #4467 from minrk/main
Abort informatively on unrecognized CLI options
2023-06-05 10:30:31 +02:00
Min RK
c930d6bf6a Abort informatively on unrecognized CLI options
rather than ignoring them, leading to unexpected behavior
2023-06-02 13:26:31 +02:00
Min RK
2ce263d45f Merge pull request #4463 from minrk/prefer-runtime-token
Reorder token request docs
2023-06-02 11:48:23 +02:00
Min RK
68f81fdc30 Merge pull request #4457 from diocas/fix_4174
Delete server button on admin page
2023-06-02 11:46:24 +02:00
Min RK
e7ab18a720 Merge pull request #4464 from opoplawski/xsrf
Add xsrf to custom_html template context
2023-06-02 11:30:53 +02:00
Orion Poplawski
582467642c Add xsrf to custom_html template context 2023-06-01 10:00:57 -06:00
Min RK
d65e2daa15 Apply suggestions from code review
Co-authored-by: Simon Li <orpheus+devel@gmail.com>
2023-06-01 12:55:07 +02:00
Min RK
4eaa7c5eb3 Reorder token request docs
- suggest token page first
- remove caveat about JupyterHub 0.8, which can be assumed now
- undocument `jupyterhub token`
- refresh token page screenshots, and remove duplicate screenshot of the token page
- minor improvements to language in token page
2023-05-31 14:25:03 +02:00
Min RK
02de44e551 Merge pull request #4458 from tfmark/rest-api-docs-servers-as-dict
'servers' should be a dict of dicts, not a list of dicts in rest-api.yml
2023-05-25 13:37:01 +02:00
tfmark
4cdf0a65cd 'servers' should be a dict of dicts, not a list of dicts in rest-api.yml 2023-05-24 16:09:26 +01:00
pre-commit-ci[bot]
b0367c21f3 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2023-05-23 15:33:08 +00:00
Diogo Castro
9d68107722 Add test case for named servers
Adapt all tests
2023-05-23 17:30:23 +02:00
Diogo Castro
ad61c23873 Allow deletion of named servers 2023-05-23 17:30:23 +02:00
Min RK
c359221ef3 Merge pull request #4454 from goseind/gallery_cern
Add CERN to Gallery of JupyterHub Deployments
2023-05-22 13:45:48 +02:00
Min RK
cc94d290ab Merge pull request #4456 from manics/doc-config-ref
Config reference: link to nicer(?) API docs first
2023-05-22 13:45:33 +02:00
Min RK
da0a58cb9c Merge pull request #4451 from minrk/preserve-cli-port
preserve CLI > env priority config in jupyterhub-singleuser extension
2023-05-22 13:16:08 +02:00
Simon Li
7ddd3b0589 Config reference: link to nicer(?) API docs first
`Configuration Reference` sounds like it's the place to go to see the full list of JupyterHub config options.
However it's not very readable as it's a plain-text dump of the output of `jupyterhub --generate-config`.

This links to some of the API doc pages instead, which present most of the information in an easier to read format. Unfortunately it also includes a lot of non-traitlets documentation.
2023-05-18 16:23:21 +01:00
Domenic Gosein
ff71d09fd1 Add CERN to Gallery of JupyterHub Deployments 2023-05-16 16:57:40 +00:00
Min RK
1eb0b1b073 preserve CLI > env priority in jupyterhub-singleuser extension 2023-05-12 17:21:12 +02:00
Min RK
9ea9902c76 Merge pull request #4448 from minrk/collab-link
Fix link to collaboration accounts doc in example
2023-05-11 21:35:35 +02:00
Min RK
6494017ce2 Fix link to collaboration accounts doc in example 2023-05-11 15:08:14 +02:00
Simon Li
b0cd9eebe9 Merge pull request #4443 from manics/node18
Update jsx dependencies as much as possible
2023-05-11 00:35:43 +01:00
Min RK
c3d4885521 Merge pull request #4428 from minrk/faq-share
update sharing faq for 2023
2023-05-10 17:08:32 +02:00
Min RK
2919aaae79 Merge pull request #4444 from manics/remove-alpine
Remove Dockerfile.alpine
2023-05-08 14:24:20 +02:00
Simon Li
1986ba71c1 Remove Dockerfile.alpine 2023-05-06 12:49:02 +01:00
Simon Li
a2c39a4dbc Remove multi-arch cross-compilation debugging 2023-05-06 12:33:32 +01:00
Simon Li
1e847c8710 Reduce container build time to 20 2023-05-06 12:22:25 +01:00
Simon Li
83a8552a63 Clean-up FROM --platform leftover from debugging 2023-05-06 12:09:43 +01:00
Simon Li
f60c633320 Replace apt -q with apt-get -qq 2023-05-06 11:58:05 +01:00
Simon Li
a5c7384228 Completely seperate jupyterhub and other wheel stages 2023-05-06 11:44:42 +01:00
Simon Li
27de930978 More debugging 2023-05-06 10:40:07 +01:00
Simon Li
98e76d52bc Debugging BUILDPLATFORM TARGETPLATFORM 2023-05-06 00:51:53 +01:00
Simon Li
729aac9bd1 Why is BUILDPLATFORM linux/arm64 when buliding arm64 on a gh amd64 runner? 2023-05-06 00:38:05 +01:00
Simon Li
bc85c445ab Attempt to reduce container build time
JupyterHub is pure Python, so can be built in a native platform image and copied into the target platform image
2023-05-06 00:03:32 +01:00
Simon Li
9f708fa10c lodash per method packages are deprecated
https://lodash.com/per-method-packages
2023-05-05 23:36:53 +01:00
Simon Li
d26c7cd6fc Try increasing release container build time to 45 2023-05-05 22:54:20 +01:00
Simon Li
0174083439 regenerate yarn.lock 2023-05-05 21:04:03 +01:00
Simon Li
e6fc2aee4a Update package.json as much as possible without tests failing 2023-05-05 21:04:03 +01:00
Simon Li
47513cfbd0 npx npm-check-updates -u 2023-05-05 21:04:03 +01:00
Simon Li
4e7147a495 Update nodejs from 12 to 18 2023-05-05 21:04:00 +01:00
Min RK
5cfc0db0d5 Merge pull request #4441 from ryanlovett/support-bot-typo 2023-05-04 08:37:24 +02:00
ryanlovett
eb862e2cbb Fix "Thanks" typo. 2023-05-03 17:35:10 -07:00
Min RK
98799e4227 Merge pull request #4432 from huntdatacenter/add-research-institution
add HUNT into research institutions
2023-05-03 14:07:04 +02:00
Min RK
ea6a0e53cc Merge pull request #4440 from jupyterhub/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-05-02 09:14:20 +02:00
Min RK
f2b42a50c8 Merge pull request #4438 from yuvipanda/no-mo-admin
Remove old admin JS code
2023-05-02 09:13:33 +02:00
pre-commit-ci[bot]
43336f5b07 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2023-05-02 04:27:03 +00:00
pre-commit-ci[bot]
bf2d948366 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.3.1 → v3.3.2](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.3.2)
- [github.com/PyCQA/autoflake: v2.0.2 → v2.1.1](https://github.com/PyCQA/autoflake/compare/v2.0.2...v2.1.1)
- [github.com/pre-commit/mirrors-prettier: v3.0.0-alpha.6 → v3.0.0-alpha.9-for-vscode](https://github.com/pre-commit/mirrors-prettier/compare/v3.0.0-alpha.6...v3.0.0-alpha.9-for-vscode)
2023-05-02 04:26:36 +00:00
YuviPanda
271fd35bce Remove old admin JS code
We have a new react based admin, and this JS was just loading
and doing nothing.
2023-05-01 11:35:22 -07:00
Min RK
1d70986c25 Merge pull request #4435 from mouse1203/playwright_more
Finish migrating browser tests from selenium to playwright
2023-04-28 12:50:52 +02:00
mouse1203
ec017d1f1d Update test_browser.py
added to test_start_stop_server_on_admin_page waiting to load page
2023-04-27 15:16:29 +02:00
mouse1203
a8c804de5b Finish to migrate tests from selenium to playwright
Removed selenium tests and configuration
Added the rest of playwright tests
2023-04-27 14:43:59 +02:00
Min RK
3578001fab Merge pull request #4431 from mouse1203/playwright_more
Migrate some tests from selenium to playwright
2023-04-27 12:59:53 +02:00
Matúš Košút
b199110276 add HUNT into research institutions 2023-04-26 16:13:13 +02:00
mouse1203
b69bba5a7d Adding new playwright tests and removing a part of Selenium tests
Added Playwright tests which are covered Login, Spawning, Home and Token pages
Removed Selenium cases which are covered Login, Spawning, Home and Token pages
2023-04-25 10:42:05 +02:00
Min RK
efdad701df Merge pull request #4420 from mouse1203/playwright_more 2023-04-24 08:49:13 +02:00
Min RK
8a074b12b5 Merge pull request #4429 from consideRatio/pr/fix-missing-redirects 2023-04-24 08:46:06 +02:00
Erik Sundell
b5e5fe630d docs: fix missing redirects for api to reference/api 2023-04-23 08:02:52 +02:00
mouse1203
5d23bf6da3 Update test.yml
remove stage:
- name: Install playwright module
2023-04-21 15:35:12 +02:00
mouse1203
e5a8939481 Update setup.py
Update setup.py
2023-04-20 14:42:37 +02:00
mouse1203
0eca901c65 Added playwright in setup.py
Added "playwright" in setup.py under test section
2023-04-20 14:37:40 +02:00
mouse1203
4a1964f881 Updated configuration for selenium/playwright
Renamed selenium/playwright to browser in markers and configuration
2023-04-20 14:19:46 +02:00
Min RK
131094b5ff Merge pull request #4426 from minrk/upgrade-note
add upgrade note for 4.0 to changelog
2023-04-20 14:16:04 +02:00
Min RK
4544a98fb9 put upgrade note to note heading
Co-authored-by: Erik Sundell <erik.i.sundell@gmail.com>
2023-04-20 14:11:38 +02:00
Min RK
cbacdecb1e update sharing faq for 2023 2023-04-20 13:52:01 +02:00
Erik Sundell
64d8b2adc9 Merge pull request #4427 from minrk/rtd-internal-links
Fix some public URL links within the docs
2023-04-20 13:48:55 +02:00
Min RK
9c83c15f67 Fix some public URL links within the docs
there shouldn't be any links to jupyterhub.readthedocs.io
2023-04-20 13:36:16 +02:00
Min RK
d2a545a01e add upgrade note for 4.0 to changelog 2023-04-20 12:59:42 +02:00
Min RK
10e7ab96e5 Bump to 4.0.0 2023-04-20 12:18:26 +02:00
Min RK
40f519544f Merge pull request #4424 from minrk/changelog-4f
final changelog for 4.0.0
2023-04-20 12:17:56 +02:00
Min RK
076c14dce6 final changelog for 4.0.0 2023-04-20 11:17:52 +02:00
Erik Sundell
e223ce59e1 Merge pull request #4423 from minrk/diataxis-redirects
add remaining redirects for docs reorg
2023-04-20 10:59:36 +02:00
Min RK
ad833755e1 update comment for rediraffe check conditions 2023-04-20 09:14:57 +02:00
Min RK
142978b4d8 Merge pull request #4417 from manics/server-admin-list-as-table
Server admin: word-wrap lists
2023-04-20 09:11:21 +02:00
Min RK
e3cab48039 github.ref is always branch name on origin in PRs 2023-04-19 16:02:43 +02:00
Min RK
203f4a5855 test PRs against base ref
rather than making assumptions about checkouts and origins
2023-04-19 15:56:09 +02:00
Erik Sundell
cfc27db43d ci: fix failure getting latest tag for make rediraffecheckdiff 2023-04-19 15:48:23 +02:00
Erik Sundell
e2a8557083 ci: don't run make rediraffecheckdiff in forks 2023-04-19 15:41:24 +02:00
Erik Sundell
d5478b1f21 maint: let rediraffecheckdiff compare with origin/main, not main 2023-04-19 15:08:00 +02:00
Erik Sundell
cf19af6f1c ci: provide git history for make rediraffecheckdiff 2023-04-19 14:52:36 +02:00
Min RK
1342f00d8e move redirect line for 4.0 to bottom so rediraffewritediff adds in the right place 2023-04-19 13:40:17 +02:00
Min RK
1e49b4379b set rediraffe auto redirect percentage to 80% 2023-04-19 13:37:17 +02:00
Min RK
a5d563217c check redirects in test-docs
check all for:

- this PR
- latest tag
- longer term (3.0)
2023-04-19 13:27:51 +02:00
Min RK
b1ac3b82dc complete redirects for diataxis reorg
ran rediraffecheckdiff with rediraffe_branch=3.1.1

add a marker indicating that redirects are up-to-date for 4.0
2023-04-19 13:26:04 +02:00
mouse1203
a376f33af1 Update test.yml
Update test.yml
2023-04-17 10:25:26 +02:00
mouse1203
6f8a49569b Update test.yml
Update test.yml - added "if matrix.playwright"
2023-04-17 10:16:34 +02:00
mouse1203
a4c553a5c5 Merge remote-tracking branch 'upstream/main' into playwright_more 2023-04-17 10:07:20 +02:00
Erik Sundell
75ebe40f86 Merge pull request #4419 from manics/disable-dev-traitlets
Disable dev traitlets
2023-04-16 15:33:41 +02:00
Simon Li
69d711929a Disable dev traitlets
JupyterHub CI is currently broken with dev traitlets: https://github.com/jupyterhub/jupyterhub/issues/4418

This temporarily disables it
2023-04-16 14:00:04 +01:00
Simon Li
4c12872dbf Dockerfile uses nodejs 12- undo upgrade of packages in yarn.lock 2023-04-15 23:07:33 +01:00
Simon Li
21cee1be31 Render tabel cells with multiple data items as RowListItem 2023-04-14 23:41:36 +01:00
Simon Li
00c782fd40 Update yarn.lock 2023-04-14 23:40:54 +01:00
Simon Li
b3f9635ecc ReactObjectTableViewer can handle components 2023-04-14 23:29:53 +01:00
Simon Li
8c10fb285e Convert ReactObjectTableViewer to tsx, remove horizontal option 2023-04-14 19:39:20 +01:00
Simon Li
8a3f5d8f2e Copy f29827028f/src/ReactObjectTableViewer.tsx 2023-04-14 19:30:48 +01:00
Simon Li
7b496a5b4a Server admin: lists are displayed as word-wrapped CSV 2023-04-14 18:02:05 +01:00
mouse1203
41445cffb4 Update pytest.ini
Update pytest.ini
Adding "and not playwright"
2023-04-14 16:29:59 +02:00
Simon Li
64e7705053 Server admin: lists are displayed as tables not csv joined 2023-04-14 15:22:51 +01:00
mouse1203
dafd2d67f6 Update test.yml
Update test.yml
2023-04-14 16:09:57 +02:00
mouse1203
823ab58f3a update test.yml
update test.yml
2023-04-14 15:54:23 +02:00
mouse1203
ab7883e5c3 Update test.yml
Update test.yml: added install playwright
2023-04-14 15:45:14 +02:00
mouse1203
8fd1fb3234 added playwright with settings
added one case with settings
2023-04-14 15:22:16 +02:00
Min RK
6502b50576 Merge pull request #4416 from crazytan/patch-1
Remove bracket around link text without address
2023-04-14 07:15:03 +02:00
Jia Tan
861347cce0 Remove bracket around link text without address. 2023-04-13 15:35:11 -07:00
Erik Sundell
43d4b65250 Merge pull request #4409 from consideRatio/pr/dependabot-rename
dependabot: rename to .yaml
2023-04-07 16:00:22 +02:00
Erik Sundell
e53ce19fcc dependabot: rename to .yaml 2023-04-05 10:31:52 +02:00
Erik Sundell
e603ff8274 Merge pull request #4408 from consideRatio/pr/dependabot-syntax-fix
dependabot: fix syntax error of not using quotes for ##:##
2023-04-04 22:37:50 +02:00
Erik Sundell
22b15f0ecf dependabot: fix syntax error of not using quotes for ##:## 2023-04-04 22:36:43 +02:00
Erik Sundell
c48c5bce99 Merge pull request #4403 from consideRatio/pr/dependabot-monthly
dependabot: monthly updates of github actions
2023-04-04 22:35:15 +02:00
Erik Sundell
fa11d7e3c6 Add ci label to dependabot updates of github actions 2023-04-04 22:34:56 +02:00
Erik Sundell
7e3f29d033 Merge pull request #4404 from jupyterhub/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-04-04 08:47:53 +02:00
pre-commit-ci[bot]
b7827687a8 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/PyCQA/autoflake: v2.0.1 → v2.0.2](https://github.com/PyCQA/autoflake/compare/v2.0.1...v2.0.2)
- [github.com/psf/black: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0)
2023-04-04 06:21:53 +00:00
Erik Sundell
0beb4639a3 dependabot: monthly updates of github actions 2023-04-01 11:44:00 +02:00
Simon Li
b010c9501e Merge pull request #4402 from minrk/named-server-trailing-slash
make sure named server URLs include trailing slash
2023-03-30 20:07:55 +01:00
Min RK
295e92270b make sure named server URLs include trailing slash 2023-03-30 12:29:56 +02:00
Min RK
e42066f1c9 Merge pull request #4394 from alekseyolg/patch-1
Reduce size of jupyterhub image
2023-03-30 09:38:55 +02:00
Aleksey Karpov
1d29fcbfb2 Update Dockerfile
The same apt command in the entire file.
2023-03-29 14:46:28 +03:00
Aleksey Karpov
bdbfbb7e32 Update Dockerfile
Silently updating the list of available apt packages.
2023-03-29 14:44:00 +03:00
Aleksey Karpov
42314ed75b Apply suggestions from code review
Co-authored-by: Min RK <benjaminrk@gmail.com>
2023-03-29 14:15:20 +03:00
Aleksey Karpov
d8141692ab Update Dockerfile
Co-authored-by: Min RK <benjaminrk@gmail.com>
2023-03-29 14:12:12 +03:00
Aleksey Karpov
025db2f9f3 Update Dockerfile
removed the installation of apt packages from the cache due to the fact that the tests did not pass.
2023-03-24 15:22:01 +03:00
Aleksey Karpov
3985140377 Update test.yml
Add env DOCKER_BUILDKIT
2023-03-24 15:06:24 +03:00
Aleksey Karpov
6886384ca3 Update Dockerfile
Add mount cache
2023-03-24 14:49:11 +03:00
Erik Sundell
4a7fe8648a Merge pull request #4400 from minrk/intersect-server-scopes
add Spawner.server_token_scopes config
2023-03-23 11:48:52 +01:00
Min RK
7383c0cf60 esnure activity permissions are present in server tokens
with a warning

avoids case where custom server token permissions remove necessary permissions for posting activity updates
2023-03-23 10:58:19 +01:00
Min RK
83186e02a2 Do not give JUPYTERHUB_API_TOKEN access to other user servers
never intended, but limiting to server wasn't possible before

No change, except when one user has multiple servers running simultaneously.
2023-03-23 10:23:53 +01:00
Erik Sundell
c6b4577c0a Merge pull request #4399 from minrk/more-db-doc
add some more detail and examples to database doc
2023-03-22 14:19:59 +01:00
Min RK
73b1922c17 add Spawner.server_token_scopes config
consistent behavior with oauth_client_allowed_scopes,
where the _intersection_ of requested and owner-held permissions is granted,
instead of failing

Enables different users to have different permissions in $JUPTYERHUB_API_TOKEN,
either via callables or via requesting as much as you may want and only granting the subset.

Additionally, the !server filter can now be correctly applied to the server token

default behavior is unchanged
2023-03-22 13:56:58 +01:00
Min RK
1430e02fa8 fix db url for mysqlclient 2023-03-22 13:56:14 +01:00
Min RK
9ef09a288a add some more detail and examples to database doc
include actual configuration samples for postgres/mysql
2023-03-22 11:31:33 +01:00
Min RK
4a093be938 test with mysqlclient
as recommended by sqlalchemy
2023-03-22 10:33:51 +01:00
Simon Li
64a253dbef Merge pull request #4398 from ryanlovett/docs-managed-groups
Fix variable spelling.
2023-03-18 15:42:04 +00:00
ryanlovett
54877025ca Fix variable spelling.
The variable is `manage_groups`, although some method and function names use "managed".
2023-03-17 10:13:52 -07:00
Aleksey Karpov
555969141e Update Dockerfile
Add env PYTHONDONTWRITEBYTECODE=1
2023-03-15 12:16:00 +03:00
Aleksey Karpov
a938982bdc Update Dockerfile
Divided the assembly image into parts
2023-03-15 12:10:28 +03:00
Aleksey Karpov
60a153718d Update Dockerfile
Add python-is-python3 
https://github.com/jupyterhub/jupyterhub/pull/4199
2023-03-15 08:20:46 +03:00
Aleksey Karpov
d72a96ec17 Update Dockerfile
Reduced the number of layers, optimized the assembly, reduced the size of the final image, removed the logs, removed unnecessary commands.
2023-03-14 21:43:48 +03:00
70 changed files with 11599 additions and 9663 deletions

View File

@@ -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:
# - Status and logs from dependabot are provided at
@@ -8,8 +8,9 @@ version: 2
updates:
# Maintain dependencies in our GitHub Workflows
- package-ecosystem: github-actions
directory: "/"
directory: /
labels: [ci]
schedule:
interval: weekly
interval: monthly
time: "05:00"
timezone: "Etc/UTC"
timezone: Etc/UTC

View File

@@ -84,7 +84,7 @@ jobs:
publish-docker:
runs-on: ubuntu-20.04
timeout-minutes: 30
timeout-minutes: 20
services:
# So that we can test this in PRs/branches

View File

@@ -25,7 +25,7 @@ jobs:
Our goal is to sustain a positive experience for both users and developers. We use GitHub issues for specific discussions related to changing a repository's content, and let the forum be where we can more generally help and inspire each other.
Thanks you for being an active member of our community! :heart:
Thank you for being an active member of our community! :heart:
close-issue: true
lock-issue: false
issue-lock-reason: "off-topic"

View File

@@ -49,6 +49,11 @@ jobs:
runs-on: ubuntu-20.04
steps:
- 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
with:
python-version: "3.9"
@@ -72,3 +77,31 @@ jobs:
run: |
cd docs
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

View File

@@ -99,7 +99,7 @@ jobs:
noextension: noextension
subset: singleuser
- python: "3.11"
selenium: selenium
browser: browser
- python: "3.11"
main_dependencies: main_dependencies
@@ -114,7 +114,7 @@ jobs:
fi
if [ "${{ matrix.db }}" == "mysql" ]; then
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
if [ "${{ matrix.ssl }}" == "ssl" ]; then
echo "SSL_ENABLED=1" >> $GITHUB_ENV
@@ -164,7 +164,9 @@ jobs:
fi
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
fi
if [ "${{ matrix.legacy_notebook }}" != "" ]; then
@@ -175,7 +177,7 @@ jobs:
pip install "jupyter_server==${{ matrix.jupyter_server }}"
fi
if [ "${{ matrix.db }}" == "mysql" ]; then
pip install mysql-connector-python
pip install mysqlclient
fi
if [ "${{ matrix.db }}" == "postgres" ]; then
pip install psycopg2-binary
@@ -227,9 +229,13 @@ jobs:
DB=postgres bash ci/init-db.sh
fi
- name: Configure selenium tests
if: matrix.selenium
run: echo "PYTEST_ADDOPTS=$PYTEST_ADDOPTS -m selenium" >> "${GITHUB_ENV}"
- name: Configure browser tests
if: matrix.browser
run: echo "PYTEST_ADDOPTS=$PYTEST_ADDOPTS -m browser" >> "${GITHUB_ENV}"
- name: Ensure browsers are installed for playwright
if: matrix.browser
run: python -m playwright install --with-deps
- name: Run pytest
run: |
@@ -246,9 +252,8 @@ jobs:
- name: build images
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:alpine -f dockerfiles/Dockerfile.alpine .
docker build -t jupyterhub/singleuser singleuser
- name: smoke test jupyterhub

View File

@@ -16,7 +16,7 @@ ci:
repos:
# Autoformat: Python code, syntax patterns are modernized
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.1
rev: v3.4.0
hooks:
- id: pyupgrade
args:
@@ -24,7 +24,7 @@ repos:
# Autoformat: Python code
- repo: https://github.com/PyCQA/autoflake
rev: v2.0.1
rev: v2.1.1
hooks:
- id: autoflake
# args ref: https://github.com/PyCQA/autoflake#advanced-usage
@@ -39,13 +39,13 @@ repos:
# Autoformat: Python code
- repo: https://github.com/psf/black
rev: 23.1.0
rev: 23.3.0
hooks:
- id: black
# Autoformat: markdown, yaml, javascript (see the file .prettierignore)
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.0-alpha.6
rev: v3.0.0-alpha.9-for-vscode
hooks:
- id: prettier

View File

@@ -21,83 +21,116 @@
# your jupyterhub_config.py will be added automatically
# from your docker directory.
######################################################################
# This Dockerfile uses multi-stage builds with optimisations to build
# the JupyterHub wheel on the native architecture only
# https://www.docker.com/blog/faster-multi-platform-builds-dockerfile-cross-compilation-guide/
ARG BASE_IMAGE=ubuntu:22.04
FROM $BASE_IMAGE AS builder
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update \
&& apt-get install -yq --no-install-recommends \
######################################################################
# The JupyterHub wheel is pure Python so can be built for any platform
# on the native architecture (avoiding QEMU emulation)
FROM --platform=${BUILDPLATFORM:-linux/amd64} $BASE_IMAGE AS jupyterhub-builder
ENV DEBIAN_FRONTEND=noninteractive
# Don't clear apt cache, and don't combine RUN commands, so that cached layers can
# be reused in other stages
RUN apt-get update -qq \
&& apt-get install -yqq --no-install-recommends \
build-essential \
ca-certificates \
curl \
locales \
python3-dev \
python3-pip \
python3-pycurl \
python3-venv \
&& python3 -m pip install --no-cache-dir --upgrade setuptools pip build wheel
# Ubuntu 22.04 comes with Nodejs 12 which is too old for building JupyterHub JS
# It's fine at runtime though (used only by configurable-http-proxy)
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -yqq --no-install-recommends \
nodejs \
npm \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN python3 -m pip install --upgrade setuptools pip build wheel
RUN npm install --global yarn
&& npm install --global yarn
WORKDIR /src/jupyterhub
# copy everything except whats in .dockerignore, its a
# compromise between needing to rebuild and maintaining
# what needs to be part of the build
COPY . /src/jupyterhub/
WORKDIR /src/jupyterhub
COPY . .
# Build client component packages (they will be copied into ./share and
# packaged with the built wheel.)
RUN python3 -m build --wheel
RUN python3 -m pip wheel --wheel-dir wheelhouse dist/*.whl
ARG PIP_CACHE_DIR=/tmp/pip-cache
RUN --mount=type=cache,target=${PIP_CACHE_DIR} \
python3 -m build --wheel
FROM $BASE_IMAGE
USER root
######################################################################
# All other wheels required by JupyterHub, some are platform specific
FROM $BASE_IMAGE AS wheel-builder
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -yq --no-install-recommends \
RUN apt-get update -qq \
&& apt-get install -yqq --no-install-recommends \
build-essential \
ca-certificates \
curl \
gnupg \
locales \
python3-dev \
python3-pip \
python3-pycurl \
nodejs \
npm \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
python3-venv \
&& python3 -m pip install --no-cache-dir --upgrade setuptools pip build wheel
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 \
LANG=en_US.UTF-8 \
LANGUAGE=en_US.UTF-8
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/
LANGUAGE=en_US.UTF-8 \
PYTHONDONTWRITEBYTECODE=1
EXPOSE 8000
LABEL maintainer="Jupyter Project <jupyter@googlegroups.com>"
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"]

View File

@@ -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"]

View File

@@ -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"

View File

@@ -6,7 +6,7 @@ info:
description: The REST API for JupyterHub
license:
name: BSD-3-Clause
version: 4.0.0b2
version: 4.0.2
servers:
- url: /hub/api
security:
@@ -1202,13 +1202,13 @@ components:
description: Timestamp of last-seen activity from the user
format: date-time
servers:
type: array
type: object
description: |
The servers for this user.
By default: only includes _active_ servers.
Changed in 3.0: if `?include_stopped_servers` parameter is specified,
stopped servers will be included as well.
items:
additionalProperties:
$ref: "#/components/schemas/Server"
auth_state:
type: object

View File

@@ -201,6 +201,7 @@ intersphinx_mapping = {
"python": ("https://docs.python.org/3/", None),
"tornado": ("https://www.tornadoweb.org/en/stable/", None),
"jupyter-server": ("https://jupyter-server.readthedocs.io/en/stable/", None),
"nbgitpuller": ("https://nbgitpuller.readthedocs.io/en/latest", None),
}
# -- Options for the opengraph extension -------------------------------------
@@ -235,8 +236,12 @@ ogp_use_first_image = True
# If you are basing changes off another branch/ commit, always change back
# rediraffe_branch to main before pushing your changes upstream.
#
rediraffe_branch = "main"
rediraffe_branch = os.environ.get("REDIRAFFE_BRANCH", "main")
rediraffe_redirects = "redirects.txt"
# allow 80% match for autogenerated redirects
rediraffe_auto_redirect_perc = 80
# rediraffe_redirects = {
# "old-file": "new-folder/new-file-name",
# }

View File

@@ -130,8 +130,8 @@ configuration:
jupyterhub -f testing/jupyterhub_config.py
```
The default JupyterHub [authenticator](https://jupyterhub.readthedocs.io/en/stable/reference/authenticators.html#the-default-pam-authenticator)
& [spawner](https://jupyterhub.readthedocs.io/en/stable/api/spawner.html#localprocessspawner)
The default JupyterHub [authenticator](PAMAuthenticator)
& [spawner](LocalProcessSpawner)
require your system to have user accounts for each user you want to log in to
JupyterHub as.

View File

@@ -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][].
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
### 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
- `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
where traditional RDBMS may be a better choice](https://sqlite.org/whentouse.html).
### 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
@@ -132,14 +140,25 @@ multiple processes which might try to access the file at the same time.
### PostgreSQL
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
additional configuration required for MySQL that is not needed for PostgreSQL.
MySQL or PostgreSQL or if you do not have a strong preference.
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
- You should use the `pymysql` sqlalchemy provider (the other one, MySQLdb,
isn't available for py3).
- You also need to set `pool_recycle` to some value (typically 60 - 300)
- 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)
- You also need to set `pool_recycle` to some value (typically 60 - 300, JupyterHub will default to 60)
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
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
default, as well as have a default `DYNAMIC` `row_format` and pose no trouble
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"
```

View File

@@ -2,35 +2,75 @@
## How do I share links to notebooks?
In short, where you see `/user/name/notebooks/foo.ipynb` use `/hub/user-redirect/notebooks/foo.ipynb` (replace `/user/name` with `/hub/user-redirect`).
Sharing links to notebooks is a common activity,
and can look different based on what you mean.
and can look different depending on what you mean by 'share.'
Your first instinct might be to copy the URL you see in the browser,
e.g. `hub.jupyter.org/user/yourname/notebooks/coolthing.ipynb`.
However, let's break down what this URL means:
e.g. `jupyterhub.example/user/yourname/notebooks/coolthing.ipynb`,
but this usually won't work, depending on the permissions of the person you share the link with.
`hub.jupyter.org/user/yourname/` is the URL prefix handled by _your server_,
which means that sharing this URL is asking the person you share the link with
to come to _your server_ and look at the exact same file.
In most circumstances, this is forbidden by permissions because the person you share with does not have access to your server.
What actually happens when someone visits this URL will depend on whether your server is running and other factors.
Unfortunately, 'share' means at least a few things to people in a JupyterHub context.
We'll cover 3 common cases here, when they are applicable, and what assumptions they make:
**But what is our actual goal?**
1. sharing links that will open the same file on the visitor's own server
2. sharing links that will bring the visitor to _your_ server (e.g. for real-time collaboration, or RTC)
3. publishing notebooks and sharing links that will download the notebook into the user's server
A typical situation is that you have some shared or common filesystem,
such that the same path corresponds to the same document
(either the exact same document or another copy of it).
Typically, what folks want when they do sharing like this
is for each visitor to open the same file _on their own server_,
so Breq would open `/user/breq/notebooks/foo.ipynb` and
Seivarden would open `/user/seivarden/notebooks/foo.ipynb`, etc.
### link to the same file on the visitor's server
JupyterHub has a special URL that does exactly this!
It's called `/hub/user-redirect/...`.
So if you replace `/user/yourname` in your URL bar
with `/hub/user-redirect` any visitor should get the same
URL on their own server, rather than visiting yours.
This is for the case where you have JupyterHub on a shared (or sufficiently similar) filesystem, where you want to share a link that will cause users to login and start their _own_ server, to view or edit the file.
In JupyterLab 2.0, this should also be the result of the "Copy Shareable Link"
action in the file browser.
**Assumption:** the same path on someone else's server is valid and points to the same file
This is useful in e.g. classes where you know students have certain files in certain locations, or collaborations where you know you have a shared filesystem where everyone has access to the same files.
A link should look like `https://jupyterhub.example/hub/user-redirect/lab/tree/foo.ipynb`.
You can hand-craft these URLs from the URL you are looking at, where you see `/user/name/lab/tree/foo.ipynb` use `/hub/user-redirect/lab/tree/foo.ipynb` (replace `/user/name/` with `/hub/user-redirect/`).
Or you can use JupyterLab's "copy shareable link" in the context menu in the file browser:
![copy shareable link in JupyterLab](../images/shareable_link.webp)
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

View File

@@ -66,7 +66,7 @@ Here is a sample of organizations that use JupyterHub:
- **Universities and colleges**: UC Berkeley, UC San Diego, Cal Poly SLO, Harvard University, University of Chicago,
University of Oslo, University of Sheffield, Université Paris Sud, University of Versailles
- **Research laboratories**: NASA, NCAR, NOAA, the Large Synoptic Survey Telescope, Brookhaven National Lab,
Minnesota Supercomputing Institute, ALCF, CERN, Lawrence Livermore National Laboratory
Minnesota Supercomputing Institute, ALCF, CERN, Lawrence Livermore National Laboratory, HUNT
- **Online communities**: Pangeo, Quantopian, mybinder.org, MathHub, Open Humans
- **Computing infrastructure providers**: NERSC, San Diego Supercomputing Center, Compute Canada
- **Companies**: Capital One, SANDVIK code, Globus
@@ -130,7 +130,7 @@ level for several years, and makes a number of "default" security decisions that
users.
- For security considerations in the base JupyterHub application,
[see the JupyterHub security page](https://jupyterhub.readthedocs.io/en/stable/reference/websecurity.html).
[see the JupyterHub security page](web-security).
- For security considerations when deploying JupyterHub on Kubernetes, see the
[JupyterHub on Kubernetes security page](https://z2jh.jupyter.org/en/latest/security.html).

View File

@@ -167,7 +167,7 @@ When your whole JupyterHub sits behind an organization proxy (_not_ a reverse pr
### Launching Jupyter Notebooks to run as an externally managed JupyterHub service with the `jupyterhub-singleuser` command returns a `JUPYTERHUB_API_TOKEN` error
[JupyterHub services](https://jupyterhub.readthedocs.io/en/stable/reference/services.html) allow processes to interact with JupyterHub's REST API. Example use-cases include:
{ref}`services` allow processes to interact with JupyterHub's REST API. Example use-cases include:
- **Secure Testing**: provide a canonical Jupyter Notebook for testing production data to reduce the number of entry points into production systems.
- **Grading Assignments**: provide access to shared Jupyter Notebooks that may be used for management tasks such as grading assignments.

View File

@@ -45,7 +45,7 @@ additional packages.
## 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)
have their own configuration systems.
@@ -212,13 +212,31 @@ By default, the single-user server launches JupyterLab,
which is based on [Jupyter Server][].
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:
```bash
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 notebook]: https://jupyter-notebook.readthedocs.io

View File

@@ -33,36 +33,13 @@ such as:
To send requests using the JupyterHub API, you must pass an API token with
the request.
The preferred way of generating an API token is by running:
```bash
openssl rand -hex 32
```
This `openssl` command generates a potential token that can then be
added to JupyterHub using `.api_tokens` configuration setting in
`jupyterhub_config.py`.
```{note}
The api_tokens configuration has been softly deprecated since the introduction of services.
```
Alternatively, you can use the `jupyterhub token` command to generate a token
for a specific hub user by passing the **username**:
```bash
jupyterhub token <username>
```
This command generates a random string to use as a token and registers
it for the given user with the Hub's database.
In [version 0.8.0](changelog), a token request page for
generating an API token is available from the JupyterHub user interface:
While JupyterHub is running, any JupyterHub user can request a token via the `token` page.
This is accessible via a `token` link in the top nav bar from the JupyterHub home page,
or at the URL `/hub/token`.
:::{figure-md}
![token request page](../images/token-request.png)
![token request page](../images/token-page.png)
JupyterHub's API token page
:::
@@ -74,6 +51,40 @@ JupyterHub's token page after successfully requesting a token.
:::
### Register API tokens via configuration
Sometimes, you'll want to pre-generate a token for access to JupyterHub,
typically for use by external services,
so that both JupyterHub and the service have access to the same value.
First, you need to generate a good random secret.
A good way of generating an API token is by running:
```bash
openssl rand -hex 32
```
This `openssl` command generates a random token that can be added to the JupyterHub configuration in `jupyterhub_config.py`.
For external services, this would be registered with JupyterHub via configuration:
```python
c.JupyterHub.services = [
{
"name": "my-service",
"api_token": the_secret_value,
},
]
```
At this point, requests authenticated with the token will be associated with The service `my-service`.
```{note}
You can also load additional tokens for users via the `JupyterHub.api_tokens` configuration.
However, this option has been deprecated since the introduction of services.
```
## Assigning permissions to a token
Prior to JupyterHub 2.0, there were two levels of permissions:

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

View File

@@ -1,23 +1,54 @@
# 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.
"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/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/config-ghoauth.md" "howto/configuration/config-ghoauth.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/templates.md" "howto/templates.md"
"quickstart-docker.md" "tutorial/quickstart-docker.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-user-env.md" "howto/configuration/config-user-env.md"
"reference/rest.md" "howto/rest.md"
"reference/separate-proxy.md" "howto/separate-proxy.md"
"admin/upgrading.md" "howto/upgrading.md"
"installation-basics.md" "tutorial/installation-basics.md"
"quickstart.md" "tutorial/quickstart.md"
"events/index.md" "reference/event-logging.md"
"reference/server-api.md" "tutorial/server-api.md"
"reference/websecurity.md" "explanation/websecurity.md"
"api/app.md" "reference/api/app.md"
"api/auth.md" "reference/api/auth.md"
"api/index.md" "reference/api/index.md"
"api/proxy.md" "reference/api/proxy.md"
"api/service.md" "reference/api/service.md"
"api/services.auth.md" "reference/api/services.auth.md"
"api/spawner.md" "reference/api/spawner.md"
"api/user.md" "reference/api/user.md"
# -- JupyterHub 4.0 --
# redirects above are up-to-date as of JupyterHub 4.0
# add future redirects below
# (e.g. with `make rediraffewritediff`)

View File

@@ -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.
This is now possible with `Authenticator.managed_groups`.
This is now possible with `Authenticator.manage_groups`.
You can set the config:

View File

@@ -8,9 +8,92 @@ command line for details.
## [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.
:::{admonition} Upgrade note
Upgrading from 3.1 to 4.0 should require no additional action beyond running `jupyterhub --upgrade-db` to upgrade the database schema after upgrading the package version.
It is otherwise a regular jupyterhub [upgrade](upgrading-jupyterhub).
:::
There are three major changes that _should_ be invisible to most users:
1. Groups can now have 'properties', editable via the admin page, which can be used by Spawners for their operations.
@@ -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.
([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
@@ -33,18 +116,21 @@ In addition to these, thanks to contributions from this years Outreachy interns,
#### 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))
- 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))
#### 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))
- 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))
#### 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))
- 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))
@@ -57,6 +143,13 @@ In addition to these, thanks to contributions from this years Outreachy interns,
#### 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))
- 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))
@@ -81,6 +174,9 @@ In addition to these, thanks to contributions from this years Outreachy interns,
#### 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))
- 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))
@@ -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.
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
@@ -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))
- The configuration `uids` for local authenticators is added to consistently assign users UNIX id's between installations [#2687](https://github.com/jupyterhub/jupyterhub/pull/2687) ([@rgerkin](https://github.com/rgerkin))
- `JupyterHub.activity_resolution` is introduced with a default value of 30s improving performance by not updating the database with user activity too often [#2605](https://github.com/jupyterhub/jupyterhub/pull/2605) ([@minrk](https://github.com/minrk))
- [HubAuth](https://jupyterhub.readthedocs.io/en/stable/api/services.auth.html#jupyterhub.services.auth.HubAuth)'s SSL configuration can now be set through environment variables [#2588](https://github.com/jupyterhub/jupyterhub/pull/2588) ([@cmd-ntrf](https://github.com/cmd-ntrf))
- [HubAuth](jupyterhub.services.auth.HubAuth)'s SSL configuration can now be set through environment variables [#2588](https://github.com/jupyterhub/jupyterhub/pull/2588) ([@cmd-ntrf](https://github.com/cmd-ntrf))
- Expose spawner.user_options in REST API. [#2755](https://github.com/jupyterhub/jupyterhub/pull/2755) ([@danielballan](https://github.com/danielballan))
- add block for scripts included in head [#2828](https://github.com/jupyterhub/jupyterhub/pull/2828) ([@bitnik](https://github.com/bitnik))
- Instrument JupyterHub to record events with jupyter_telemetry [Part II] [#2698](https://github.com/jupyterhub/jupyterhub/pull/2698) ([@Zsailer](https://github.com/Zsailer))

View File

@@ -14,6 +14,12 @@ section, the `jupyterhub_config.py` can be automatically generated via
> jupyterhub --generate-config
> ```
Most of this information is available in a nicer format in:
- [](./api/app.md)
- [](./api/auth.md)
- [](./api/spawner.md)
The following contains the output of that command for reference.
```{eval-rst}

View File

@@ -1,6 +1,6 @@
# 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

View File

@@ -63,6 +63,15 @@ easy to do with RStudio too.
- [jupyterhub-deploy-teaching](https://github.com/jupyterhub/jupyterhub-deploy-teaching) based on work by Brian Granger for Cal Poly's Data Science 301 Course
### CERN
[CERN](https://home.cern/), also known as the European Organization for Nuclear Research, is a world-renowned scientific research centre and the home of the Large Hadron Collider (LHC).
Within CERN, there are two noteworthy JupyterHub deployments in operation:
- [SWAN](https://swan.web.cern.ch/swan/), which stands for Service for Web based Analysis, serves as an interactive data analysis platform primarily utilized at CERN.
- [VRE](https://vre-hub.github.io/), which stands for Virtual Research Environment, is an analysis platform developed within the [EOSC Project](https://eoscfuture.eu/) to cater to the needs of scientific communities involved in European projects.
### Chameleon
[Chameleon](https://www.chameleoncloud.org) is a NSF-funded configurable experimental environment for large-scale computer science systems research with [bare metal reconfigurability](https://chameleoncloud.readthedocs.io/en/latest/technical/baremetal.html). Chameleon users utilize JupyterHub to document and reproduce their complex CISE and networking experiments.

View File

@@ -39,7 +39,7 @@ openssl rand -hex 32
In [version 0.8.0](changelog), a TOKEN request page for
generating an API token is available from the JupyterHub user interface:
![Request API TOKEN page](/images/token-request.png)
![Request API TOKEN page](/images/token-page.png)
![API TOKEN success page](/images/token-request-success.png)

View File

@@ -2,4 +2,4 @@
An example of enabling real-time collaboration with dedicated accounts for collaborations.
See [collaboration account docs](docs/source/tutorial/collaboration-accounts.md) for details.
See [collaboration account docs](../../docs/source/tutorial/collaboration-users.md) for details.

View File

@@ -25,51 +25,49 @@
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|less)$": "identity-obj-proxy"
}
},
"testEnvironment": "jsdom"
},
"dependencies": {
"bootstrap": "^4.5.3",
"history": "^5.0.0",
"lodash.debounce": "^4.0.8",
"prop-types": "^15.7.2",
"react": "^17.0.1",
"react-bootstrap": "^2.1.1",
"react-dom": "^17.0.1",
"react-icons": "^4.1.0",
"react-multi-select-component": "^3.0.7",
"react-object-table-viewer": "^1.0.7",
"react-redux": "^7.2.2",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"recompose": "npm:react-recompose@^0.31.2",
"redux": "^4.0.5",
"regenerator-runtime": "^0.13.9"
"bootstrap": "^5.2.3",
"history": "^5.3.0",
"lodash": "^4.17.21",
"prop-types": "^15.8.1",
"react": "^17.0.2",
"react-bootstrap": "^2.7.4",
"react-dom": "^17.0.2",
"react-icons": "^4.8.0",
"react-multi-select-component": "^4.3.4",
"react-redux": "^7.2.8",
"react-router-dom": "^5.3.4",
"recompose": "npm:react-recompose@^0.33.0",
"redux": "^4.2.1",
"regenerator-runtime": "^0.13.11"
},
"devDependencies": {
"@babel/core": "^7.12.3",
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@testing-library/jest-dom": "^5.15.1",
"@testing-library/react": "^12.1.2",
"@babel/core": "^7.21.4",
"@babel/preset-env": "^7.21.4",
"@babel/preset-react": "^7.18.6",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^13.5.0",
"@webpack-cli/serve": "^1.7.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.5",
"babel-jest": "^26.6.3",
"babel-loader": "^8.2.1",
"css-loader": "^5.0.1",
"enzyme": "^3.11.0",
"eslint": "^7.18.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-unused-imports": "^1.1.1",
"@webpack-cli/serve": "^2.0.1",
"babel-jest": "^29.5.0",
"babel-loader": "^9.1.2",
"css-loader": "^6.7.3",
"eslint": "^8.38.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-unused-imports": "^2.0.0",
"file-loader": "^6.2.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3",
"prettier": "^2.2.1",
"sinon": "^13.0.1",
"style-loader": "^2.0.0",
"webpack": "^5.76.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3"
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"prettier": "^2.8.7",
"sinon": "^15.0.3",
"style-loader": "^3.3.2",
"webpack": "^5.79.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.13.3"
}
}

View 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.

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, Fragment } from "react";
import { useSelector, useDispatch } from "react-redux";
import { debounce } from "lodash";
import PropTypes from "prop-types";
@@ -12,7 +12,7 @@ import {
CardGroup,
Collapse,
} from "react-bootstrap";
import ReactObjectTableViewer from "react-object-table-viewer";
import ReactObjectTableViewer from "../ReactObjectTableViewer/ReactObjectTableViewer";
import { Link } from "react-router-dom";
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
@@ -29,6 +29,13 @@ const AccessServerButton = ({ url }) => (
</a>
);
const RowListItem = ({ text }) => (
<span className="server-dashboard-row-list-item">{text}</span>
);
RowListItem.propTypes = {
text: PropTypes.string,
};
const ServerDashboard = (props) => {
let base_url = window.base_url || "/";
// sort methods
@@ -67,6 +74,7 @@ const ServerDashboard = (props) => {
shutdownHub,
startServer,
stopServer,
deleteServer,
startAll,
stopAll,
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 }) => {
var [isDisabled, setIsDisabled] = useState(false);
return (
@@ -236,8 +288,13 @@ const ServerDashboard = (props) => {
break;
}
if (Array.isArray(value)) {
// cast arrays (e.g. roles, groups) to string
value = value.sort().join(", ");
value = (
<Fragment>
{value.sort().flatMap((v) => (
<RowListItem text={v} />
))}
</Fragment>
);
}
result[key] = value;
return result;
@@ -266,7 +323,11 @@ const ServerDashboard = (props) => {
const userServerName = user.name + serverNameDash;
const open = collapseStates[userServerName] || false;
return [
<tr key={`${userServerName}-row`} className="user-row">
<tr
key={`${userServerName}-row`}
data-testid={`user-row-${userServerName}`}
className="user-row"
>
<td data-testid="user-row-name">
<span>
<Button
@@ -312,6 +373,10 @@ const ServerDashboard = (props) => {
userName={user.name}
style={{ marginRight: 20 }}
/>
<DeleteServerButton
serverName={server.name}
userName={user.name}
/>
<a
href={`${base_url}spawn/${user.name}${
server.name ? "/" + server.name : ""
@@ -570,6 +635,7 @@ ServerDashboard.propTypes = {
shutdownHub: PropTypes.func,
startServer: PropTypes.func,
stopServer: PropTypes.func,
deleteServer: PropTypes.func,
startAll: PropTypes.func,
stopAll: PropTypes.func,
dispatch: PropTypes.func,

View File

@@ -2,7 +2,7 @@ import React from "react";
import "@testing-library/jest-dom";
import { act } from "react-dom/test-utils";
import userEvent from "@testing-library/user-event";
import { render, screen, fireEvent } from "@testing-library/react";
import { render, screen, fireEvent, getByText } from "@testing-library/react";
import { HashRouter, Switch } from "react-router-dom";
import { Provider, useSelector } from "react-redux";
import { createStore } from "redux";
@@ -43,6 +43,31 @@ var mockAsync = (data) =>
var mockAsyncRejection = () =>
jest.fn().mockImplementation(() => Promise.reject());
var bar_servers = {
"": {
name: "",
last_activity: "2020-12-07T20:58:02.437408Z",
started: "2020-12-07T20:58:01.508266Z",
pending: null,
ready: false,
state: { pid: 12345 },
url: "/user/bar/",
user_options: {},
progress_url: "/hub/api/users/bar/progress",
},
servername: {
name: "servername",
last_activity: "2020-12-07T20:58:02.437408Z",
started: "2020-12-07T20:58:01.508266Z",
pending: null,
ready: false,
state: { pid: 12345 },
url: "/user/bar/servername",
user_options: {},
progress_url: "/hub/api/users/bar/servername/progress",
},
};
var mockAppState = () =>
Object.assign({}, initialState, {
user_data: [
@@ -78,19 +103,7 @@ var mockAppState = () =>
pending: null,
created: "2020-12-07T18:46:27.115528Z",
last_activity: "2020-12-07T20:43:51.013613Z",
servers: {
"": {
name: "",
last_activity: "2020-12-07T20:58:02.437408Z",
started: "2020-12-07T20:58:01.508266Z",
pending: null,
ready: false,
state: { pid: 12345 },
url: "/user/bar/",
user_options: {},
progress_url: "/hub/api/users/bar/progress",
},
},
servers: bar_servers,
},
],
user_page: {
@@ -150,9 +163,11 @@ test("Renders users from props.user_data into table", async () => {
let foo = screen.getByTestId("user-name-div-foo");
let bar = screen.getByTestId("user-name-div-bar");
let bar_server = screen.getByTestId("user-name-div-bar-servername");
expect(foo).toBeVisible();
expect(bar).toBeVisible();
expect(bar_server).toBeVisible();
});
test("Renders correctly the status of a single-user server", async () => {
@@ -162,10 +177,13 @@ test("Renders correctly the status of a single-user server", async () => {
render(serverDashboardJsx(callbackSpy));
});
let start = screen.getByText("Start Server");
let stop = screen.getByText("Stop Server");
let start_elems = screen.getAllByText("Start Server");
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
start_elems.forEach((start) => {
expect(start).toBeVisible();
});
expect(start).toBeVisible();
let stop = screen.getByText("Stop Server");
expect(stop).toBeVisible();
});
@@ -176,9 +194,12 @@ test("Renders spawn page link", async () => {
render(serverDashboardJsx(callbackSpy));
});
let link = screen.getByText("Spawn Page").closest("a");
let url = new URL(link.href);
expect(url.pathname).toEqual("/spawn/bar");
for (let server in bar_servers) {
let row = screen.getByTestId(`user-row-bar${server ? "-" + server : ""}`);
let link = getByText(row, "Spawn Page").closest("a");
let url = new URL(link.href);
expect(url.pathname).toEqual("/spawn/bar" + (server ? "/" + server : ""));
}
});
test("Invokes the startServer event on button click", async () => {
@@ -188,10 +209,11 @@ test("Invokes the startServer event on button click", async () => {
render(serverDashboardJsx(callbackSpy));
});
let start = screen.getByText("Start Server");
let start_elems = screen.getAllByText("Start Server");
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
await act(async () => {
fireEvent.click(start);
fireEvent.click(start_elems[0]);
});
expect(callbackSpy).toHaveBeenCalled();
@@ -453,10 +475,11 @@ test("Shows a UI error dialogue when start user server fails", async () => {
);
});
let start = screen.getByText("Start Server");
let start_elems = screen.getAllByText("Start Server");
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
await act(async () => {
fireEvent.click(start);
fireEvent.click(start_elems[0]);
});
let errorDialog = screen.getByText("Failed to start server.");
@@ -487,10 +510,11 @@ test("Shows a UI error dialogue when start user server returns an improper statu
);
});
let start = screen.getByText("Start Server");
let start_elems = screen.getAllByText("Start Server");
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
await act(async () => {
fireEvent.click(start);
fireEvent.click(start_elems[0]);
});
let errorDialog = screen.getByText("Failed to start server.");
@@ -656,3 +680,20 @@ test("Interacting with PaginationFooter causes state update and refresh via useE
// expect(callbackSpy.mock.calls).toHaveLength(2);
// expect(callbackSpy).toHaveBeenCalledWith(2, 2, "");
});
test("Server delete button exists for named servers", async () => {
let callbackSpy = mockAsync();
await act(async () => {
render(serverDashboardJsx(callbackSpy));
});
for (let server in bar_servers) {
if (server === "") {
continue;
}
let row = screen.getByTestId(`user-row-bar-${server}`);
let delete_button = getByText(row, "Delete Server");
expect(delete_button).toBeEnabled();
}
});

View File

@@ -30,3 +30,11 @@
tr.noborder > td {
border: none !important;
}
.server-dashboard-row-list-item {
display: inline-block;
padding: 0 5px;
margin: 2px;
border: 1px solid #ddd;
border-radius: 2px;
}

View File

@@ -18,6 +18,12 @@ const withAPI = withProps(() => ({
jhapiRequest("/users/" + name + "/servers/" + (serverName || ""), "POST"),
stopServer: (name, serverName = "") =>
jhapiRequest("/users/" + name + "/servers/" + (serverName || ""), "DELETE"),
deleteServer: (name, serverName = "") =>
jhapiRequest(
"/users/" + name + "/servers/" + (serverName || ""),
"DELETE",
{ remove: true },
),
startAll: (names) =>
names.map((e) => jhapiRequest("/users/" + e + "/server", "POST")),
stopAll: (names) =>

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
# version_info updated by running `tbump`
version_info = (4, 0, 0, "b2", "")
version_info = (4, 0, 2, "", "")
# pep 440 version: no dot before beta/rc, but before .dev
# 0.1.0rc1

View File

@@ -89,6 +89,11 @@ class APIHandler(BaseHandler):
if not hasattr(self, '_jupyterhub_user'):
# called too early to check if we're token-authenticated
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 token-authenticated, ignore XSRF
return

View File

@@ -9,6 +9,7 @@ import logging
import os
import re
import secrets
import shlex
import signal
import socket
import ssl
@@ -2840,6 +2841,10 @@ class JupyterHub(Application):
super().initialize(*args, **kwargs)
if self.generate_config or self.generate_certs or self.subapp:
return
if self.extra_args:
self.exit(
f"Unrecognized command-line arguments: {' '.join(shlex.quote(arg) for arg in self.extra_args)!r}"
)
self._start_future = asyncio.Future()
def record_start(f):

View File

@@ -236,11 +236,13 @@ class BaseHandler(RequestHandler):
def check_xsrf_cookie(self):
try:
return super().check_xsrf_cookie()
except Exception as e:
# ensure _juptyerhub_user is defined on rejected requests
except web.HTTPError as e:
# ensure _jupyterhub_user is defined on rejected requests
if not hasattr(self, "_jupyterhub_user"):
self._jupyterhub_user = None
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
@property
@@ -1431,6 +1433,12 @@ class UserUrlHandler(BaseHandler):
# accept token auth for API requests that are probably to non-running servers
_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=''):
"""Fail an API request to a not-running server"""
self.log.warning(

View File

@@ -105,6 +105,7 @@ class LoginHandler(BaseHandler):
'next': self.get_argument('next', ''),
},
),
"xsrf": self.xsrf_token.decode('ascii'),
}
custom_html = Template(
self.authenticator.get_custom_html(self.hub.base_url)

View File

@@ -52,7 +52,7 @@ def get_default_roles():
'description': 'Post activity only',
'scopes': [
'users:activity!user',
'access:servers!user',
'access:servers!server',
],
},
{

View File

@@ -845,6 +845,15 @@ def needs_scope(*scopes):
def scope_decorator(func):
@functools.wraps(func)
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)
bound_sig = sig.bind(self, *args, **kwargs)
bound_sig.apply_defaults()
@@ -853,6 +862,11 @@ def needs_scope(*scopes):
self.expanded_scopes = {}
self.parsed_scopes = {}
try:
end_point = self.request.path
except AttributeError:
end_point = self.__name__
s_kwargs = {}
for resource in {'user', 'server', 'group', 'service'}:
resource_name = resource + '_name'
@@ -860,14 +874,10 @@ def needs_scope(*scopes):
resource_value = bound_sig.arguments[resource_name]
s_kwargs[resource] = resource_value
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)
if has_access:
return func(self, *args, **kwargs)
try:
end_point = self.request.path
except AttributeError:
end_point = self.__name__
app_log.warning(
"Not authorizing access to {}. Requires any of [{}], not derived from scopes [{}]".format(
end_point, ", ".join(scopes), ", ".join(self.expanded_scopes)

View File

@@ -6,7 +6,7 @@
.. versionchanged:: 2.0
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
@@ -27,7 +27,25 @@ JUPYTERHUB_SINGLEUSER_APP = _app_shortcuts.get(
JUPYTERHUB_SINGLEUSER_APP.replace("_", "-"), 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)
else:
App = None

View File

@@ -483,6 +483,11 @@ class JupyterHubSingleUser(ExtensionApp):
cfg.answer_yes = True
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
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
if url.port:
@@ -511,16 +516,25 @@ class JupyterHubSingleUser(ExtensionApp):
# Jupyter Server default: config files have higher priority than extensions,
# by:
# 1. load config files
# 1. load config files and CLI
# 2. load extension config
# 3. merge file config into extension config
# we invert that by merging our extension config into server config before
# they get merged the other way
# this way config from this extension should always have highest priority
# but this also puts our config above _CLI_ options,
# and CLI should come before env,
# so merge that into _our_ config before loading
if self.serverapp.cli_config:
for cls_name, cls_config in self.serverapp.cli_config.items():
if cls_name in self.config:
self.config[cls_name].merge(cls_config)
self.serverapp.update_config(self.config)
# add our custom templates
# config below here has _lower_ priority than user config
self.config.NotebookApp.extra_template_paths.append(SINGLEUSER_TEMPLATES_DIR)
@default("default_url")
@@ -608,7 +622,9 @@ class JupyterHubSingleUser(ExtensionApp):
app.web_app.settings[
"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
headers = app.web_app.settings.setdefault("headers", {})
headers["X-JupyterHub-Version"] = __version__

View File

@@ -669,7 +669,8 @@ class SingleUserNotebookAppMixin(Configurable):
# load the hub-related settings into the tornado settings dict
self.init_hub_auth()
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['group'] = self.group
s['hub_prefix'] = self.hub_prefix

View File

@@ -382,6 +382,23 @@ class Spawner(LoggingConfigurable):
scopes.append(f"access:servers!server={self.user.name}/{self.name}")
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(
False,
help="""Whether the Spawner will resume on next start

View 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()

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,7 @@ class JupyterHubTestHandler(JupyterHandler):
info = {
"current_user": self.current_user,
"config": self.app.config,
"root_dir": self.contents_manager.root_dir,
"disable_user_config": getattr(self.app, "disable_user_config", None),
"settings": self.settings,
"config_file_paths": self.app.config_file_paths,

View File

@@ -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()

View File

@@ -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

View File

@@ -122,6 +122,41 @@ async def test_xsrf_check(app, username, method, path, xsrf_in_url):
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
# --------------

View File

@@ -35,7 +35,7 @@ def generate_old_db(env_dir, hub_version, db_url):
pkgs.append('sqlalchemy<2')
if 'mysql' in db_url:
pkgs.append('mysql-connector-python')
pkgs.append('mysqlclient')
elif 'postgres' in db_url:
pkgs.append('psycopg2-binary')
check_call([env_pip, 'install'] + pkgs)

View File

@@ -848,8 +848,12 @@ async def test_server_token_role(app):
orm_server_token = orm.APIToken.find(app.db, server_token)
assert orm_server_token
server_role = orm.Role.find(app.db, 'server')
assert set(server_role.scopes) == set(orm_server_token.scopes)
# 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(orm_server_token.scopes) == server_role_scopes
assert orm_server_token.user.name == user.name
assert user.api_tokens == [orm_server_token]

View File

@@ -2,6 +2,7 @@
import os
import sys
from contextlib import nullcontext
from pprint import pprint
from subprocess import CalledProcessError, check_output
from unittest import mock
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()
info = r.json()
import pprint
pprint.pprint(info)
pprint(info)
assert info['disable_user_config']
server_config = info['config']
settings = info['settings']
@@ -198,6 +197,79 @@ async def test_disable_user_config(request, app, tmpdir, full_spawn):
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():
out = check_output(
[sys.executable, '-m', 'jupyterhub.singleuser', '--help-all']

View File

@@ -20,7 +20,7 @@ from ..objects import Hub, Server
from ..scopes import access_scopes
from ..spawner import LocalProcessSpawner, Spawner
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 .test_api import add_user
from .utils import async_requests
@@ -336,6 +336,12 @@ async def test_spawner_insert_api_token(app):
assert found
assert found.user.name == user.name
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()
@@ -361,6 +367,58 @@ async def test_spawner_bad_api_token(app):
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):
"""Test deleting spawner.server

View File

@@ -53,3 +53,16 @@ def test_sync_groups(app, user, group_names):
assert user.orm_user in group.users
else:
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

View File

@@ -13,7 +13,7 @@ from tornado import gen, web
from tornado.httputil import urlencode
from tornado.log import app_log
from . import orm
from . import orm, roles, scopes
from ._version import __version__, _check_version
from .crypto import CryptKeeper, EncryptionUnavailable, InvalidToken, decrypt, encrypt
from .metrics import RUNNING_SERVERS, TOTAL_USERS
@@ -588,7 +588,7 @@ class User:
if not server_name:
return self.url
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=''):
"""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)
db.add(orm_server)
note = "Server at %s" % base_url
api_token = self.new_api_token(note=note, roles=['server'])
db.commit()
spawner = self.get_spawner(server_name, replace_failed=True)
spawner.server = server = Server(orm_server=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
# e.g. for processing GET params
spawner.handler = handler
@@ -808,6 +858,7 @@ class User:
spawner.api_token,
generated=False,
note="retrieved from spawner %s" % server_name,
scopes=resolved_scopes,
)
# update OAuth client secret with updated API token
if oauth_provider:

View File

@@ -43,7 +43,7 @@ target_version = [
github_url = "https://github.com/jupyterhub/jupyterhub"
[tool.tbump.version]
current = "4.0.0b2"
current = "4.0.2"
# Example of a semver regexp.
# Make sure this matches current_version before

View File

@@ -7,7 +7,7 @@
asyncio_mode = auto
# jupyter_server plugin is incompatible with notebook imports
addopts = -p no:jupyter_server -m 'not selenium' --color yes --durations 10 --verbose
addopts = -p no:jupyter_server -m 'not browser' --color yes --durations 10 --verbose
python_files = test_*.py
markers =
@@ -17,7 +17,7 @@ markers =
user: mark as a test for a user
slow: mark a test as slow
role: mark as a test for roles
selenium: web tests that run with selenium
browser: web tests that run with playwright
filterwarnings =
ignore:.*The new signature is "def engine_connect\(conn\)"*:sqlalchemy.exc.SADeprecationWarning

View File

@@ -144,7 +144,7 @@ setup_args = dict(
"pytest-asyncio>=0.17",
"pytest-cov",
"requests-mock",
"selenium",
"playwright",
"virtualenv",
],
},

View File

@@ -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;
});

View File

@@ -66,7 +66,9 @@
// common form display
.form-control:focus {
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px @jupyter-orange;
box-shadow:
inset 0 1px 1px rgba(0, 0, 0, 0.075),
0 0 8px @jupyter-orange;
border-color: @jupyter-orange;
outline-color: @jupyter-orange;
}

View File

@@ -17,10 +17,3 @@
</div>
</div>
{% endblock %}
{% block script %}
{{ super() }}
<script type="text/javascript">
require(["admin"]);
</script>
{% endblock %}

View File

@@ -21,7 +21,7 @@
This note will help you keep track of what your tokens are for.
</small>
<br><br>
<label for="token-expiration-seconds">Token expires</label>
<label for="token-expiration-seconds">Token expires in</label>
{% block expiration_options %}
<select id="token-expiration-seconds"
class="form-control">
@@ -33,7 +33,7 @@
</select>
{% endblock expiration_options %}
<small id="note-expires-at" class="form-text text-muted">
You can configure when your token will be expired.
You can configure when your token will expire.
</small>
</div>
</form>
@@ -62,8 +62,8 @@
<div class="row">
<h2>API Tokens</h2>
<p>
These are tokens with full access to the JupyterHub API.
Anything you can do with JupyterHub can be done with these tokens.
These are tokens with access to the JupyterHub API.
Permissions for each token may be viewed via the JupyterHub tokens API.
Revoking the API token for a running server will require restarting that server.
</p>
<table class="table table-striped">
@@ -72,7 +72,7 @@
<th>Note</th>
<th>Last used</th>
<th>Created</th>
<th>Expires at</th>
<th>Expires</th>
</tr>
</thead>
<tbody>