Compare commits

...

113 Commits
5.1.0 ... 5.2.1

Author SHA1 Message Date
Min RK
9749b6eb6a Bump to 5.2.1 2024-10-21 11:35:33 +02:00
Min RK
979b47d1e0 Merge pull request #4935 from consideRatio/pr/cl521
changelog for 5.2.1
2024-10-21 11:35:02 +02:00
Erik Sundell
c12ccafe22 changelog for 5.2.1 2024-10-21 11:15:33 +02:00
Erik Sundell
acc51dbe24 Merge pull request #4934 from minrk/nicer-import-error
informative error on missing dependencies for singleuser server
2024-10-21 11:11:48 +02:00
Min RK
51dcbe4c80 jupyterhub[singleuser]'s not a thing
why did I think it was?
2024-10-21 09:15:43 +02:00
Min RK
6da70e9960 informative error on missing dependencies for singleuser server
- defer jupyter_core import that caused earlier, less informative ImportError
- point to `pip install jupyterhub[singleuser]` in the error
- use `raise from` so original import error is still reported
2024-10-21 09:12:48 +02:00
Min RK
1cb98ce9ff Merge pull request #4932 from manics/subdowmain-doc
Remove out-of-date info from subdomain_hook doc
2024-10-20 09:14:56 +02:00
Min RK
f2ecf6a307 Merge pull request #4930 from consideRatio/pr/startup-service
Abort jupyterhub startup only if managed services fail
2024-10-20 09:14:36 +02:00
Min RK
0a4c3bbfd3 Remove unnecessary exc_info from log
Co-authored-by: Erik Sundell <erik.i.sundell@gmail.com>
2024-10-20 08:40:32 +02:00
Simon Li
e4ae7ce4fe Remove out-of-date info from subdomain_hook doc 2024-10-20 00:11:27 +01:00
Simon Li
ab43f6beb8 Merge pull request #4931 from jupyterhub/dependabot/npm_and_yarn/jsx/multi-9f37c16f8f
Bump cookie and express in /jsx
2024-10-19 17:49:10 +01:00
dependabot[bot]
e8806372c6 Bump cookie and express in /jsx
Bumps [cookie](https://github.com/jshttp/cookie) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `cookie` from 0.6.0 to 0.7.1
- [Release notes](https://github.com/jshttp/cookie/releases)
- [Commits](https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.1)

Updates `express` from 4.21.0 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.0...4.21.1)

---
updated-dependencies:
- dependency-name: cookie
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-19 10:34:25 +00:00
Erik Sundell
6e353df033 Abort jupyterhub startup only if managed services fail 2024-10-18 15:21:11 +02:00
Min RK
06507b426d Merge pull request #4922 from manics/entrypointtypes-help 2024-10-05 17:12:56 +02:00
Simon Li
e282205139 Use set instead of list 2024-10-04 20:23:21 +01:00
Simon Li
e4ff84b7c9 Loaded EntryPointTypes are types, not instances 2024-10-04 16:59:25 +01:00
Simon Li
8c4dbd7a32 Merge pull request #4920 from jupyterhub/dependabot/npm_and_yarn/jsx/jsx-minor-b45f950fe0
Bump the jsx-minor group in /jsx with 9 updates
2024-10-01 22:55:16 +01:00
dependabot[bot]
1336df621b Bump the jsx-minor group in /jsx with 9 updates
Bumps the jsx-minor group in /jsx with 9 updates:

| Package | From | To |
| --- | --- | --- |
| [react-bootstrap](https://github.com/react-bootstrap/react-bootstrap) | `2.10.4` | `2.10.5` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `6.26.1` | `6.26.2` |
| [@testing-library/react](https://github.com/testing-library/react-testing-library) | `16.0.0` | `16.0.1` |
| [babel-loader](https://github.com/babel/babel-loader) | `9.1.3` | `9.2.1` |
| [eslint](https://github.com/eslint/eslint) | `9.9.1` | `9.11.1` |
| [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) | `7.35.0` | `7.37.1` |
| [eslint-plugin-unused-imports](https://github.com/sweepline/eslint-plugin-unused-imports) | `4.1.3` | `4.1.4` |
| [webpack](https://github.com/webpack/webpack) | `5.94.0` | `5.95.0` |
| [webpack-dev-server](https://github.com/webpack/webpack-dev-server) | `5.0.4` | `5.1.0` |


Updates `react-bootstrap` from 2.10.4 to 2.10.5
- [Release notes](https://github.com/react-bootstrap/react-bootstrap/releases)
- [Changelog](https://github.com/react-bootstrap/react-bootstrap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-bootstrap/react-bootstrap/compare/v2.10.4...v2.10.5)

Updates `react-router-dom` from 6.26.1 to 6.26.2
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.26.2/packages/react-router-dom)

Updates `@testing-library/react` from 16.0.0 to 16.0.1
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v16.0.0...v16.0.1)

Updates `babel-loader` from 9.1.3 to 9.2.1
- [Release notes](https://github.com/babel/babel-loader/releases)
- [Changelog](https://github.com/babel/babel-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel-loader/compare/v9.1.3...v9.2.1)

Updates `eslint` from 9.9.1 to 9.11.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.9.1...v9.11.1)

Updates `eslint-plugin-react` from 7.35.0 to 7.37.1
- [Release notes](https://github.com/jsx-eslint/eslint-plugin-react/releases)
- [Changelog](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jsx-eslint/eslint-plugin-react/compare/v7.35.0...v7.37.1)

Updates `eslint-plugin-unused-imports` from 4.1.3 to 4.1.4
- [Commits](https://github.com/sweepline/eslint-plugin-unused-imports/compare/v4.1.3...v4.1.4)

Updates `webpack` from 5.94.0 to 5.95.0
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.94.0...v5.95.0)

Updates `webpack-dev-server` from 5.0.4 to 5.1.0
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v5.0.4...v5.1.0)

---
updated-dependencies:
- dependency-name: react-bootstrap
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: jsx-minor
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: jsx-minor
- dependency-name: "@testing-library/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: jsx-minor
- dependency-name: babel-loader
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: jsx-minor
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: jsx-minor
- dependency-name: eslint-plugin-react
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: jsx-minor
- dependency-name: eslint-plugin-unused-imports
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: jsx-minor
- dependency-name: webpack
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: jsx-minor
- dependency-name: webpack-dev-server
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: jsx-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-01 20:38:06 +00:00
Simon Li
b66931306e Merge pull request #4921 from jupyterhub/dependabot/npm_and_yarn/jsx/eslint-plugin-prettier-5.2.1
Bump eslint-plugin-prettier from 4.2.1 to 5.2.1 in /jsx
2024-10-01 21:36:10 +01:00
dependabot[bot]
83003c7e3d Bump eslint-plugin-prettier from 4.2.1 to 5.2.1 in /jsx
Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 4.2.1 to 5.2.1.
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v4.2.1...v5.2.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-prettier
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-01 17:43:11 +00:00
Simon Li
23b9400c53 Merge pull request #4919 from jupyterhub/dependabot/npm_and_yarn/npm-minor-9181cd8b3d
Bump sass from 1.77.8 to 1.79.4 in the npm-minor group
2024-10-01 18:32:52 +01:00
dependabot[bot]
98e9117633 Bump sass from 1.77.8 to 1.79.4 in the npm-minor group
Bumps the npm-minor group with 1 update: [sass](https://github.com/sass/dart-sass).


Updates `sass` from 1.77.8 to 1.79.4
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.77.8...1.79.4)

---
updated-dependencies:
- dependency-name: sass
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-01 17:12:53 +00:00
Min RK
b2d9f93601 Bump to 5.3.0.dev 2024-10-01 14:13:51 +02:00
Min RK
61c39972da Bump to 5.2.0 2024-10-01 14:13:38 +02:00
Min RK
08f6ff52b0 Merge pull request #4918 from minrk/520
changelog for 5.2.0
2024-10-01 14:13:20 +02:00
Min RK
949496eb36 releasing today! 2024-10-01 14:05:09 +02:00
Min RK
7af4cc2fa9 changelog for 5.2.0 2024-10-01 13:35:32 +02:00
Erik Sundell
3d60ad3956 Merge pull request #4916 from dirtbirb/main
Fix typo in concepts.md
2024-09-26 08:36:50 +02:00
dirtbirb
689a5ba190 Fix typo in concepts.md 2024-09-25 23:14:41 -07:00
Min RK
80b9f02332 Merge pull request #4913 from emmanuel-ferdman/main
Fix incorrect rounding function
2024-09-23 01:15:50 -07:00
Emmanuel Ferdman
8bd1219b92 Fix incorrect rounding function
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2024-09-21 18:57:00 +03:00
Simon Li
4ea74c4869 Merge pull request #4881 from manics/linkcheck-usable-results
Display Sphinx linkcheck output in a more readable format
2024-09-13 16:13:22 +01:00
Min RK
24fb08d513 Merge pull request #4911 from minrk/audit-fix
npm audit fix on devDependencies
2024-09-13 05:43:14 -07:00
Min RK
6b22599149 npm audit fix on devDependencies 2024-09-13 14:10:18 +02:00
Min RK
70ca293977 Merge pull request #4892 from jupyterhub/dependabot/npm_and_yarn/jsx/jsx-react-0625fe9078
Bump the jsx-react group in /jsx with 6 updates
2024-09-13 05:04:52 -07:00
Min RK
aeaffa654f try waiting for networkidle after clicking next
maybe that will cause it to load properly
2024-09-13 13:10:49 +02:00
Min RK
86e4f42035 simplify pagination buttons
- no custom css
- put click events on buttons instead of labels
- use standard disabled state instead of custom cursor, grey text
2024-09-13 13:10:48 +02:00
Min RK
6ccb809a2a consistent use of fakeTimers 2024-09-13 13:10:48 +02:00
Min RK
992bc98ff1 MainContainer.children is a node
not array or object
2024-09-13 13:10:48 +02:00
Min RK
43597febcb update import for React.act 2024-09-13 13:10:48 +02:00
Min RK
6464e3629c use createRoot for react 18 2024-09-13 13:10:48 +02:00
dependabot[bot]
62d2a4bec2 Bump the jsx-react group in /jsx with 6 updates
Bumps the jsx-react group in /jsx with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `17.0.2` | `18.3.1` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `17.0.2` | `18.3.1` |
| [react-icons](https://github.com/react-icons/react-icons) | `4.9.0` | `5.3.0` |
| [react-redux](https://github.com/reduxjs/react-redux) | `7.2.9` | `9.1.2` |
| [redux](https://github.com/reduxjs/redux) | `4.2.1` | `5.0.1` |
| [@testing-library/react](https://github.com/testing-library/react-testing-library) | `12.1.5` | `16.0.0` |


Updates `react` from 17.0.2 to 18.3.1
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v18.3.1/packages/react)

Updates `react-dom` from 17.0.2 to 18.3.1
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v18.3.1/packages/react-dom)

Updates `react-icons` from 4.9.0 to 5.3.0
- [Release notes](https://github.com/react-icons/react-icons/releases)
- [Commits](https://github.com/react-icons/react-icons/compare/v4.9.0...v5.3.0)

Updates `react-redux` from 7.2.9 to 9.1.2
- [Release notes](https://github.com/reduxjs/react-redux/releases)
- [Changelog](https://github.com/reduxjs/react-redux/blob/master/CHANGELOG.md)
- [Commits](https://github.com/reduxjs/react-redux/compare/v7.2.9...v9.1.2)

Updates `redux` from 4.2.1 to 5.0.1
- [Release notes](https://github.com/reduxjs/redux/releases)
- [Changelog](https://github.com/reduxjs/redux/blob/master/CHANGELOG.md)
- [Commits](https://github.com/reduxjs/redux/compare/v4.2.1...v5.0.1)

Updates `@testing-library/react` from 12.1.5 to 16.0.0
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v12.1.5...v16.0.0)

---
updated-dependencies:
- dependency-name: react
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: jsx-react
- dependency-name: react-dom
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: jsx-react
- dependency-name: react-icons
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: jsx-react
- dependency-name: react-redux
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: jsx-react
- dependency-name: redux
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: jsx-react
- dependency-name: "@testing-library/react"
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: jsx-react
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-13 13:10:48 +02:00
Erik Sundell
6e3913456b Merge pull request #4906 from minrk/text_content
browser tests: use text_content instead of inner_text
2024-09-13 12:15:40 +02:00
Min RK
de39fda9a7 browser tests: use text_content instead of inner_text
inner_text takes visibility into account, text_content is just what's there
2024-09-13 11:59:10 +02:00
Min RK
abca5546b7 Merge pull request #4904 from edmorley/update-spawner-env_keep
Add `LD_LIBRARY_PATH` to `LocalProcessSpawner.env_keep`, move most env_keep defaults to LocalProcessSpawner
2024-09-13 02:53:08 -07:00
Ed Morley
1b87e9c668 Use separate env_keep defaults for LocalProcessSpawner
Since none of the current defaults (except `JUPYTERHUB_SINGLEUSER_APP`)
make sense for spawners other than `LocalProcessSpawner`.
2024-09-13 09:30:50 +01:00
Ed Morley
70561c8727 Add LD_LIBRARY_PATH toSpawner.env_keep
For security reasons, only allow-listed env vars in the parent
JupyterHub process are passed to the single-user server Python process.
This allow-list is controlled by `Spawner.env_keep`, which by default
includes common env vars that are (a) both necessary for the single-user
server process to work, (b) don't contain credentials or sensitive
information that shouldn't be revealed to users of the Notebook.

However, this allow-list was missing the `LD_LIBRARY_PATH` env var,
which causes shared library errors when using a relocated Python that
has been compiled in shared mode (`--enable-shared`). This prevents
JupyterHub from working out of the box on platforms like Heroku.

Fixes #4903.
2024-09-12 09:45:03 +01:00
Min RK
b13d3afa0f Merge pull request #4897 from minrk/dark
add dark mode toggle
2024-09-04 01:14:24 -07:00
Min RK
5f6748abd4 Merge pull request #4902 from jupyterhub/pre-commit-ci-update-config 2024-09-03 06:47:31 -07:00
pre-commit-ci[bot]
8b944a3293 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-09-03 13:34:40 +00:00
pre-commit-ci[bot]
5dddd97132 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-09-02 22:21:39 +00:00
pre-commit-ci[bot]
20a600ffa0 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.6 → v0.6.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.6...v0.6.3)
- [github.com/djlint/djLint: v1.34.1 → v1.35.2](https://github.com/djlint/djLint/compare/v1.34.1...v1.35.2)
2024-09-02 22:21:17 +00:00
Simon Li
de2841e00d Merge pull request #4898 from jupyterhub/dependabot/npm_and_yarn/jsx/eslint-9.9.1
Bump eslint from 8.57.0 to 9.9.1 in /jsx
2024-09-01 22:21:37 +01:00
Simon Li
33af239911 Merge pull request #4899 from jupyterhub/dependabot/npm_and_yarn/jsx/prettier-3.3.3
Bump prettier from 2.8.8 to 3.3.3 in /jsx
2024-09-01 22:19:44 +01:00
dependabot[bot]
2aeb49690b Bump prettier from 2.8.8 to 3.3.3 in /jsx
Bumps [prettier](https://github.com/prettier/prettier) from 2.8.8 to 3.3.3.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.8.8...3.3.3)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-01 17:05:45 +00:00
dependabot[bot]
265fcbc874 Bump eslint from 8.57.0 to 9.9.1 in /jsx
Bumps [eslint](https://github.com/eslint/eslint) from 8.57.0 to 9.9.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.57.0...v9.9.1)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-01 17:05:36 +00:00
Min RK
98a6338247 Merge pull request #4896 from rinvii/add/university-of-portland
add University of Portland to sample orgs
2024-08-28 05:37:20 -07:00
Min RK
d519bacd8a add dark mode toggle button
- toggle in upper right
- stored in localStorage
- adds btn-contrast, btn-outline-contrast for light-on-dark / dark-on-light
2024-08-28 11:08:59 +02:00
rinvii
ad39fe3823 add University of Portland to sample orgs 2024-08-27 17:42:29 -07:00
Min RK
aca10da71d Merge pull request #4895 from jupyterhub/dependabot/npm_and_yarn/jsx/testing-library/user-event-14.5.2
Bump @testing-library/user-event from 13.5.0 to 14.5.2 in /jsx
2024-08-27 05:55:08 -07:00
Min RK
e8b2bd82c8 userEvent is async now 2024-08-27 14:52:02 +02:00
Min RK
5616ade51d audit fix 2024-08-27 14:23:10 +02:00
Min RK
b83f6d178b Merge pull request #4894 from jupyterhub/dependabot/npm_and_yarn/jsx/eslint-plugin-unused-imports-4.1.3
Bump eslint-plugin-unused-imports from 2.0.0 to 4.1.3 in /jsx
2024-08-27 03:56:20 -07:00
Min RK
3068e3911b Merge pull request #4893 from jupyterhub/dependabot/npm_and_yarn/jsx/jsx-webpack-a795b026f0
Bump the jsx-webpack group in /jsx with 3 updates
2024-08-27 03:55:20 -07:00
dependabot[bot]
6867f3b141 Bump the jsx-webpack group in /jsx with 3 updates
Bumps the jsx-webpack group in /jsx with 3 updates: [css-loader](https://github.com/webpack-contrib/css-loader), [style-loader](https://github.com/webpack-contrib/style-loader) and [webpack-dev-server](https://github.com/webpack/webpack-dev-server).


Updates `css-loader` from 6.8.1 to 7.1.2
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v6.8.1...v7.1.2)

Updates `style-loader` from 3.3.3 to 4.0.0
- [Release notes](https://github.com/webpack-contrib/style-loader/releases)
- [Changelog](https://github.com/webpack-contrib/style-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/style-loader/compare/v3.3.3...v4.0.0)

Updates `webpack-dev-server` from 4.15.1 to 5.0.4
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.15.1...v5.0.4)

---
updated-dependencies:
- dependency-name: css-loader
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: jsx-webpack
- dependency-name: style-loader
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: jsx-webpack
- dependency-name: webpack-dev-server
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: jsx-webpack
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-27 10:54:25 +00:00
dependabot[bot]
aec601dbff Bump @testing-library/user-event from 13.5.0 to 14.5.2 in /jsx
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 13.5.0 to 14.5.2.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v13.5.0...v14.5.2)

---
updated-dependencies:
- dependency-name: "@testing-library/user-event"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-27 10:54:14 +00:00
dependabot[bot]
748b6c98d5 Bump eslint-plugin-unused-imports from 2.0.0 to 4.1.3 in /jsx
Bumps [eslint-plugin-unused-imports](https://github.com/sweepline/eslint-plugin-unused-imports) from 2.0.0 to 4.1.3.
- [Commits](https://github.com/sweepline/eslint-plugin-unused-imports/commits/v4.1.3)

---
updated-dependencies:
- dependency-name: eslint-plugin-unused-imports
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-27 10:54:12 +00:00
Min RK
d6d03e8e38 Merge pull request #4891 from jupyterhub/dependabot/npm_and_yarn/jsx/jsx-minor-7cb2cc7612
Bump the jsx-minor group in /jsx with 7 updates
2024-08-27 03:53:01 -07:00
dependabot[bot]
14d32c5bae Bump the jsx-minor group in /jsx with 7 updates
Bumps the jsx-minor group in /jsx with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [react-bootstrap](https://github.com/react-bootstrap/react-bootstrap) | `2.10.1` | `2.10.4` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `6.22.2` | `6.26.1` |
| [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) | `7.22.5` | `7.25.4` |
| [@babel/preset-react](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react) | `7.22.5` | `7.24.7` |
| [babel-loader](https://github.com/babel/babel-loader) | `9.1.2` | `9.1.3` |
| [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) | `7.32.2` | `7.35.0` |
| [webpack](https://github.com/webpack/webpack) | `5.87.0` | `5.94.0` |


Updates `react-bootstrap` from 2.10.1 to 2.10.4
- [Release notes](https://github.com/react-bootstrap/react-bootstrap/releases)
- [Changelog](https://github.com/react-bootstrap/react-bootstrap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-bootstrap/react-bootstrap/compare/v2.10.1...v2.10.4)

Updates `react-router-dom` from 6.22.2 to 6.26.1
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.26.1/packages/react-router-dom)

Updates `@babel/preset-env` from 7.22.5 to 7.25.4
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.25.4/packages/babel-preset-env)

Updates `@babel/preset-react` from 7.22.5 to 7.24.7
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.24.7/packages/babel-preset-react)

Updates `babel-loader` from 9.1.2 to 9.1.3
- [Release notes](https://github.com/babel/babel-loader/releases)
- [Changelog](https://github.com/babel/babel-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel-loader/compare/v9.1.2...v9.1.3)

Updates `eslint-plugin-react` from 7.32.2 to 7.35.0
- [Release notes](https://github.com/jsx-eslint/eslint-plugin-react/releases)
- [Changelog](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jsx-eslint/eslint-plugin-react/compare/v7.32.2...v7.35.0)

Updates `webpack` from 5.87.0 to 5.94.0
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.87.0...v5.94.0)

---
updated-dependencies:
- dependency-name: react-bootstrap
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: jsx-minor
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: jsx-minor
- dependency-name: "@babel/preset-env"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: jsx-minor
- dependency-name: "@babel/preset-react"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: jsx-minor
- dependency-name: babel-loader
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: jsx-minor
- dependency-name: eslint-plugin-react
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: jsx-minor
- dependency-name: webpack
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: jsx-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-27 10:24:49 +00:00
Min RK
653922605a Merge pull request #4890 from jupyterhub/dependabot/npm_and_yarn/jsx/jsx-minor-478ff049ae
Bump the jsx-minor group across 1 directory with 5 updates
2024-08-27 03:21:34 -07:00
dependabot[bot]
52f5aacce1 Bump the jsx-minor group across 1 directory with 5 updates
Bumps the jsx-minor group with 5 updates in the /jsx directory:

| Package | From | To |
| --- | --- | --- |
| [bootstrap](https://github.com/twbs/bootstrap) | `5.3.0` | `5.3.3` |
| [regenerator-runtime](https://github.com/facebook/regenerator) | `0.13.11` | `0.14.1` |
| [babel-jest](https://github.com/jestjs/jest/tree/HEAD/packages/babel-jest) | `29.5.0` | `29.7.0` |
| [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) | `29.5.0` | `29.7.0` |
| [jest-environment-jsdom](https://github.com/jestjs/jest/tree/HEAD/packages/jest-environment-jsdom) | `29.5.0` | `29.7.0` |



Updates `bootstrap` from 5.3.0 to 5.3.3
- [Release notes](https://github.com/twbs/bootstrap/releases)
- [Commits](https://github.com/twbs/bootstrap/compare/v5.3.0...v5.3.3)

Updates `regenerator-runtime` from 0.13.11 to 0.14.1
- [Release notes](https://github.com/facebook/regenerator/releases)
- [Commits](https://github.com/facebook/regenerator/compare/regenerator-runtime@0.13.11...regenerator-runtime@0.14.1)

Updates `babel-jest` from 29.5.0 to 29.7.0
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v29.7.0/packages/babel-jest)

Updates `jest` from 29.5.0 to 29.7.0
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v29.7.0/packages/jest)

Updates `jest-environment-jsdom` from 29.5.0 to 29.7.0
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v29.7.0/packages/jest-environment-jsdom)

---
updated-dependencies:
- dependency-name: bootstrap
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: jsx-minor
- dependency-name: regenerator-runtime
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: jsx-minor
- dependency-name: babel-jest
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: jsx-minor
- dependency-name: jest
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: jsx-minor
- dependency-name: jest-environment-jsdom
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: jsx-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-27 10:01:33 +00:00
Simon Li
e00ef75f15 Merge pull request #4888 from jupyterhub/dependabot/npm_and_yarn/jsx/testing-library/jest-dom-6.5.0
Bump @testing-library/jest-dom from 5.16.5 to 6.5.0 in /jsx
2024-08-27 11:00:52 +01:00
Simon Li
50879db41c Merge pull request #4889 from jupyterhub/dependabot/npm_and_yarn/jsx/eslint-8.57.0
Bump eslint from 8.43.0 to 8.57.0 in /jsx
2024-08-27 10:59:47 +01:00
dependabot[bot]
8c4a170f4e Bump eslint from 8.43.0 to 8.57.0 in /jsx
Bumps [eslint](https://github.com/eslint/eslint) from 8.43.0 to 8.57.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.43.0...v8.57.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-27 09:01:05 +00:00
dependabot[bot]
f36e5420f5 Bump @testing-library/jest-dom from 5.16.5 to 6.5.0 in /jsx
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.5 to 6.5.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.16.5...v6.5.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-27 09:00:56 +00:00
Min RK
27d83dd6c2 Merge pull request #4884 from minrk/main
update-types typo in dependabot.yml
2024-08-27 01:57:17 -07:00
Min RK
aa43ce85bd update-types typo in dependabot.yml 2024-08-27 10:56:36 +02:00
Min RK
53205764ca Merge pull request #4882 from minrk/dependabot-group-minor
only group minor dependencies in dependabot
2024-08-27 01:38:59 -07:00
Simon Li
a7fc94c22a Merge pull request #4883 from minrk/sphinx-links
docs: remove some outdated links
2024-08-27 09:23:19 +01:00
Min RK
9419c7f2c0 Merge pull request #4872 from jupyterhub/dependabot/npm_and_yarn/npm-d8a6477732
Bump the npm group with 4 updates
2024-08-27 00:50:14 -07:00
Min RK
73e0d7092e docs: remove some outdated links
- CURC no longer uses JupyterHub
- Remove outdated links to spark/yarn docs
2024-08-27 09:41:37 +02:00
Min RK
562f86026d only group minor dependencies in dependabot 2024-08-27 09:38:04 +02:00
Min RK
3a64eb85a8 Merge pull request #4880 from manics/faq-websockets
FAQ: websocket problems
2024-08-27 00:13:35 -07:00
Simon Li
e4340a467c Try manics/action-sphinx-linkcheck-summary@main
https://github.com/manics/action-sphinx-linkcheck-summary
2024-08-27 00:12:11 +01:00
Simon Li
f8c00092d2 FAQ: websocket problems 2024-08-26 23:51:27 +01:00
Olivier Benz
bd00f376d7 Introduce dark theme 2024-08-26 22:00:59 +02:00
Simon Li
99b32dd372 Merge pull request #4876 from minrk/mapp
fix python3 -m jupyterhub.app
2024-08-25 15:28:49 +01:00
Min RK
7a94830a29 fix python3 -m jupyterhub.app
python3 -m jupyterhub.app means _both_ jupyterhub.app and __main__ modules are defined
and are not the same, so instance/isinstance checks don't work
2024-08-23 08:30:39 +02:00
dependabot[bot]
eeb867947a Bump the npm group with 4 updates
Bumps the npm group with 4 updates: [@fortawesome/fontawesome-free](https://github.com/FortAwesome/Font-Awesome), [jquery](https://github.com/jquery/jquery), [moment](https://github.com/moment/moment) and [sass](https://github.com/sass/dart-sass).


Updates `@fortawesome/fontawesome-free` from 6.5.2 to 6.6.0
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/6.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/6.5.2...6.6.0)

Updates `jquery` from 3.7.0 to 3.7.1
- [Release notes](https://github.com/jquery/jquery/releases)
- [Changelog](https://github.com/jquery/jquery/blob/main/changelog.md)
- [Commits](https://github.com/jquery/jquery/compare/3.7.0...3.7.1)

Updates `moment` from 2.29.4 to 2.30.1
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.4...2.30.1)

Updates `sass` from 1.74.1 to 1.77.8
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.74.1...1.77.8)

---
updated-dependencies:
- dependency-name: "@fortawesome/fontawesome-free"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: jquery
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: moment
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: sass
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-21 14:34:06 +00:00
Simon Li
ccac4aa53f Merge pull request #4869 from minrk/dependabot-top
add dependabot config for npm
2024-08-21 15:33:20 +01:00
Simon Li
38c313eef7 Merge pull request #4865 from minrk/blocked_users
Revoke all permissions from Authenticator.blocked_users
2024-08-21 15:23:10 +01:00
Simon Li
251aa1f12c Merge pull request #4870 from minrk/metrics-start
start metrics collector in start
2024-08-21 14:54:37 +01:00
Min RK
b6b596cd34 start metrics collector in start
instead of initialize, which should only create objects

improves symmetry with stop, should remove some warnings about unfinished coroutines in some tests
2024-08-21 13:40:39 +02:00
Min RK
2391d0f764 fix app cleanup in test_app 2024-08-21 13:19:43 +02:00
Min RK
959cd5a6e1 add dependabot config for npm 2024-08-21 12:59:53 +02:00
Min RK
036dcb644c Merge pull request #4868 from minrk/bump-rjs
bump require.js
2024-08-20 08:29:21 +02:00
Min RK
bdc7ee40f4 bump require.js 2024-08-19 09:20:05 +02:00
Min RK
5383a60d4a test: make sure we don't lose users across temp hubs 2024-08-15 12:55:38 +02:00
Min RK
78649b9118 Merge pull request #4867 from manics/admin-pages-baseurl
Admin pages: use inherited base_url from render_template
2024-08-14 07:41:48 +02:00
Simon Li
e63ec9aedc Admin pages: use inherited base_url from render_template 2024-08-13 16:33:16 +01:00
Min RK
6be699c333 Revoke all permissions from Authenticator.blocked_users
rather than only disabling login, fully block the user from Hub operations
by removing all group membership and role assignments
2024-08-12 15:01:32 +02:00
Min RK
a377f8bc7f Merge pull request #4664 from minrk/fix-pytest-asyncio
unpin pytest-asyncio
2024-08-12 10:57:40 +02:00
Min RK
7ba36ef760 Merge pull request #4864 from oliver-sanders/singleuser-mixin-fix-shutdown
singleuser: fix shutdown mixin
2024-08-08 14:06:11 +02:00
Oliver Sanders
6f13355446 singleuser: fix shutdown mixin:
* The singleuser mixin is attempting to bypass jupyter_server's
  interactive prompt on shutdown by stopping the IO loop.
* This does disable the interactive prompt, but also causes SIGINT
  to be ignored causing SIGTERM to be issued after the timeout is hit.
* Closing the IO loop also prevents the server from closing async resources.
* This change allows jupyter_server to run its cleanup logic as
  intended.
2024-08-08 10:03:07 +01:00
Min RK
a5f08035a2 Merge pull request #4863 from Will-Shanks/main
Add PAMAuthenticator.executor_threads configurable, increase default to 4
2024-08-08 08:35:34 +02:00
Will Shanks
3d0256a757 PAMAuthenticator: set executor thread default to 4 2024-08-07 11:02:49 -07:00
Min RK
cca7cc6e92 test with prerelease dependencies 2024-08-07 13:19:47 +02:00
Min RK
3ab54e6eeb test compatibility with pytest-asyncio 0.24
- remove reference to event_loop fixture on 0.24
- add asyncio_default_fixture_loop_scope = module config
2024-08-07 13:18:56 +02:00
Min RK
ce7e532ab6 Revert "temporarily pin pytest-asyncio"
This reverts commit 8f2ad59254.
2024-08-07 12:59:40 +02:00
Will Shanks
da79a89f22 PAMAuthenticator: make executor threads configurable 2024-08-06 15:06:18 -07:00
Min RK
d75bcc03c0 Merge pull request #4862 from jupyterhub/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-08-06 08:58:47 +02:00
pre-commit-ci[bot]
a03fd54982 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.0 → v0.5.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.0...v0.5.6)
2024-08-05 23:06:43 +00:00
Min RK
f4fa229645 Bump to 5.2.0.dev 2024-07-31 11:24:12 +02:00
43 changed files with 3862 additions and 2529 deletions

View File

@@ -14,3 +14,44 @@ updates:
interval: monthly
time: "05:00"
timezone: Etc/UTC
- package-ecosystem: npm
directory: /
groups:
# one big pull request for minor bumps
npm-minor:
patterns:
- "*"
update-types:
- minor
- patch
schedule:
interval: monthly
- package-ecosystem: npm
directory: /jsx
groups:
# one big pull request for minor bumps
jsx-minor:
patterns:
- "*"
update-types:
- minor
- patch
# group major bumps of react-related dependencies
jsx-react:
patterns:
- "react*"
- "redux*"
- "*react"
- "recompose"
update-types:
- major
# group major bumps of webpack-related dependencies
jsx-webpack:
patterns:
- "webpack*"
- "@babel/*"
- "*-loader"
update-types:
- major
schedule:
interval: monthly

View File

@@ -81,10 +81,12 @@ jobs:
cd docs
make html
# Output broken and permanently redirected links in a readable format
- name: check links
run: |
cd docs
make linkcheck
uses: manics/action-sphinx-linkcheck-summary@main
with:
docs-dir: docs
build-dir: docs/_build
# make rediraffecheckdiff compares files for different changesets
# these diff targets aren't always available

View File

@@ -173,7 +173,7 @@ jobs:
# make sure our `>=` pins really do express our minimum supported versions
pip install -r ci/oldest-dependencies/requirements.old -e .
else
pip install -e ".[test]"
pip install --pre -e ".[test]"
fi
if [ "${{ matrix.main_dependencies }}" != "" ]; then

View File

@@ -16,7 +16,7 @@ ci:
repos:
# autoformat and lint Python code
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
rev: v0.6.3
hooks:
- id: ruff
types_or:
@@ -37,7 +37,7 @@ repos:
# autoformat HTML templates
- repo: https://github.com/djlint/djLint
rev: v1.34.1
rev: v1.35.2
hooks:
- id: djlint-reformat-jinja
files: ".*templates/.*.html"

View File

@@ -7,7 +7,7 @@ info:
license:
name: BSD-3-Clause
identifier: BSD-3-Clause
version: 5.1.0
version: 5.2.1
servers:
- url: /hub/api
security:

View File

@@ -3,7 +3,7 @@
# JupyterHub: A conceptual overview
```{warning}
This page could is missing cross-links to other parts of
This page could be missing cross-links to other parts of
the documentation. You can help by adding them!
```

View File

@@ -66,7 +66,7 @@ industry, and government research labs. It is most-commonly used by two kinds of
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
University of Oslo, University of Sheffield, Université Paris Sud, University of Versailles, University of Portland
- **Research laboratories**: NASA, NCAR, NOAA, the Large Synoptic Survey Telescope, Brookhaven National Lab,
Minnesota Supercomputing Institute, ALCF, CERN, Lawrence Livermore National Laboratory, HUNT
- **Online communities**: Pangeo, Quantopian, mybinder.org, MathHub, Open Humans

View File

@@ -198,6 +198,23 @@ With a docker container, pass in the environment variable with the run command:
[This example](https://github.com/jupyterhub/jupyterhub/tree/HEAD/examples/service-notebook/external) demonstrates how to combine the use of the `jupyterhub-singleuser` environment variables when launching a Notebook as an externally managed service.
### Jupyter Notebook/Lab can be launched, but notebooks seem to hang when trying to execute a cell
This often occurs when your browser is unable to open a websocket connection to a Jupyter kernel.
#### Diagnose
Open your browser console, e.g. [Chrome](https://developer.chrome.com/docs/devtools/console), [Firefox](https://firefox-source-docs.mozilla.org/devtools-user/web_console/).
If you see errors related to opening websockets this is likely to be the problem.
#### Solutions
This could be caused by anything related to the network between your computer/browser and the server running JupyterHub, such as:
- reverse proxies (see {ref}`howto:config:reverse-proxy` for example configurations)
- anti-virus or firewalls running on your computer or JupyterHub server
- transparent proxies running on your network
## How do I...?
### Use a chained SSL certificate
@@ -259,17 +276,6 @@ the entire filesystem and set the default to the user's home directory.
c.Spawner.notebook_dir = '/'
c.Spawner.default_url = '/home/%U' # %U will be replaced with the username
### How do I increase the number of pySpark executors on YARN?
From the command line, pySpark executors can be configured using a command
similar to this one:
pyspark --total-executor-cores 2 --executor-memory 1G
[Cloudera documentation for configuring spark on YARN applications](https://www.cloudera.com/documentation/enterprise/latest/topics/cdh_ig_running_spark_on_yarn.html#spark_on_yarn_config_apps)
provides additional information. The [pySpark configuration documentation](https://spark.apache.org/docs/0.9.0/configuration.html)
is also helpful for programmatic configuration examples.
### How do I use JupyterLab's pre-release version with JupyterHub?
While JupyterLab is still under active development, we have had users

View File

@@ -20,6 +20,82 @@ Contributors to major version bumps in JupyterHub include:
## [Unreleased]
## 5.2
### 5.2.1 - 2024-10-21
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/5.2.0...5.2.1))
#### Enhancements made
- informative error on missing dependencies for singleuser server [#4934](https://github.com/jupyterhub/jupyterhub/pull/4934) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
#### Bugs fixed
- Abort jupyterhub startup only if managed services fail [#4930](https://github.com/jupyterhub/jupyterhub/pull/4930) ([@consideRatio](https://github.com/consideRatio), [@minrk](https://github.com/minrk))
- Loaded EntryPointTypes are types, not instances [#4922](https://github.com/jupyterhub/jupyterhub/pull/4922) ([@manics](https://github.com/manics), [@minrk](https://github.com/minrk))
#### Documentation improvements
- Remove out-of-date info from subdomain_hook doc [#4932](https://github.com/jupyterhub/jupyterhub/pull/4932) ([@manics](https://github.com/manics), [@minrk](https://github.com/minrk))
#### 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=2024-10-01&to=2024-10-21&type=c))
@consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2024-10-01..2024-10-21&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2024-10-01..2024-10-21&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2024-10-01..2024-10-21&type=Issues))
### 5.2.0 - 2024-10-01
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/5.1.0...5.2.0))
#### New features added
- add dark mode toggle [#4897](https://github.com/jupyterhub/jupyterhub/pull/4897) ([@minrk](https://github.com/minrk), [@benz0li](https://github.com/benz0li))
- Revoke all permissions from Authenticator.blocked_users [#4865](https://github.com/jupyterhub/jupyterhub/pull/4865) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
#### Enhancements made
- Add `LD_LIBRARY_PATH` to `LocalProcessSpawner.env_keep`, move most env_keep defaults to LocalProcessSpawner [#4904](https://github.com/jupyterhub/jupyterhub/pull/4904) ([@edmorley](https://github.com/edmorley), [@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
- Add PAMAuthenticator.executor_threads configurable, increase default to 4 [#4863](https://github.com/jupyterhub/jupyterhub/pull/4863) ([@Will-Shanks](https://github.com/Will-Shanks), [@minrk](https://github.com/minrk))
#### Bugs fixed
- Fix incorrect rounding function with large spawn_throttle_retry_range upper bound [#4913](https://github.com/jupyterhub/jupyterhub/pull/4913) ([@emmanuel-ferdman](https://github.com/emmanuel-ferdman), [@minrk](https://github.com/minrk))
- fix python3 -m jupyterhub.app [#4876](https://github.com/jupyterhub/jupyterhub/pull/4876) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
- Admin pages: use inherited base_url from render_template [#4867](https://github.com/jupyterhub/jupyterhub/pull/4867) ([@manics](https://github.com/manics), [@minrk](https://github.com/minrk))
- singleuser: fix shutdown mixin [#4864](https://github.com/jupyterhub/jupyterhub/pull/4864) ([@oliver-sanders](https://github.com/oliver-sanders), [@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
#### Maintenance and upkeep improvements
- browser tests: use text_content instead of inner_text [#4906](https://github.com/jupyterhub/jupyterhub/pull/4906) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
- update-types typo in dependabot.yml [#4884](https://github.com/jupyterhub/jupyterhub/pull/4884) ([@minrk](https://github.com/minrk))
- only group minor dependencies in dependabot [#4882](https://github.com/jupyterhub/jupyterhub/pull/4882) ([@minrk](https://github.com/minrk))
- start metrics collector in start [#4870](https://github.com/jupyterhub/jupyterhub/pull/4870) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
- bump require.js [#4868](https://github.com/jupyterhub/jupyterhub/pull/4868) ([@minrk](https://github.com/minrk))
- unpin pytest-asyncio [#4664](https://github.com/jupyterhub/jupyterhub/pull/4664) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics), [@seifertm](https://github.com/seifertm))
- Display Sphinx linkcheck output in a more readable format [#4881](https://github.com/jupyterhub/jupyterhub/pull/4881) ([@manics](https://github.com/manics), [@minrk](https://github.com/minrk))
- add dependabot config for npm [#4869](https://github.com/jupyterhub/jupyterhub/pull/4869) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
#### Documentation improvements
- Fix typo in concepts.md [#4916](https://github.com/jupyterhub/jupyterhub/pull/4916) ([@dirtbirb](https://github.com/dirtbirb), [@consideRatio](https://github.com/consideRatio))
- add University of Portland to sample orgs [#4896](https://github.com/jupyterhub/jupyterhub/pull/4896) ([@rinvii](https://github.com/rinvii), [@minrk](https://github.com/minrk))
- docs: remove some outdated links [#4883](https://github.com/jupyterhub/jupyterhub/pull/4883) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
- FAQ: websocket problems [#4880](https://github.com/jupyterhub/jupyterhub/pull/4880) ([@manics](https://github.com/manics), [@minrk](https://github.com/minrk))
#### 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=2024-07-31&to=2024-10-01&type=c))
@benz0li ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Abenz0li+updated%3A2024-07-31..2024-10-01&type=Issues)) | @consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2024-07-31..2024-10-01&type=Issues)) | @dirtbirb ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Adirtbirb+updated%3A2024-07-31..2024-10-01&type=Issues)) | @edmorley ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aedmorley+updated%3A2024-07-31..2024-10-01&type=Issues)) | @emmanuel-ferdman ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aemmanuel-ferdman+updated%3A2024-07-31..2024-10-01&type=Issues)) | @jelmd ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ajelmd+updated%3A2024-07-31..2024-10-01&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2024-07-31..2024-10-01&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2024-07-31..2024-10-01&type=Issues)) | @oliver-sanders ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aoliver-sanders+updated%3A2024-07-31..2024-10-01&type=Issues)) | @rinvii ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Arinvii+updated%3A2024-07-31..2024-10-01&type=Issues)) | @seifertm ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aseifertm+updated%3A2024-07-31..2024-10-01&type=Issues)) | @Will-Shanks ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AWill-Shanks+updated%3A2024-07-31..2024-10-01&type=Issues))
## 5.1
### 5.1.0 - 2024-07-31

View File

@@ -82,15 +82,6 @@ Within CERN, there are two noteworthy JupyterHub deployments in operation:
- Advanced Computing
- [Palmetto cluster and JupyterHub](https://citi.sites.clemson.edu/2016/08/18/JupyterHub-for-Palmetto-Cluster.html)
### University of Colorado Boulder
- (CU Research Computing) CURC
- [JupyterHub User Guide](https://curc.readthedocs.io/en/latest/gateways/jupyterhub.html)
- Slurm job dispatched on Crestone compute cluster
- log troubleshooting
- Profiles in IPython Clusters tab
### ETH Zurich
[ETH Zurich](https://ethz.ch/en.html), (Federal Institute of Technology Zurich), is a public research university in Zürich, Switzerland, with focus on science, technology, engineering, and mathematics, although its 16 departments span a variety of disciplines and subjects.

5233
jsx/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,9 @@
"plugins": []
},
"jest": {
"fakeTimers": {
"enableGlobally": true
},
"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"
@@ -29,44 +32,44 @@
"testEnvironment": "jsdom"
},
"dependencies": {
"bootstrap": "^5.2.3",
"bootstrap": "^5.3.3",
"history": "^5.3.0",
"lodash": "^4.17.21",
"prop-types": "^15.8.1",
"react": "^17.0.2",
"react-bootstrap": "^2.10.1",
"react-dom": "^17.0.2",
"react-icons": "^4.8.0",
"react": "^18.3.1",
"react-bootstrap": "^2.10.5",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-multi-select-component": "^4.3.4",
"react-redux": "^7.2.8",
"react-router-dom": "^6.22.2",
"react-redux": "^9.1.2",
"react-router-dom": "^6.26.2",
"recompose": "npm:react-recompose@^0.33.0",
"redux": "^4.2.1",
"regenerator-runtime": "^0.13.11"
"redux": "^5.0.1",
"regenerator-runtime": "^0.14.1"
},
"devDependencies": {
"@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",
"@babel/preset-env": "^7.25.4",
"@babel/preset-react": "^7.24.7",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@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",
"babel-jest": "^29.7.0",
"babel-loader": "^9.2.1",
"css-loader": "^7.1.2",
"eslint": "^9.11.1",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.1",
"eslint-plugin-unused-imports": "^4.1.4",
"file-loader": "^6.2.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"prettier": "^2.8.7",
"style-loader": "^3.3.2",
"webpack": "^5.79.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"prettier": "^3.3.3",
"style-loader": "^4.0.0",
"webpack": "^5.95.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.13.3"
"webpack-dev-server": "^5.1.0"
}
}

View File

@@ -1,5 +1,5 @@
import React from "react";
import ReactDOM from "react-dom";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import { createStore } from "redux";
import { compose } from "recompose";
@@ -40,4 +40,5 @@ const App = () => {
);
};
ReactDOM.render(<App />, document.getElementById("react-admin-hook"));
const root = createRoot(document.getElementById("react-admin-hook"));
root.render(<App />);

View File

@@ -1,6 +1,5 @@
import React from "react";
import React, { act } from "react";
import "@testing-library/jest-dom";
import { act } from "react-dom/test-utils";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Provider, useDispatch, useSelector } from "react-redux";
@@ -46,6 +45,7 @@ beforeEach(() => {
afterEach(() => {
useDispatch.mockClear();
jest.runAllTimers();
});
test("Renders", async () => {
@@ -67,7 +67,7 @@ test("Removes users when they fail Regex", async () => {
fireEvent.blur(textarea, { target: { value: "foo \n bar\na@b.co\n \n\n" } });
await act(async () => {
fireEvent.click(submit);
await fireEvent.click(submit);
});
expect(callbackSpy).toHaveBeenCalledWith(["foo", "bar", "a@b.co"], false);
@@ -79,15 +79,15 @@ test("Correctly submits admin", async () => {
await act(async () => {
render(addUserJsx(callbackSpy));
});
let textarea = screen.getByTestId("user-textarea");
let submit = screen.getByTestId("submit");
let check = screen.getByTestId("check");
userEvent.click(check);
fireEvent.blur(textarea, { target: { value: "foo" } });
await fireEvent.blur(textarea, { target: { value: "foo" } });
await fireEvent.click(check);
await fireEvent.click(submit);
await act(async () => {
fireEvent.click(submit);
await jest.runAllTimers();
});
expect(callbackSpy).toHaveBeenCalledWith(["foo"], true);
@@ -103,7 +103,7 @@ test("Shows a UI error dialogue when user creation fails", async () => {
let submit = screen.getByTestId("submit");
await act(async () => {
fireEvent.click(submit);
await fireEvent.click(submit);
});
let errorDialog = screen.getByText("Failed to create user.");
@@ -122,7 +122,7 @@ test("Shows a more specific UI error dialogue when user creation returns an impr
let submit = screen.getByTestId("submit");
await act(async () => {
fireEvent.click(submit);
await fireEvent.click(submit);
});
let errorDialog = screen.getByText(

View File

@@ -1,6 +1,5 @@
import React from "react";
import React, { act } from "react";
import "@testing-library/jest-dom";
import { act } from "react-dom/test-utils";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Provider, useDispatch, useSelector } from "react-redux";
@@ -45,6 +44,7 @@ beforeEach(() => {
afterEach(() => {
useDispatch.mockClear();
jest.runAllTimers();
});
test("Renders", async () => {
@@ -63,9 +63,10 @@ test("Calls createGroup on submit", async () => {
let input = screen.getByTestId("group-input");
let submit = screen.getByTestId("submit");
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
userEvent.type(input, "groupname");
await act(async () => fireEvent.click(submit));
await user.type(input, "groupname");
await act(async () => await fireEvent.click(submit));
expect(callbackSpy).toHaveBeenNthCalledWith(1, "groupname");
});
@@ -80,7 +81,7 @@ test("Shows a UI error dialogue when group creation fails", async () => {
let submit = screen.getByTestId("submit");
await act(async () => {
fireEvent.click(submit);
await fireEvent.click(submit);
});
let errorDialog = screen.getByText("Failed to create group.");
@@ -99,7 +100,7 @@ test("Shows a more specific UI error dialogue when user creation returns an impr
let submit = screen.getByTestId("submit");
await act(async () => {
fireEvent.click(submit);
await fireEvent.click(submit);
});
let errorDialog = screen.getByText(

View File

@@ -1,6 +1,5 @@
import React from "react";
import React, { act } from "react";
import "@testing-library/jest-dom";
import { act } from "react-dom/test-utils";
import { render, screen, fireEvent } from "@testing-library/react";
import { Provider, useDispatch, useSelector } from "react-redux";
import { createStore } from "redux";
@@ -58,6 +57,7 @@ beforeEach(() => {
afterEach(() => {
useDispatch.mockClear();
jest.runAllTimers();
});
test("Renders", async () => {
@@ -80,7 +80,7 @@ test("Calls the delete user function when the button is pressed", async () => {
let deleteUser = screen.getByTestId("delete-user");
await act(async () => {
fireEvent.click(deleteUser);
await fireEvent.click(deleteUser);
});
expect(callbackSpy).toHaveBeenCalled();
@@ -95,7 +95,7 @@ test("Submits the edits when the button is pressed", async () => {
let submit = screen.getByTestId("submit");
await act(async () => {
fireEvent.click(submit);
await fireEvent.click(submit);
});
expect(callbackSpy).toHaveBeenCalled();
@@ -113,7 +113,7 @@ test("Shows a UI error dialogue when user edit fails", async () => {
fireEvent.blur(usernameInput, { target: { value: "whatever" } });
await act(async () => {
fireEvent.click(submit);
await fireEvent.click(submit);
});
let errorDialog = screen.getByText("Failed to edit user.");
@@ -134,7 +134,7 @@ test("Shows a UI error dialogue when user edit returns an improper status code",
fireEvent.blur(usernameInput, { target: { value: "whatever" } });
await act(async () => {
fireEvent.click(submit);
await fireEvent.click(submit);
});
let errorDialog = screen.getByText("Failed to edit user.");

View File

@@ -1,6 +1,5 @@
import React from "react";
import React, { act } from "react";
import "@testing-library/jest-dom";
import { act } from "react-dom/test-utils";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Provider, useSelector } from "react-redux";
@@ -58,6 +57,7 @@ beforeEach(() => {
afterEach(() => {
useSelector.mockClear();
jest.runAllTimers();
});
test("Renders", async () => {
@@ -80,13 +80,15 @@ test("Adds user from input to user selectables on button click", async () => {
let input = screen.getByTestId("username-input");
let validateUser = screen.getByTestId("validate-user");
let submit = screen.getByTestId("submit");
userEvent.type(input, "bar");
fireEvent.click(validateUser);
await act(async () => okPacket);
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
await user.type(input, "bar");
await user.click(validateUser);
await act(async () => {
await jest.runAllTimers();
});
await act(async () => {
fireEvent.click(submit);
await fireEvent.click(submit);
});
expect(callbackSpy).toHaveBeenNthCalledWith(1, ["bar"], "group");
@@ -100,7 +102,7 @@ test("Removes a user recently added from input from the selectables list", async
});
let selectedUser = screen.getByText("foo");
fireEvent.click(selectedUser);
await await fireEvent.click(selectedUser);
let unselectedUser = screen.getByText("foo");
@@ -117,14 +119,14 @@ test("Grays out a user, already in the group, when unselected and calls deleteUs
let submit = screen.getByTestId("submit");
let groupUser = screen.getByText("foo");
fireEvent.click(groupUser);
await fireEvent.click(groupUser);
let unselectedUser = screen.getByText("foo");
expect(unselectedUser.className).toBe("item unselected");
// test deleteUser call
await act(async () => {
fireEvent.click(submit);
await fireEvent.click(submit);
});
expect(callbackSpy).toHaveBeenNthCalledWith(1, ["foo"], "group");
@@ -140,7 +142,7 @@ test("Calls deleteGroup on button click", async () => {
let deleteGroup = screen.getByTestId("delete-group");
await act(async () => {
fireEvent.click(deleteGroup);
await fireEvent.click(deleteGroup);
});
expect(callbackSpy).toHaveBeenNthCalledWith(1, "group");
@@ -154,12 +156,12 @@ test("Shows a UI error dialogue when group edit fails", async () => {
});
let groupUser = screen.getByText("foo");
fireEvent.click(groupUser);
await fireEvent.click(groupUser);
let submit = screen.getByTestId("submit");
await act(async () => {
fireEvent.click(submit);
await fireEvent.click(submit);
});
let errorDialog = screen.getByText("Failed to edit group.");
@@ -176,12 +178,12 @@ test("Shows a UI error dialogue when group edit returns an improper status code"
});
let groupUser = screen.getByText("foo");
fireEvent.click(groupUser);
await fireEvent.click(groupUser);
let submit = screen.getByTestId("submit");
await act(async () => {
fireEvent.click(submit);
await fireEvent.click(submit);
});
let errorDialog = screen.getByText("Failed to edit group.");
@@ -200,7 +202,7 @@ test("Shows a UI error dialogue when group delete fails", async () => {
let deleteGroup = screen.getByTestId("delete-group");
await act(async () => {
fireEvent.click(deleteGroup);
await fireEvent.click(deleteGroup);
});
let errorDialog = screen.getByText("Failed to delete group.");
@@ -219,7 +221,7 @@ test("Shows a UI error dialogue when group delete returns an improper status cod
let deleteGroup = screen.getByTestId("delete-group");
await act(async () => {
fireEvent.click(deleteGroup);
await fireEvent.click(deleteGroup);
});
let errorDialog = screen.getByText("Failed to delete group.");

View File

@@ -1,6 +1,5 @@
import React from "react";
import React, { act } from "react";
import "@testing-library/jest-dom";
import { act } from "react-dom/test-utils";
import { render, screen, fireEvent } from "@testing-library/react";
import { Provider, useSelector } from "react-redux";
import { createStore } from "redux";
@@ -71,6 +70,7 @@ afterEach(() => {
useSelector.mockClear();
mockReducers.mockClear();
useSearchParams.mockClear();
jest.runAllTimers();
});
test("Renders", async () => {
@@ -138,7 +138,7 @@ test("Interacting with PaginationFooter causes page refresh", async () => {
let next = screen.getByTestId("paginate-next");
await act(async () => {
fireEvent.click(next);
await fireEvent.click(next);
});
expect(updateGroupsSpy).toBeCalledWith(2, 2);
// mocked updateGroups means callback after load doesn't fire

View File

@@ -2,8 +2,6 @@ import React from "react";
import PropTypes from "prop-types";
import { Button, FormControl } from "react-bootstrap";
import "./pagination-footer.css";
const PaginationFooter = (props) => {
const { offset, limit, visible, total, next, prev, handleLimit } = props;
return (
@@ -13,33 +11,45 @@ const PaginationFooter = (props) => {
{total ? `of ${total}` : ""}
<br />
{offset >= 1 ? (
<Button variant="light" size="sm">
<span
className="active-pagination"
data-testid="paginate-prev"
onClick={prev}
>
Previous
</span>
<Button
variant="light"
size="sm"
onClick={prev}
className="me-2"
data-testid="paginate-prev"
>
Previous
</Button>
) : (
<Button variant="light" size="sm">
<span className="inactive-pagination">Previous</span>
<Button
variant="light"
size="sm"
className="me-2"
disabled
aria-disabled="true"
>
Previous
</Button>
)}
{offset + visible < total ? (
<Button variant="light" size="sm">
<span
className="active-pagination"
data-testid="paginate-next"
onClick={next}
>
Next
</span>
<Button
variant="light"
size="sm"
className="me-2"
onClick={next}
data-testid="paginate-next"
>
Next
</Button>
) : (
<Button variant="light" size="sm">
<span className="inactive-pagination">Next</span>
<Button
variant="light"
size="sm"
className="me-2"
disabled
aria-disabled="true"
>
Next
</Button>
)}
<label>

View File

@@ -1,14 +0,0 @@
@import url(../../style/root.css);
.pagination-footer * button {
margin-right: 10px;
}
.pagination-footer * .inactive-pagination {
color: gray;
cursor: not-allowed;
}
.pagination-footer * button.spaced {
color: var(--blue);
}

View File

@@ -453,7 +453,7 @@ const ServerDashboard = (props) => {
setStateFilter(event.target.checked ? "active" : null);
}}
/>
<Form.Check.Label for="active-servers-filter">
<Form.Check.Label htmlFor="active-servers-filter">
{"only active servers"}
</Form.Check.Label>
</Form.Check>

View File

@@ -1,7 +1,6 @@
import React from "react";
import React, { act } from "react";
import { withProps } from "recompose";
import "@testing-library/jest-dom";
import { act } from "react-dom/test-utils";
import userEvent from "@testing-library/user-event";
import {
render,
@@ -207,7 +206,6 @@ let mockUpdateUsers = jest.fn(({ offset, limit, sort, name_filter, state }) => {
let searchParams = new URLSearchParams();
beforeEach(() => {
jest.useFakeTimers();
useSelector.mockImplementation((callback) => {
return callback(mockAppState());
});
@@ -291,7 +289,7 @@ test("Invokes the startServer event on button click", async () => {
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
await act(async () => {
fireEvent.click(start_elems[0]);
await fireEvent.click(start_elems[0]);
});
expect(callbackSpy).toHaveBeenCalled();
@@ -307,7 +305,7 @@ test("Invokes the stopServer event on button click", async () => {
let stop = screen.getByText("Stop Server");
await act(async () => {
fireEvent.click(stop);
await fireEvent.click(stop);
});
expect(callbackSpy).toHaveBeenCalled();
@@ -323,7 +321,7 @@ test("Invokes the shutdownHub event on button click", async () => {
let shutdown = screen.getByText("Shutdown Hub");
await act(async () => {
fireEvent.click(shutdown);
await fireEvent.click(shutdown);
});
expect(callbackSpy).toHaveBeenCalled();
@@ -338,7 +336,7 @@ test("Sorts according to username", async () => {
expect(searchParams.get("sort")).toEqual(null);
let handler = screen.getByTestId(testId);
fireEvent.click(handler);
await fireEvent.click(handler);
expect(searchParams.get("sort")).toEqual("name");
await act(async () => {
@@ -346,7 +344,7 @@ test("Sorts according to username", async () => {
handler = screen.getByTestId(testId);
});
fireEvent.click(handler);
await fireEvent.click(handler);
expect(searchParams.get("sort")).toEqual("-name");
await act(async () => {
@@ -354,7 +352,7 @@ test("Sorts according to username", async () => {
handler = screen.getByTestId(testId);
});
fireEvent.click(handler);
await fireEvent.click(handler);
expect(searchParams.get("sort")).toEqual("name");
});
@@ -367,7 +365,7 @@ test("Sorts according to last activity", async () => {
expect(searchParams.get("sort")).toEqual(null);
let handler = screen.getByTestId(testId);
fireEvent.click(handler);
await fireEvent.click(handler);
expect(searchParams.get("sort")).toEqual("last_activity");
await act(async () => {
@@ -375,7 +373,7 @@ test("Sorts according to last activity", async () => {
handler = screen.getByTestId(testId);
});
fireEvent.click(handler);
await fireEvent.click(handler);
expect(searchParams.get("sort")).toEqual("-last_activity");
await act(async () => {
@@ -383,7 +381,7 @@ test("Sorts according to last activity", async () => {
handler = screen.getByTestId(testId);
});
fireEvent.click(handler);
await fireEvent.click(handler);
expect(searchParams.get("sort")).toEqual("last_activity");
});
@@ -392,12 +390,10 @@ test("Filter according to server status (running/not running)", async () => {
await act(async () => {
rerender = render(serverDashboardJsx()).rerender;
});
console.log(rerender);
console.log("begin test");
const label = "only active servers";
let handler = screen.getByLabelText(label);
expect(handler.checked).toEqual(false);
fireEvent.click(handler);
await fireEvent.click(handler);
// FIXME: need to force a rerender to get updated checkbox
// I don't think this should be required
@@ -408,7 +404,7 @@ test("Filter according to server status (running/not running)", async () => {
expect(searchParams.get("state")).toEqual("active");
expect(handler.checked).toEqual(true);
fireEvent.click(handler);
await fireEvent.click(handler);
await act(async () => {
rerender(serverDashboardJsx());
@@ -431,17 +427,14 @@ test("Shows server details with button click", async () => {
expect(collapse).toHaveClass("collapse");
expect(collapse).not.toHaveClass("show");
expect(collapseBar).not.toHaveClass("show");
await fireEvent.click(button);
await act(async () => {
fireEvent.click(button);
jest.runAllTimers();
});
expect(collapse).toHaveClass("collapse show");
expect(collapseBar).not.toHaveClass("show");
await fireEvent.click(button);
await act(async () => {
fireEvent.click(button);
jest.runAllTimers();
});
@@ -449,8 +442,8 @@ test("Shows server details with button click", async () => {
expect(collapse).not.toHaveClass("show");
expect(collapseBar).not.toHaveClass("show");
await fireEvent.click(button);
await act(async () => {
fireEvent.click(button);
jest.runAllTimers();
});
@@ -480,7 +473,7 @@ test("Shows a UI error dialogue when start all servers fails", async () => {
let startAll = screen.getByTestId("start-all");
await act(async () => {
fireEvent.click(startAll);
await fireEvent.click(startAll);
});
let errorDialog = screen.getByText("Failed to start servers.");
@@ -496,7 +489,7 @@ test("Shows a UI error dialogue when stop all servers fails", async () => {
let stopAll = screen.getByTestId("stop-all");
await act(async () => {
fireEvent.click(stopAll);
await fireEvent.click(stopAll);
});
let errorDialog = screen.getByText("Failed to stop servers.");
@@ -513,7 +506,7 @@ test("Shows a UI error dialogue when start user server fails", async () => {
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
await act(async () => {
fireEvent.click(start_elems[0]);
await fireEvent.click(start_elems[0]);
});
let errorDialog = screen.getByText("Failed to start server.");
@@ -531,7 +524,7 @@ test("Shows a UI error dialogue when start user server returns an improper statu
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
await act(async () => {
fireEvent.click(start_elems[0]);
await fireEvent.click(start_elems[0]);
});
let errorDialog = screen.getByText("Failed to start server.");
@@ -550,7 +543,7 @@ test("Shows a UI error dialogue when stop user servers fails", async () => {
let stop = screen.getByText("Stop Server");
await act(async () => {
fireEvent.click(stop);
await fireEvent.click(stop);
});
let errorDialog = screen.getByText("Failed to stop server.");
@@ -569,7 +562,7 @@ test("Shows a UI error dialogue when stop user server returns an improper status
let stop = screen.getByText("Stop Server");
await act(async () => {
fireEvent.click(stop);
await fireEvent.click(stop);
});
let errorDialog = screen.getByText("Failed to stop server.");
@@ -584,12 +577,13 @@ test("Search for user calls updateUsers with name filter", async () => {
render(serverDashboardJsx());
});
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
let search = screen.getByLabelText("user-search");
expect(mockUpdateUsers.mock.calls).toHaveLength(1);
expect(searchParams.get("offset")).toEqual("2");
userEvent.type(search, "a");
await user.type(search, "a");
expect(search.value).toEqual("a");
await act(async () => {
jest.runAllTimers();
@@ -599,7 +593,7 @@ test("Search for user calls updateUsers with name filter", async () => {
// FIXME: useSelector mocks prevent updateUsers from being called
// expect(mockUpdateUsers.mock.calls).toHaveLength(2);
// expect(mockUpdateUsers).toBeCalledWith(0, 100, "a");
userEvent.type(search, "b");
await user.type(search, "b");
expect(search.value).toEqual("ab");
await act(async () => {
jest.runAllTimers();
@@ -672,7 +666,7 @@ test("Start server and confirm pending state", async () => {
expect(buttons[2].textContent).toBe("Edit User");
await act(async () => {
fireEvent.click(buttons[0]);
await fireEvent.click(buttons[0]);
});
expect(mockUpdateUsers.mock.calls).toHaveLength(1);

View File

@@ -3,7 +3,7 @@ const base_url = jhdata.base_url || "/";
const xsrfToken = jhdata.xsrf_token;
export const jhapiRequest = (endpoint, method, data) => {
let api_url = new URL(`${base_url}hub/api` + endpoint, location.origin);
let api_url = new URL(`${base_url}api` + endpoint, location.origin);
if (xsrfToken) {
api_url.searchParams.set("_xsrf", xsrfToken);
}

View File

@@ -34,5 +34,5 @@ export const MainContainer = (props) => {
MainContainer.propTypes = {
errorAlert: PropTypes.string,
setErrorAlert: PropTypes.func,
children: PropTypes.array,
children: PropTypes.node,
};

View File

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

View File

@@ -282,7 +282,7 @@ class JupyterHub(Application):
@default('classes')
def _load_classes(self):
classes = [Spawner, Authenticator, CryptKeeper]
classes = {Spawner, Authenticator, CryptKeeper}
for name, trait in self.traits(config=True).items():
# load entry point groups into configurable class list
# so that they show up in config files, etc.
@@ -298,9 +298,9 @@ class JupyterHub(Application):
e,
)
continue
if cls not in classes and isinstance(cls, Configurable):
classes.append(cls)
return classes
if issubclass(cls, Configurable):
classes.add(cls)
return list(classes)
load_groups = Dict(
Union([Dict(), List()]),
@@ -873,13 +873,7 @@ class JupyterHub(Application):
but your identity provider is likely much more strict,
allowing you to make assumptions about the name.
The default behavior is to have all services
on a single `services.{domain}` subdomain,
and each user on `{username}.{domain}`.
This is the 'legacy' scheme,
and doesn't work for all usernames.
The 'idna' scheme is a new scheme that should produce a valid domain name for any user,
The 'idna' hook should produce a valid domain name for any user,
using IDNA encoding for unicode usernames, and a truncate-and-hash approach for
any usernames that can't be easily encoded into a domain component.
@@ -2182,7 +2176,11 @@ class JupyterHub(Application):
# but changes to the allowed_users set can occur in the database,
# and persist across sessions.
total_users = 0
blocked_users = self.authenticator.blocked_users
for user in db.query(orm.User):
if user.name in blocked_users:
# don't call add_user with blocked users
continue
try:
f = self.authenticator.add_user(user)
if f:
@@ -2238,6 +2236,35 @@ class JupyterHub(Application):
await maybe_future(f)
return user
async def init_blocked_users(self):
"""Revoke all permissions for users in Authenticator.blocked_users"""
blocked_users = self.authenticator.blocked_users
if not blocked_users:
# nothing to check
return
db = self.db
for user in db.query(orm.User).filter(orm.User.name.in_(blocked_users)):
# revoke permissions from blocked users
# so already-issued credentials have no access to the API
self.log.debug(f"Found blocked user in database: {user.name}")
if user.admin:
self.log.warning(
f"Removing admin permissions from blocked user {user.name}"
)
user.admin = False
if user.roles:
self.log.warning(
f"Removing blocked user {user.name} from roles: {', '.join(role.name for role in user.roles)}"
)
user.roles = []
if user.groups:
self.log.warning(
f"Removing blocked user {user.name} from groups: {', '.join(group.name for group in user.groups)}"
)
user.groups = []
db.commit()
async def init_groups(self):
"""Load predefined groups into the database"""
db = self.db
@@ -2965,6 +2992,18 @@ class JupyterHub(Application):
async def check_spawner(user, name, spawner):
status = 0
if spawner.server:
if user.name in self.authenticator.blocked_users:
self.log.warning(
f"Stopping spawner for blocked user: {spawner._log_name}"
)
try:
await user.stop(name)
except Exception:
self.log.exception(
f"Failed to stop {spawner._log_name}",
exc_info=True,
)
return
try:
status = await spawner.poll()
except Exception:
@@ -3356,6 +3395,7 @@ class JupyterHub(Application):
self.init_services()
await self.init_api_tokens()
await self.init_role_assignment()
await self.init_blocked_users()
self.init_tornado_settings()
self.init_handlers()
self.init_tornado_application()
@@ -3414,7 +3454,6 @@ class JupyterHub(Application):
metrics_collector = self.metrics_collector = PeriodicMetricsCollector(
parent=self, db=self.db
)
metrics_collector.start()
async def cleanup(self):
"""Shutdown managed services and various subprocesses. Cleanup runtime files."""
@@ -3595,7 +3634,7 @@ class JupyterHub(Application):
if service.managed:
status = await service.spawner.poll()
if status is not None:
self.log.error(
self.log.critical(
"Service %s exited with status %s",
service_name,
status,
@@ -3604,12 +3643,19 @@ class JupyterHub(Application):
else:
return True
else:
self.log.error(
"Cannot connect to %s service %s at %s. Is it running?",
service.kind,
service_name,
service.url,
)
if service.managed:
self.log.critical(
"Cannot connect to %s service %s",
service_name,
service.kind,
)
else:
self.log.warning(
"Cannot connect to %s service %s at %s. Is it running?",
service.kind,
service_name,
service.url,
)
return False
return True
@@ -3643,6 +3689,9 @@ class JupyterHub(Application):
loop.stop()
return
# start collecting metrics
self.metrics_collector.start()
# start the proxy
if self.proxy.should_start:
try:
@@ -3697,18 +3746,8 @@ class JupyterHub(Application):
# start the service(s)
for service_name, service in self._service_map.items():
service_ready = await self.start_service(service_name, service, ssl_context)
if not service_ready:
if service.from_config:
# Stop the application if a config-based service failed to start.
self.exit(1)
else:
# Only warn for database-based service, so that admin can connect
# to hub to remove the service.
self.log.error(
"Failed to reach externally managed service %s",
service_name,
exc_info=True,
)
if not service_ready and service.managed:
self.exit(1)
await self.proxy.check_routes(self.users, self._service_map)
@@ -3900,4 +3939,8 @@ UpgradeDB.classes.append(JupyterHub)
main = JupyterHub.launch_instance
if __name__ == "__main__":
main()
# don't invoke __main__.main here because __main__.JupyterHub
# is not jupyterhub.app.JupyterHub. There will be two!
from jupyterhub import app
app.JupyterHub.launch_instance()

View File

@@ -300,6 +300,14 @@ class Authenticator(LoggingConfigurable):
If empty, does not perform any additional restriction.
.. versionadded: 0.9
.. versionchanged:: 5.2
Users blocked via `blocked_users` that may have logged in in the past
have all permissions and group membership revoked
and all servers stopped at JupyterHub startup.
Previously, User permissions (e.g. API tokens)
and servers were unaffected and required additional
administrator operations to block after a user is added to `blocked_users`.
.. versionchanged:: 1.2
`Authenticator.blacklist` renamed to `blocked_users`
@@ -1230,7 +1238,20 @@ class PAMAuthenticator(LocalAuthenticator):
@default('executor')
def _default_executor(self):
return ThreadPoolExecutor(1)
return ThreadPoolExecutor(self.executor_threads)
executor_threads = Integer(
4,
config=True,
help="""
Number of executor threads.
PAM auth requests happen in this thread, so it is mostly
waiting for the pam stack. One thread is usually enough,
unless your pam stack is doing something slow like network
requests
""",
)
encoding = Unicode(
'utf8',

View File

@@ -1064,7 +1064,7 @@ class BaseHandler(RequestHandler):
human_retry_time = "%i0 seconds" % math.ceil(retry_time / 10.0)
else:
# round number of minutes
human_retry_time = "%i minutes" % math.round(retry_time / 60.0)
human_retry_time = "%i minutes" % round(retry_time / 60.0)
self.log.warning(
'%s pending spawns, throttling. Suggested retry in %s seconds.',

View File

@@ -466,7 +466,6 @@ class AdminHandler(BaseHandler):
named_server_limit_per_user=await self.get_current_user_named_server_limit(),
server_version=f'{__version__} {self.version_hash}',
api_page_limit=self.settings["api_page_default_limit"],
base_url=self.settings["base_url"],
)
self.finish(html)

View File

@@ -1141,7 +1141,6 @@ class APIToken(Hashed, Base):
expires_in=None,
client_id=None,
oauth_client=None,
return_orm=False,
):
"""Generate a new API token for a user or service"""
assert user or service

View File

@@ -63,9 +63,29 @@ if _as_extension:
f"Cannot use JUPYTERHUB_SINGLEUSER_EXTENSION={_extension_env} with JUPYTERHUB_SINGLEUSER_APP={_app_env}."
" Please pick one or the other."
)
from .extension import main
try:
from .extension import main
except ImportError as e:
# raise from to preserve original import error
raise ImportError(
"Failed to import JupyterHub singleuser extension."
" Make sure to install dependencies for your single-user server, e.g.\n"
" pip install jupyterlab"
) from e
else:
from .app import SingleUserNotebookApp, main
try:
from .app import SingleUserNotebookApp, main
except ImportError as e:
# raise from to preserve original import error
if _app_env:
_app_env_log = f"JUPYTERHUB_SINGLEUSER_APP={_app_env}"
else:
_app_env_log = "default single-user server"
raise ImportError(
f"Failed to import {_app_env_log}."
" Make sure to install dependencies for your single-user server, e.g.\n"
" pip install jupyterlab"
) from e
# backward-compatibility
if SingleUserNotebookApp is not None:

View File

@@ -22,8 +22,6 @@ rather than keeing these monkey patches around.
import os
from pathlib import Path
from jupyter_core import paths
def _is_relative_to(path, prefix):
"""
@@ -68,6 +66,10 @@ def _disable_user_config(serverapp):
2. Search paths for extensions, etc.
3. import path
"""
# delayed import to avoid triggering early ImportError
# with unmet dependencies
from jupyter_core import paths
original_jupyter_path = paths.jupyter_path()
jupyter_path_without_home = list(_exclude_home(original_jupyter_path))

View File

@@ -361,9 +361,8 @@ class SingleUserNotebookAppMixin(Configurable):
"""override default log format to include time"""
return "%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s %(module)s:%(lineno)d]%(end_color)s %(message)s"
def _confirm_exit(self):
# disable the exit confirmation for background notebook processes
self.io_loop.add_callback_from_signal(self.io_loop.stop)
def _handle_sigint(self, *args, **kwargs):
self._signal_stop(*args, **kwargs)
def migrate_config(self):
if self.disable_user_config:

View File

@@ -701,16 +701,7 @@ class Spawner(LoggingConfigurable):
)
env_keep = List(
[
'PATH',
'PYTHONPATH',
'CONDA_ROOT',
'CONDA_DEFAULT_ENV',
'VIRTUAL_ENV',
'LANG',
'LC_ALL',
'JUPYTERHUB_SINGLEUSER_APP',
],
['JUPYTERHUB_SINGLEUSER_APP'],
help="""
List of environment variables for the single-user server to inherit from the JupyterHub process.
@@ -1716,6 +1707,20 @@ class LocalProcessSpawner(Spawner):
""",
)
@default("env_keep")
def _env_keep_default(self):
return [
"CONDA_DEFAULT_ENV",
"CONDA_ROOT",
"JUPYTERHUB_SINGLEUSER_APP",
"LANG",
"LC_ALL",
"LD_LIBRARY_PATH",
"PATH",
"PYTHONPATH",
"VIRTUAL_ENV",
]
def make_preexec_fn(self, name):
"""
Return a function that can be used to set the user id of the spawned process to user with name `name`

View File

@@ -283,7 +283,7 @@ async def test_spawn_pending_progress(
await launch_btn.click()
# wait for progress message to appear
progress = browser.locator("#progress-message")
progress_message = await progress.inner_text()
progress_message = await progress.text_content()
async with browser.expect_navigation(url=re.compile(".*/user/" + f"{urlname}/")):
# wait for log messages to appear
expected_messages = [
@@ -293,7 +293,7 @@ async def test_spawn_pending_progress(
]
while not user.spawner.ready:
logs_list = [
await log.inner_text()
await log.text_content()
for log in await browser.locator("div.progress-log-event").all()
]
if progress_message:
@@ -654,7 +654,7 @@ async def test_request_token_expiration(
await api_token_table_area.locator("tr.token-row")
.get_by_role("cell")
.nth(0)
.inner_text()
.text_content()
)
assert note_on_page == expected_note
@@ -663,7 +663,7 @@ async def test_request_token_expiration(
await api_token_table_area.locator("tr.token-row")
.get_by_role("cell")
.nth(2)
.inner_text()
.text_content()
)
assert last_used_text == "Never"
@@ -671,7 +671,7 @@ async def test_request_token_expiration(
await api_token_table_area.locator("tr.token-row")
.get_by_role("cell")
.nth(4)
.inner_text()
.text_content()
)
if token_opt == "Never":
@@ -734,7 +734,7 @@ async def test_request_token_permissions(
if not granted:
error_dialog = browser.locator("#error-dialog")
await expect(error_dialog).to_be_visible()
error_message = await error_dialog.locator(".modal-body").inner_text()
error_message = await error_dialog.locator(".modal-body").text_content()
assert "API request failed (400)" in error_message
assert expected_error in error_message
await error_dialog.locator("button[aria-label='Close']").click()
@@ -1087,6 +1087,7 @@ async def open_admin_page(app, browser, login_as=None):
# url = url_path_join(public_host(app), app.hub.base_url, "/login?next=" + admin_page)
await browser.goto(admin_page)
await expect(browser).to_have_url(re.compile(".*/hub/admin"))
await browser.wait_for_load_state("networkidle")
def create_list_of_users(create_user_with_scopes, n):
@@ -1186,7 +1187,7 @@ async def test_paging_on_admin_page(
re.compile(".*" + f"1-{min(users_count_db, 50)}" + ".*")
)
if users_count_db > 50:
await expect(btn_next.locator("//span")).to_have_class("active-pagination")
await expect(btn_next).to_be_enabled()
# click on Next button
await btn_next.click()
if users_count_db <= 100:
@@ -1195,15 +1196,13 @@ async def test_paging_on_admin_page(
)
else:
await expect(displaying).to_have_text(re.compile(".*" + "51-100" + ".*"))
await expect(btn_next.locator("//span")).to_have_class("active-pagination")
await expect(btn_previous.locator("//span")).to_have_class("active-pagination")
await expect(btn_next).to_be_enabled()
await expect(btn_previous).to_be_enabled()
# click on Previous button
await btn_previous.click()
else:
await expect(btn_next.locator("//span")).to_have_class("inactive-pagination")
await expect(btn_previous.locator("//span")).to_have_class(
"inactive-pagination"
)
await expect(btn_next).to_be_disabled()
await expect(btn_previous).to_be_disabled()
@pytest.mark.parametrize(
@@ -1256,6 +1255,7 @@ async def test_search_on_admin_page(
await expect(displaying).to_contain_text(re.compile("1-50"))
# click on Next button to verify that the rest part of filtered list is displayed on the next page
await browser.get_by_role("button", name="Next").click()
await browser.wait_for_load_state("networkidle")
filtered_list_on_next_page = browser.locator('//tr[@class="user-row"]')
await expect(filtered_list_on_page).to_have_count(users_count_db_filtered - 50)
for element in await filtered_list_on_next_page.get_by_test_id(

View File

@@ -33,7 +33,9 @@ import sys
from subprocess import TimeoutExpired
from unittest import mock
from pytest import fixture, raises
import pytest_asyncio
from packaging.version import parse as parse_version
from pytest import fixture, mark, raises
from sqlalchemy import event
from tornado.httpclient import HTTPError
from tornado.platform.asyncio import AsyncIOMainLoop
@@ -57,6 +59,41 @@ from .utils import add_user
# global db session object
_db = None
_pytest_asyncio_24 = parse_version(pytest_asyncio.__version__) >= parse_version(
"0.24.0.dev0"
)
def pytest_collection_modifyitems(items):
if _pytest_asyncio_24:
# apply loop_scope="module" to all async tests by default
# this is only for pytest_asyncio >= 0.24
# pytest_asyncio < 0.24 uses overridden `event_loop` fixture
# this can be hopefully be removed in favor of config if
# https://github.com/pytest-dev/pytest-asyncio/issues/793
# is addressed
pytest_asyncio_tests = (
item for item in items if pytest_asyncio.is_async_test(item)
)
asyncio_scope_marker = mark.asyncio(loop_scope="module")
for async_test in pytest_asyncio_tests:
# add asyncio marker _if_ not already present
asyncio_marker = async_test.get_closest_marker('asyncio')
if not asyncio_marker or not asyncio_marker.kwargs:
async_test.add_marker(asyncio_scope_marker, append=False)
if not _pytest_asyncio_24:
# pre-pytest-asyncio 0.24, overriding event_loop fixture
# was the way to change scope of event_loop
# post-0.24 uses modifyitems above
@fixture(scope='module')
def event_loop(request):
"""Same as pytest-asyncio.event_loop, but re-scoped to module-level"""
event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(event_loop)
return event_loop
@fixture(scope='module')
def ssl_tmpdir(tmpdir_factory):
@@ -125,15 +162,7 @@ def db():
@fixture(scope='module')
def event_loop(request):
"""Same as pytest-asyncio.event_loop, but re-scoped to module-level"""
event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(event_loop)
return event_loop
@fixture(scope='module')
async def io_loop(event_loop, request):
async def io_loop(request):
"""Mostly obsolete fixture for tornado event loop
Main purpose is to register cleanup (close) after we're done with the loop.
@@ -141,6 +170,7 @@ async def io_loop(event_loop, request):
happens before the io_loop is closed.
"""
io_loop = AsyncIOMainLoop()
event_loop = asyncio.get_running_loop()
assert asyncio.get_event_loop() is event_loop
assert io_loop.asyncio_loop is event_loop

View File

@@ -8,6 +8,7 @@ import os
import re
import sys
import time
from concurrent.futures import ThreadPoolExecutor
from subprocess import PIPE, Popen, check_output
from tempfile import NamedTemporaryFile, TemporaryDirectory
from unittest.mock import patch
@@ -16,6 +17,8 @@ import pytest
import traitlets
from traitlets.config import Config
from jupyterhub.scopes import get_scopes_for
from .. import orm
from ..app import COOKIE_SECRET_BYTES, JupyterHub
from .mocking import MockHub
@@ -289,8 +292,7 @@ def persist_db(tmpdir):
def new_hub(request, tmpdir, persist_db):
"""Fixture to launch a new hub for testing"""
async def new_hub():
kwargs = {}
async def new_hub(**kwargs):
ssl_enabled = getattr(request.module, "ssl_enabled", False)
if ssl_enabled:
kwargs['internal_certs_location'] = str(tmpdir)
@@ -306,17 +308,6 @@ def new_hub(request, tmpdir, persist_db):
async def test_resume_spawners(tmpdir, request, new_hub):
async def new_hub():
kwargs = {}
ssl_enabled = getattr(request.module, "ssl_enabled", False)
if ssl_enabled:
kwargs['internal_certs_location'] = str(tmpdir)
app = MockHub(test_clean_db=False, **kwargs)
app.config.ConfigurableHTTPProxy.should_start = False
app.config.ConfigurableHTTPProxy.auth_token = 'unused'
await app.initialize([])
return app
app = await new_hub()
db = app.db
# spawn a user's server
@@ -537,3 +528,74 @@ async def test_recreate_service_from_database(
# start one more, service should be gone
app = await new_hub()
assert service_name not in app._service_map
async def test_revoke_blocked_users(username, groupname, new_hub):
config = Config()
config.Authenticator.admin_users = {username}
kept_username = username + "-kept"
config.Authenticator.allowed_users = {username, kept_username}
config.JupyterHub.load_groups = {
groupname: {
"users": [username],
},
}
config.JupyterHub.load_roles = [
{
"name": "testrole",
"scopes": ["access:services"],
"groups": [groupname],
}
]
app = await new_hub(config=config)
user = app.users[username]
# load some credentials, start server
await user.spawn()
# await app.proxy.add_user(user)
spawner = user.spawners['']
token = user.new_api_token()
orm_token = orm.APIToken.find(app.db, token)
app.cleanup_servers = False
app.stop()
# before state
assert await spawner.poll() is None
assert sorted(role.name for role in user.roles) == ['admin', 'user']
assert [g.name for g in user.groups] == [groupname]
assert user.admin
user_scopes = get_scopes_for(user)
assert "access:servers" in user_scopes
token_scopes = get_scopes_for(orm_token)
assert "access:servers" in token_scopes
# start a new hub, now with blocked users
config = Config()
name_doesnt_exist = user.name + "-doesntexist"
config.Authenticator.blocked_users = {user.name, name_doesnt_exist}
config.JupyterHub.init_spawners_timeout = 60
# background spawner.proc.wait to avoid waiting for zombie process here
with ThreadPoolExecutor(1) as pool:
pool.submit(spawner.proc.wait)
app2 = await new_hub(config=config)
assert app2.db_url == app.db_url
# check that blocked user has no permissions
user2 = app2.users[user.name]
assert user2.roles == []
assert user2.groups == []
assert user2.admin is False
user_scopes = get_scopes_for(user2)
assert user_scopes == set()
orm_token = orm.APIToken.find(app2.db, token)
token_scopes = get_scopes_for(orm_token)
assert token_scopes == set()
# spawner stopped
assert user2.spawners == {}
assert await spawner.poll() is not None
# (sanity check) didn't lose other user
kept_user = app2.users[kept_username]
assert 'user' in [r.name for r in kept_user.roles]
app2.stop()

216
package-lock.json generated
View File

@@ -21,10 +21,9 @@
}
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.2.tgz",
"integrity": "sha512-hRILoInAx8GNT5IMkrtIt9blOdrqHOnPBH+k70aWUAqPZPgopb9G5EQJFpaBx/S8zp2fC+mPW349Bziuk1o28Q==",
"hasInstallScript": true,
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz",
"integrity": "sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow==",
"engines": {
"node": ">=6"
}
@@ -39,31 +38,6 @@
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bootstrap": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
@@ -82,78 +56,19 @@
"@popperjs/core": "^2.11.8"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
"dev": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 8.10.0"
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/immutable": {
@@ -162,98 +77,37 @@
"integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==",
"dev": true
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/jquery": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
"integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ=="
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
},
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"engines": {
"node": "*"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"node_modules/readdirp": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.1.tgz",
"integrity": "sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": {
"node": ">=8.6"
"node": ">= 14.16.0"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/requirejs": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz",
"integrity": "sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==",
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.7.tgz",
"integrity": "sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw==",
"license": "MIT",
"bin": {
"r_js": "bin/r.js",
"r.js": "bin/r.js"
@@ -263,12 +117,12 @@
}
},
"node_modules/sass": {
"version": "1.74.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.74.1.tgz",
"integrity": "sha512-w0Z9p/rWZWelb88ISOLyvqTWGmtmu2QJICqDBGyNnfG4OUnPX9BBjjYIXUpXCMOOg5MQWNpqzt876la1fsTvUA==",
"version": "1.79.4",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.79.4.tgz",
"integrity": "sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"chokidar": "^4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
@@ -287,18 +141,6 @@
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
}
}
}

View File

@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
# ref: https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
[project]
name = "jupyterhub"
version = "5.1.0"
version = "5.2.1"
dynamic = ["readme", "dependencies"]
description = "JupyterHub: A multi-user server for Jupyter notebooks"
authors = [
@@ -51,8 +51,7 @@ test = [
# the test test_nbclassic_control_panel.
"nbclassic",
"pytest>=3.3",
# FIXME: unpin pytest-asyncio
"pytest-asyncio>=0.17,<0.23",
"pytest-asyncio>=0.17,!=0.23.*",
"pytest-cov",
"pytest-rerunfailures",
"requests-mock",
@@ -147,7 +146,7 @@ indent_size = 2
github_url = "https://github.com/jupyterhub/jupyterhub"
[tool.tbump.version]
current = "5.1.0"
current = "5.2.1"
# Example of a semver regexp.
# Make sure this matches current_version before

View File

@@ -5,6 +5,8 @@
# automatically run coroutine tests with asyncio
asyncio_mode = auto
# use module-level loop scope (requires pytest-asyncio 0.24)
asyncio_default_fixture_loop_scope = module
# jupyter_server plugin is incompatible with notebook imports
addopts = -p no:jupyter_server -m 'not browser' --color yes --durations 10 --verbose

View File

@@ -0,0 +1,59 @@
"use strict";
/* Simplified from bootstrap dark mode toggle
https://getbootstrap.com/docs/5.3/customize/color-modes/#javascript
*/
// theme is stored in localStorage
const getStoredTheme = () => localStorage.getItem("jupyterhub-bs-theme");
const setStoredTheme = (theme) =>
localStorage.setItem("jupyterhub-bs-theme", theme);
const getPreferredTheme = () => {
// return chosen theme. Pick value in localStorage if there,
// otherwise use system setting if defined
const storedTheme = getStoredTheme();
if (storedTheme) {
return storedTheme;
}
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
};
const setTheme = (theme) => {
if (theme === "auto") {
document.documentElement.setAttribute(
"data-bs-theme",
window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light",
);
} else {
document.documentElement.setAttribute("data-bs-theme", theme);
}
};
setTheme(getPreferredTheme());
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", () => {
// handle system change if no explicit theme preference is stored
const storedTheme = getStoredTheme();
if (storedTheme !== "light" && storedTheme !== "dark") {
setTheme(getPreferredTheme());
}
});
window.addEventListener("DOMContentLoaded", () => {
// clicking #dark-theme-toggle toggles dark theme
// (in page.html)
const toggle = document.getElementById("dark-theme-toggle");
toggle.addEventListener("click", () => {
const currentTheme = document.documentElement.getAttribute("data-bs-theme");
const theme = currentTheme == "dark" ? "light" : "dark";
setStoredTheme(theme);
setTheme(theme);
});
});

View File

@@ -18,7 +18,16 @@ $grid-float-breakpoint: map-get($grid-breakpoints, "sm");
&:focus {
// no color change
color: var(--#{$prefix}navbar-color);
background-color: darken($body-tertiary-bg, 10%);
background-color: shift-color($body-tertiary-bg, 10%);
}
}
}
[data-bs-theme="dark"] .navbar-nav {
.nav-link {
&:hover,
&:focus {
background-color: shift-color($body-tertiary-bg-dark, -20%);
}
}
}
@@ -91,3 +100,24 @@ $grid-float-breakpoint: map-get($grid-breakpoints, "sm");
$hover-color: #fff
);
}
// contrast button variants
// same as btn-dark on light and btn-light on dark
.btn-contrast,
[data-bs-theme="light"] .btn-contrast {
@extend .btn-dark;
}
.btn-outline-contrast,
[data-bs-theme="light"] .btn-outline-contrast {
@extend .btn-outline-dark;
}
[data-bs-theme="dark"] {
.btn-contrast {
@extend .btn-light;
}
.btn-outline-contrast {
@extend .btn-outline-light;
}
}

View File

@@ -39,15 +39,18 @@
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% block stylesheet %}
<link rel="stylesheet" href="{{ static_url("css/style.min.css") }}" type="text/css" />
<link rel="stylesheet"
href="{{ static_url('css/style.min.css') }}"
type="text/css" />
{% endblock stylesheet %}
{% block favicon %}
<link rel="icon" href="{{ static_url("favicon.ico") }}" type="image/x-icon">
<link rel="icon" href="{{ static_url('favicon.ico') }}" type="image/x-icon">
{% endblock favicon %}
{% block scripts %}
<script src="{{static_url("components/bootstrap/dist/js/bootstrap.bundle.min.js") }}" type="text/javascript" charset="utf-8"></script>
<script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
<script src="{{static_url("components/jquery/dist/jquery.min.js") }}" type="text/javascript" charset="utf-8"></script>
<script src="{{static_url("js/darkmode.js") }}" type="text/javascript" charset="utf-8"></script>
{% endblock scripts %}
{# djlint js formatting doesn't handle template blocks in js #}
{# djlint: off #}
@@ -126,8 +129,8 @@
</button>
{% endif %}
<div class="collapse navbar-collapse" id="thenavbar">
{% if user %}
<ul class="navbar-nav me-auto mb-0">
<ul class="navbar-nav me-auto mb-0">
{% if user %}
{% block nav_bar_left_items %}
<li class="nav-item">
<a class="nav-link" href="{{ base_url }}home">Home</a>
@@ -159,23 +162,33 @@
</li>
{% endif %}
{% endblock nav_bar_left_items %}
</ul>
{% endif %}
{% endif %}
</ul>
<ul class="nav navbar-nav me-2">
{% block nav_bar_right_items %}
<li class="nav-item">
{% block theme_toggle %}
<button class="btn btn-sm"
id="dark-theme-toggle"
aria-label="Toggle dark mode"
title="Toggle dark mode">
<i aria-hidden="true" class="fa fa-circle-half-stroke"></i>
</button>
{% endblock theme_toggle %}
</li>
<li class="nav-item">
{% block login_widget %}
<span id="login_widget">
{% if user %}
<span class="navbar-text me-1">{{ user.name }}</span>
<span class="me-1">{{ user.name }}</span>
<a id="logout"
role="button"
class="btn btn-sm btn-outline-dark"
class="btn btn-sm btn-outline-contrast"
href="{{ logout_url }}"> <i aria-hidden="true" class="fa fa-sign-out"></i> Logout</a>
{% else %}
<a id="login"
role="button"
class="btn btn-sm btn-outline-dark"
class="btn btn-sm btn-outline-contrast"
href="{{ login_url }}">Login</a>
{% endif %}
</span>