├── setup.py ├── screenshot.png ├── MANIFEST.in ├── labextension ├── .gitignore ├── tsconfig.json ├── .yarnrc.yml ├── README.md ├── LICENSE ├── package.json └── src │ ├── tokens.ts │ └── index.ts ├── docs ├── requirements.txt ├── source │ ├── _static │ │ └── images │ │ │ ├── logo │ │ │ └── favicon.ico │ │ │ ├── nbextension-tree.png │ │ │ └── labextension-launcher.png │ ├── install.md │ ├── convenience │ │ ├── new.md │ │ └── packages │ │ │ └── theia.md │ ├── launchers.md │ ├── examples.md │ ├── index.md │ ├── arbitrary-ports-hosts.md │ ├── conf.py │ ├── standalone.md │ └── server-process.md ├── Makefile └── make.bat ├── jupyter_server_proxy ├── etc │ ├── nbconfig │ │ └── tree.d │ │ │ └── jupyter-server-proxy.json │ ├── jupyter_server_config.d │ │ └── jupyter-server-proxy.json │ └── jupyter_notebook_config.d │ │ └── jupyter-server-proxy.json ├── _version.py ├── standalone │ ├── __init__.py │ ├── activity.py │ ├── proxy.py │ └── app.py ├── unixsock.py ├── utils.py ├── static │ └── tree.js ├── __init__.py ├── api.py ├── websocket.py ├── rawsocket.py └── config.py ├── contrib ├── template │ ├── cookiecutter.json │ └── {{cookiecutter.project_name}} │ │ ├── jupyter_{{cookiecutter.project_name}}_proxy │ │ └── __init__.py │ │ └── setup.py ├── code-server-traitlet │ ├── README.md │ └── jupyter_notebook_config.py └── theia │ ├── setup.py │ ├── jupyter_theia_proxy │ ├── __init__.py │ └── icons │ │ └── theia.svg │ └── README.rst ├── tests ├── acceptance │ ├── resources │ │ ├── index.html │ │ └── jupyter_config.json │ ├── __init__.robot │ ├── Classic.robot │ ├── Lab.robot │ ├── test_acceptance.py │ └── Notebook.robot ├── test_utils.py ├── test_config.py ├── resources │ ├── rawsocket.py │ ├── gzipserver.py │ ├── eventstream.py │ ├── proxyextension.py │ ├── httpinfo.py │ ├── redirectserver.py │ ├── websocket.py │ └── jupyter_server_config.py ├── conftest.py └── test_standalone.py ├── .flake8 ├── .readthedocs.yaml ├── .github ├── dependabot.yaml └── workflows │ ├── linkcheck.yaml │ ├── publish.yaml │ └── test.yaml ├── LICENSE ├── .pre-commit-config.yaml ├── RELEASE.md ├── .gitignore ├── README.md ├── pyproject.toml └── CONTRIBUTING.md /setup.py: -------------------------------------------------------------------------------- 1 | # this file intentionally left blank for legacy tools to find 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/jupyter-server-proxy/HEAD/screenshot.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | recursive-include labextension * 3 | prune labextension/node_modules 4 | -------------------------------------------------------------------------------- /labextension/.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.egg-info/ 5 | .ipynb_checkpoints 6 | ../.idea/ 7 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | myst-parser 2 | sphinx-autobuild 3 | sphinx-book-theme 4 | sphinx-copybutton 5 | sphinxext-opengraph 6 | sphinxext-rediraffe 7 | -------------------------------------------------------------------------------- /docs/source/_static/images/logo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/jupyter-server-proxy/HEAD/docs/source/_static/images/logo/favicon.ico -------------------------------------------------------------------------------- /jupyter_server_proxy/etc/nbconfig/tree.d/jupyter-server-proxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "load_extensions": { 3 | "jupyter_server_proxy/tree": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /docs/source/_static/images/nbextension-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/jupyter-server-proxy/HEAD/docs/source/_static/images/nbextension-tree.png -------------------------------------------------------------------------------- /docs/source/_static/images/labextension-launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/jupyter-server-proxy/HEAD/docs/source/_static/images/labextension-launcher.png -------------------------------------------------------------------------------- /contrib/template/cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_name": "", 3 | "author_name": "Project Jupyter Contributors", 4 | "author_email": "projectjupyter@gmail.com" 5 | } 6 | -------------------------------------------------------------------------------- /tests/acceptance/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |((alpha|beta|rc)\.\d+)|) 181 | (?P(0\.dev)|) 182 | ''' 183 | 184 | [tool.tbump.git] 185 | message_template = "Bump to {new_version}" 186 | tag_template = "v{new_version}" 187 | 188 | [[tool.tbump.file]] 189 | src = "jupyter_server_proxy/_version.py" 190 | 191 | [[tool.tbump.file]] 192 | src = "pyproject.toml" 193 | 194 | [[tool.tbump.file]] 195 | src = "labextension/package.json" 196 | 197 | 198 | # pytest is used for running Python based tests 199 | # 200 | # ref: https://docs.pytest.org/en/stable/ 201 | # 202 | [tool.pytest.ini_options] 203 | addopts = [ 204 | "--verbose", 205 | "--durations=10", 206 | "--color=yes", 207 | "--cov=jupyter_server_proxy", 208 | "--cov-branch", 209 | "--cov-context=test", 210 | "--cov-report=term-missing:skip-covered", 211 | "--cov-report=html:build/coverage", 212 | "--no-cov-on-fail", 213 | "--html=build/pytest/index.html", 214 | ] 215 | asyncio_mode = "auto" 216 | testpaths = ["tests"] 217 | cache_dir = "build/.cache/pytest" 218 | 219 | 220 | # pytest-cov / coverage is used to measure code coverage of tests 221 | # 222 | # ref: https://coverage.readthedocs.io/en/stable/config.html 223 | # 224 | [tool.coverage.run] 225 | data_file = "build/.coverage" 226 | concurrency = [ 227 | "multiprocessing", 228 | "thread" 229 | ] 230 | 231 | [tool.coverage.html] 232 | show_contexts = true 233 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # This is a GitHub workflow defining a set of jobs with a set of steps. 2 | # ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions 3 | # 4 | name: Test 5 | 6 | on: 7 | pull_request: 8 | paths-ignore: 9 | - "docs/**" 10 | - "contrib/**" 11 | - "**.md" 12 | - ".github/workflows/*" 13 | - "!.github/workflows/test.yaml" 14 | push: 15 | paths-ignore: 16 | - "docs/**" 17 | - "contrib/**" 18 | - "**.md" 19 | - ".github/workflows/*" 20 | - "!.github/workflows/test.yaml" 21 | branches-ignore: 22 | - "dependabot/**" 23 | - "pre-commit-ci-update-config" 24 | schedule: 25 | # Run at 05:00 on monday and thursday, ref: https://crontab.guru/#0_5_*_*_1,4 26 | - cron: "0 5 * * 1,4" 27 | workflow_dispatch: 28 | 29 | env: 30 | # avoid warnings about config paths 31 | JUPYTER_PLATFORM_DIRS: "1" 32 | # avoid looking at every version of pip ever released 33 | PIP_DISABLE_PIP_VERSION_CHECK: "1" 34 | 35 | jobs: 36 | build: 37 | runs-on: ubuntu-22.04 38 | steps: 39 | - uses: actions/checkout@v6 40 | 41 | - uses: actions/setup-python@v6 42 | with: 43 | python-version: "3.12" 44 | 45 | - uses: actions/setup-node@v6 46 | with: 47 | cache: yarn 48 | node-version: "lts/*" 49 | registry-url: https://registry.npmjs.org 50 | cache-dependency-path: labextension/yarn.lock 51 | 52 | - name: Update root build packages 53 | run: pip install --upgrade build 54 | 55 | - name: Build Python package 56 | run: pyproject-build 57 | 58 | - name: Upload built artifacts 59 | uses: actions/upload-artifact@v5 60 | with: 61 | name: dist-${{ github.run_attempt }} 62 | path: ./dist 63 | 64 | test: 65 | name: ${{ matrix.os }} ${{ matrix.python-version }} ${{ matrix.pip-extras }} ${{ (matrix.pip-install-constraints != '' && '(oldest deps)') || '' }} 66 | needs: [build] 67 | timeout-minutes: 30 68 | runs-on: ${{ matrix.os }} 69 | defaults: 70 | run: 71 | shell: bash --noprofile --norc -eo pipefail {0} # windows default isn't bash 72 | 73 | strategy: 74 | fail-fast: false 75 | matrix: 76 | os: [ubuntu-22.04, windows-2022] 77 | python-version: ["3.8", "3.12"] 78 | pip-extras: [lab, classic] 79 | pip-install-constraints: [""] 80 | exclude: 81 | # windows should work for all test variations, but a limited selection 82 | # is run to avoid doubling the amount of test runs 83 | - os: windows-2022 84 | python-version: "3.12" 85 | pip-extras: classic 86 | - os: windows-2022 87 | python-version: "3.8" 88 | pip-extras: lab 89 | 90 | # pip-extras classic (notebook v6) isn't working with python 3.12 or 91 | # later, so we exclude it here and then include it below to run with 92 | # python 3.11 instead. 93 | - os: ubuntu-22.04 94 | python-version: "3.12" 95 | pip-extras: classic 96 | include: 97 | # Compensates for an excluded test case above 98 | - os: ubuntu-22.04 99 | python-version: "3.11" 100 | pip-extras: classic 101 | 102 | # this test is manually updated to reflect the lower bounds of 103 | # versions from dependencies 104 | - os: ubuntu-22.04 105 | python-version: "3.8" 106 | pip-extras: classic 107 | pip-install-constraints: >- 108 | jupyter-server==1.24.0 109 | simpervisor==1.0.0 110 | tornado==6.1.0 111 | traitlets==5.1.0 112 | 113 | steps: 114 | - uses: actions/checkout@v6 115 | 116 | - uses: actions/setup-python@v6 117 | with: 118 | python-version: "${{ matrix.python-version }}" 119 | 120 | - name: Update root build packages 121 | run: python -m pip install --upgrade pip 122 | 123 | - name: Download built artifacts 124 | uses: actions/download-artifact@v6 125 | with: 126 | name: dist-${{ github.run_attempt }} 127 | path: ./dist 128 | 129 | - name: Install Python package 130 | # NOTE: See CONTRIBUTING.md for a local development setup that differs 131 | # slightly from this. 132 | # 133 | # Pytest options are set in `pyproject.toml`. 134 | run: | 135 | pip install -vv $(ls ./dist/*.whl)\[acceptance,${{ matrix.pip-extras }}\] ${{ matrix.pip-install-constraints }} 136 | 137 | - name: List Python packages 138 | run: | 139 | pip freeze 140 | pip check 141 | 142 | - name: Check server extension for jupyter_server 143 | run: | 144 | jupyter server extension list 145 | jupyter server extension list 2>&1 | grep -iE "jupyter_server_proxy.*OK" - 146 | 147 | - name: Check server extension for notebook v6 148 | if: contains(matrix.pip-extras, 'classic') 149 | run: | 150 | jupyter serverextension list 151 | jupyter serverextension list 2>&1 | grep -iE "jupyter_server_proxy.*OK" - 152 | 153 | - name: Check frontend extension for notebook v6 154 | if: contains(matrix.pip-extras, 'classic') 155 | run: | 156 | jupyter nbextension list 157 | PYTHONUNBUFFERED=1 jupyter nbextension list 2>&1 | grep -A1 -iE '.*jupyter_server_proxy.*enabled' | grep -B1 -iE "Validating.*OK" 158 | 159 | - name: Check frontend extension for notebook v7+ 160 | if: ${{ !contains(matrix.pip-extras, 'classic') }} 161 | run: | 162 | jupyter notebook extension list 163 | jupyter notebook extension list 2>&1 | grep -iE 'jupyter_server_proxy.*OK.*' 164 | 165 | - name: Check frontend extension for jupyterlab 166 | run: | 167 | jupyter lab extension list 168 | jupyter lab extension list 2>&1 | grep -iE 'jupyter_server_proxy.*OK.*' 169 | 170 | # we have installed a pre-built wheel and configured code coverage to 171 | # inspect "jupyter_server_proxy", by re-locating to another directory, 172 | # there is no confusion about "jupyter_server_proxy" referring to our 173 | # installed package rather than the local directory 174 | - name: Run tests 175 | run: | 176 | mkdir build 177 | cd build 178 | pytest -c ../pyproject.toml ../tests 179 | 180 | - name: Upload test reports 181 | if: always() 182 | uses: actions/upload-artifact@v5 183 | with: 184 | name: |- 185 | tests-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.pip-extras }}-${{ (matrix.pip-install-constraints != '' && 'oldest-') || '' }}${{ github.run_attempt }} 186 | path: | 187 | ./build/pytest 188 | ./build/coverage 189 | ./build/robot 190 | 191 | # GitHub action reference: https://github.com/codecov/codecov-action 192 | - uses: codecov/codecov-action@v5 193 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Welcome! As a [Jupyter](https://jupyter.org) project, we follow the [Jupyter contributor guide](https://jupyter.readthedocs.io/en/latest/contributor/content-contributor.html). 4 | 5 | To setup a local development environment and run tests, see the small section in 6 | the README.md file. 7 | 8 | ## Local development setup 9 | 10 | ### `conda` 11 | 12 | 13 | 14 |33 | 34 | ### Python package 35 | 36 | ```bash 37 | # Clone the repo to your local environment 38 | git clone https://github.com/jupyterhub/jupyter-server-proxy.git 39 | # Change directory to the jupyter-server-proxy directory 40 | cd jupyter-server-proxy 41 | # Install package in development mode, with the latest Jupyter clients 42 | pip install -e ".[test,lab]" 43 | # Link your development version of the extension with JupyterLab and Notebook 44 | jupyter labextension develop --overwrite . 45 | # Server extension must be manually installed in develop mode 46 | jupyter server extension enable jupyter_server_proxy 47 | ``` 48 | 49 | ## Testing 50 | 51 | Run the tests: 52 | 53 | ```bash 54 | pytest 55 | ``` 56 | 57 | These generate test and coverage reports in `build/pytest` and `build/coverage`. 58 | 59 | ### Acceptance tests 60 | 61 | In `tests/acceptance`, a number of 62 | [`.robot` files](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html) 63 | emulate a user starting a Jupyter server, opening a real browser, clicking on 64 | screen elements, and seeing several working proxy servers in supported Jupyter clients. 65 | 66 | These tests are slower and more resource intensive than the unit and integration 67 | tests, and generate additional screenshots, browser logs, server logs, and report 68 | HTML in `build/robot`. 69 | 70 | #### Extra browser dependencies 71 | 72 | Compatible versions of [`geckodriver`](https://github.com/mozilla/geckodriver) 73 | and [`firefox`](https://www.mozilla.org/en-US/firefox) need to be on `$PATH`. 74 | 75 | These can be provisioned by [a `conda`-compatible installer](#conda), a system 76 | package manager, or as a last resort, direct binary downloads. 77 | 78 | #### Acceptance test dependencies 79 | 80 | To install the additional dependencies beyond the [Python package](#python-package) 81 | test dependencies, and run the tests against the latest Jupyter clients: 82 | 83 | ```bash 84 | pip install -e ".[test,acceptance,lab]" 85 | pytest 86 | ``` 87 | 88 | To run _only_ the acceptance tests, use the `-k` switch: 89 | 90 | ```bash 91 | pytest -k acceptance 92 | ``` 93 | 94 | #### Older Jupyter Clients 95 | 96 | To run the acceptance tests against the previous major versions of Notebook 97 | and JupyterLab, it is advisable to use a separate, isolated environment, testing the 98 | as-built assets from `pyproject-build`. 99 | 100 | After creating and activating such an environment with `virtualenv` or [`conda`](#conda): 101 | 102 | ```bash 103 | pip install --find-links ./dist/ --no-index-url jupyter-server-proxy[test,acceptance,classic] 104 | ``` 105 | 106 | ## Frontend Development 107 | 108 | To support a wide range of clients, both JupyterLab and Notebook Classic extensions 109 | are built and distributed, each with their own quirks. 110 | 111 | ### JupyterLab/Notebook extension 112 | 113 | The `./labextension/` directory contains the extension for the 114 | [`lumino`](https://github.com/jupyterlab/lumino/)-based JupyterLab and Notebook 115 | clients. 116 | 117 | #### `nodejs` 118 | 119 | Building this extension requires a compatible version of 120 | [`nodejs`](https://nodejs.org/en/download/package-manager), with a supported, long 121 | term support (LTS) release recommended. 122 | 123 | #### `jlpm` 124 | 125 | The `jlpm` command is a vendored, pinned version of the [`yarn`](https://yarnpkg.com) 126 | package manager. Installed with JupyterLab, it performs commands such 127 | as installing `npm` dependencies listed in `labextension/package.json`, building 128 | and watching the extension from source, and formatting web-related source code files. 129 | 130 | #### The built Lab extension 131 | 132 | During a [`pyproject-build`](https://pypi.org/project/build/) 133 | of the python package, a temporary JupyterLab and `jlpm` will be installed as part 134 | of the `build-system`, executing roughly the commands: 135 | 136 | ```bash 137 | cd labextension # Change to the root of the labextension 138 | jlpm # Install dependencies 139 | jlpm build:prod # Build: 140 | # - `labextension/lib` with type checking 141 | # - `jupyter_server_proxy/labextension` with minimization 142 | ``` 143 | 144 | During `pip install`, the built assets are copied to the user's 145 | `{sys.prefix}/share/jupyter/labextensions/@jupyterhub/jupyter-server-proxy` to be 146 | found by the application at startup. 147 | 148 | #### Developing the Lab extension 149 | 150 | For fine-grained access to the `jlpm` command and various build steps: 151 | 152 | ```bash 153 | pip install -e .[lab] # Ensure a compatible jlpm 154 | cd labextension # Change to the root of the labextension 155 | jlpm 156 | jlpm install:extension # Symlink into `{sys.prefix}/share/jupyter/labextensions` 157 | ``` 158 | 159 | Watch the source directory and automatically rebuild the `labextension/lib` 160 | and `jupyter_server_proxy/labextension` folders: 161 | 162 | ```bash 163 | cd labextension 164 | # Watch the source directory in one terminal, automatically rebuilding when needed 165 | jlpm watch 166 | # Run JupyterLab in another terminal 167 | jupyter lab 168 | ``` 169 | 170 | While running `jlpm watch`, every saved change to a `.ts` file will immediately be 171 | built locally and available in your running Jupyter client. "Hard" refresh JupyterLab or Notebook 172 | with CTRL-F5 or ⌘-F5 to load the change in your browser 173 | (you may need to wait several seconds for the extension to be fully rebuilt). 174 | 175 | #### Source Maps 176 | 177 | By default, the `jlpm build` and `jlpm watch` commands generate 178 | [source maps](https://firefox-source-docs.mozilla.org/devtools-user/debugger/how_to/use_a_source_map/) 179 | for this extension to improve debugging using the browser development tools, 180 | often revealed by pressing F12. 181 | 182 | To also generate source maps for the JupyterLab core application, run the following command: 183 | 184 | ```bash 185 | jupyter lab build --minimize=False 186 | ``` 187 | 188 | ### Notebook Classic extension 189 | 190 | The files in `jupyter_server_proxy/static` extend the Notebook Classic application's 191 | _Tree_ page. 192 | 193 | #### RequireJS 194 | 195 | The Notebook Classic extension uses the [`require.js`](https://requirejs.org) 196 | dependency injection system, and presently uses no dependencies beyond what is 197 | provided by Notebook Classic. 198 | 199 | #### The built Classic extension 200 | 201 | During a user's `pip install`, the static assets are copied to 202 | `{sys.prefix}/share/jupyter/nbextensions/jupyter_server_proxy`, to be 203 | found by the application at startup. 204 | 205 | #### Developing the Classic extension 206 | 207 | While this extension is served as-is once installed, for live development the 208 | extension assets must be linked: 209 | 210 | ```bash 211 | pip install -e ".[classic]" 212 | jupyter nbextension install --symlink --sys-prefix --py jupyter_server_proxy 213 | ``` 214 | 215 | After making changes, "hard" refresh the browser application, usually with 216 | CTRL-F5 or ⌘-F5. 217 | 218 | ## Documentation 219 | 220 | The documentation uses a fairly standard [Sphinx](https://www.sphinx-doc.org) 221 | build chain, and requires `make` on Linux/MacOS, which cannot be installed with 222 | `pip` 223 | 224 | > `make` is available from [`conda-forge`](#conda) as `make` for Linux/OSX, and `m2-make` 225 | > on Windows 226 | 227 | In addition to any system packages, building the documentation requires 228 | additional packages. To install the needed packages: 229 | 230 | ```bash 231 | pip install -r docs/requirements.txt 232 | ``` 233 | 234 | Once installed, enter the docs folder with: 235 | 236 | ```bash 237 | cd docs 238 | ``` 239 | 240 | ... then build the HTML site: 241 | 242 | ```bash 243 | make 244 | ``` 245 | 246 | ... or check that all hyperlinks can be resolved: 247 | 248 | ```bash 249 | make linkcheck 250 | ``` 251 | 252 | ... or start an auto-reloading server and open a web browser: 253 | 254 | ```bash 255 | make devenv 256 | ``` 257 | 258 | ## Linting 259 | 260 | During continuous integration (CI) the `pre-commit` package is used to run a 261 | number of checks, with each tool in a private virtual environment. If it is able to, 262 | the CI bot will push to a PR with fixes. 263 | 264 | By installing `pre-commit` with `pip` or `conda`, you can have this same experience, 265 | or inspect the configuration to try to recreate it yourself locally. 266 | -------------------------------------------------------------------------------- /labextension/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The main extension file, which provides plugins that can be loaded into 3 | * a Lumino application such as JupyterLab or Notebook 7. 4 | * 5 | * Outside of this file, of note is `./tokens.ts`, which provides a number of 6 | * run-time constants and compile-type interfaces for type-checking. 7 | * 8 | * Imports (mostly) adhere to the ES `import` semantics, exceptions noted below 9 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import 10 | */ 11 | 12 | // this is a type-only import for low-level components of the UI 13 | // @see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html 14 | import type { 15 | // the base class for all on-screen components 16 | Widget, 17 | // the Lumino component for menu bars 18 | Menu, 19 | } from "@lumino/widgets"; 20 | 21 | import { 22 | // the dependency-injection (DI) Token required to request a dependency for... 23 | ILayoutRestorer, 24 | // any lumino-based Jupyter application... 25 | JupyterFrontEnd, 26 | // to be extended with... 27 | JupyterFrontEndPlugin, 28 | } from "@jupyterlab/application"; 29 | import { 30 | // a DI token for the toolbar, used to extend Notebook's Tree 31 | IToolbarWidgetRegistry, 32 | // the concrete class used to create iframes in the JupyterLab main area 33 | IFrame, 34 | // a wrapper for the iframe that handles house-keeping boilerplate 35 | MainAreaWidget, 36 | // a tracker which handles iframe placement housekeeping when JupyterLab is reloaded 37 | WidgetTracker, 38 | } from "@jupyterlab/apputils"; 39 | // utilities for working with URLs and the `jupyter-config-data` script 40 | import { PageConfig, URLExt } from "@jupyterlab/coreutils"; 41 | // a DI token for the file browser, present in JupyterLab and Notebook 7 42 | import { IDefaultFileBrowser } from "@jupyterlab/filebrowser"; 43 | // a DI token for the card-based Launcher, present in JupyterLab 44 | import { ILauncher } from "@jupyterlab/launcher"; 45 | // the application-wide configuration for making HTTP requests with headers, etc. 46 | import { ServerConnection } from "@jupyterlab/services"; 47 | 48 | // local imports from `tokens.ts` for immutable constants 49 | import { 50 | CommandIDs, 51 | IOpenArgs, 52 | IServerProcess, 53 | IServersInfo, 54 | NAME, 55 | NS, 56 | argSchema, 57 | sandbox, 58 | } from "./tokens"; 59 | 60 | /* 61 | * top level constants that won't change during the application lifecycle 62 | */ 63 | const baseUrl = PageConfig.getBaseUrl(); 64 | /** 65 | * The Notebook 7 sub-application: `tree`, `notebook`, `editor`, `terminal`, etc. 66 | */ 67 | const notebookPage = PageConfig.getOption("notebookPage"); 68 | /** 69 | * Whether the current application is `/tree`: otherwise we don't do anything 70 | */ 71 | const isTree = notebookPage === "tree"; 72 | /** 73 | * Whether this is a notebook app at all 74 | */ 75 | const isNotebook7 = !!notebookPage; 76 | 77 | /** 78 | * Data to register the extension with jupyterlab which also clarifies the shape of 79 | * what is required by the extension and passed to our provided activate function. 80 | * 81 | * This plugin is `export`ed at the end of the file. 82 | * 83 | * @see https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html#application-plugins 84 | */ 85 | const plugin: JupyterFrontEndPlugin15 | 16 | Optional, but especially recommended on non-Linux platforms... 17 | 18 |
19 | 20 | Using the `conda` (or `mamba` or `micromamba`) package manager with packages from 21 | [`conda-forge`](https://conda-forge.org/feedstock-outputs) can help isolate development 22 | environments on nearly any operating system and architecture. 23 | 24 | For example, after installing [`mambaforge`](https://conda-forge.org/miniforge), 25 | create a new environment with all heavy development and test dependencies: 26 | 27 | ```yaml 28 | mamba create --name=jupyter-server-proxy --channel=conda-forge "python=3.12" "nodejs=20" pip git geckodriver firefox 29 | mamba activate jupyter-server-proxy 30 | ``` 31 | 32 |= { 86 | id: `${NAME}:add-launcher-entries`, 87 | autoStart: true, 88 | // to support JupyterLab and Notebook, we don't _require_ any other DI tokens... 89 | requires: [], 90 | // ... but some decisions will be made on the presence of these optional DI tokens, 91 | // which will be given to `activate` in the order listed here 92 | optional: [ 93 | ILauncher, 94 | ILayoutRestorer, 95 | IToolbarWidgetRegistry, 96 | IDefaultFileBrowser, 97 | ], 98 | activate, 99 | }; 100 | 101 | /** 102 | * The activate function is registered to be called on activation of the 103 | * JupyterLab/Notebook 7 plugin. 104 | * 105 | * @see https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html 106 | */ 107 | async function activate( 108 | app: JupyterFrontEnd, 109 | // `requires` would have show up here with concrete types ... 110 | // ... but all `optional` DI tokens _might_ be `null` 111 | launcher: ILauncher | null, 112 | restorer: ILayoutRestorer | null, 113 | toolbarRegistry: IToolbarWidgetRegistry | null, 114 | fileBrowser: IDefaultFileBrowser | null, 115 | ): Promise { 116 | // when viewing `/notebook`, `/terminal` or `/editor`, bail as early as possible 117 | if (isNotebook7 && !isTree) { 118 | return; 119 | } 120 | 121 | // server connection settings (such as headers) _can't_ be a global, as they 122 | // can potentially be configured by other extensions 123 | const serverSettings = ServerConnection.makeSettings(); 124 | 125 | // Fetch configured server processes from {base_url}/server-proxy/servers-info 126 | // TODO: consider moving this to a separate plugin 127 | // TODO: consider not blocking the application load 128 | const url = URLExt.join(baseUrl, `${NS}/servers-info`); 129 | const response = await ServerConnection.makeRequest(url, {}, serverSettings); 130 | 131 | if (!response.ok) { 132 | console.warn( 133 | "Could not fetch metadata about registered servers. Make sure jupyter-server-proxy is installed.", 134 | ); 135 | console.warn(response); 136 | return; 137 | } 138 | 139 | // load and trust the JSON as a type of data described by the `IServersInfo` interface 140 | // TODO: consider adding JSON schema-derived types 141 | const data = (await response.json()) as IServersInfo; 142 | 143 | // handle restoring persistent JupyterLab workspace widgets on page reload 144 | // this is created even in the Notebook `tree` page to reduce complexity below 145 | const tracker = new WidgetTracker >({ namespace: NS }); 146 | if (restorer) { 147 | void restorer.restore(tracker, { 148 | command: CommandIDs.open, 149 | args: (widget) => ({ 150 | url: widget.content.url, 151 | title: widget.content.title.label, 152 | newBrowserTab: false, 153 | id: widget.content.id, 154 | }), 155 | name: (widget) => widget.content.id, 156 | }); 157 | } 158 | 159 | // register commands 160 | // commands provide "loose" coupling, based on well-known strings and JSON-like 161 | // structures instead of heavy DI tokens 162 | const { commands, shell } = app; 163 | commands.addCommand(CommandIDs.open, { 164 | label: (args) => (args as IOpenArgs).title, 165 | describedBy: async () => { 166 | return { args: argSchema }; 167 | }, 168 | execute: (args) => { 169 | // the syntax below is an example of "destructuring" 170 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment 171 | const { id, title, url, newBrowserTab } = args as IOpenArgs; 172 | if (newBrowserTab) { 173 | window.open(url, "_blank"); 174 | return; 175 | } 176 | let widget = tracker.find((widget) => widget.content.id === id); 177 | if (!widget) { 178 | widget = newServerProxyWidget(id, url, title); 179 | } 180 | if (!tracker.has(widget)) { 181 | void tracker.add(widget); 182 | } 183 | if (!widget.isAttached) { 184 | shell.add(widget); 185 | } else { 186 | shell.activateById(widget.id); 187 | } 188 | return widget; 189 | }, 190 | }); 191 | 192 | // handle adding JupyterLab launcher cards 193 | // TODO: consider moving this to a separate plugin (keeping this ID) 194 | if (launcher) { 195 | for (let server_process of data.server_processes) { 196 | const { launcher_entry } = server_process; 197 | 198 | if (!launcher_entry.enabled) { 199 | continue; 200 | } 201 | 202 | launcher.add({ 203 | command: CommandIDs.open, 204 | args: argsForServer(server_process), 205 | category: launcher_entry.category, 206 | kernelIconUrl: launcher_entry.icon_url || void 0, 207 | }); 208 | } 209 | } 210 | 211 | // handle adding servers menu items to the Notebook 7 _Tree_ toolbar 212 | // TODO: consider moving this to a separate plugin 213 | if (isTree && toolbarRegistry && fileBrowser) { 214 | const { toolbar } = fileBrowser; 215 | const widgets = ((toolbar.layout || {}) as any).widgets as Widget[]; 216 | if (widgets && widgets.length) { 217 | for (const widget of widgets) { 218 | if (widget && (widget as any).menus) { 219 | // simple DOM queries can't be used, as there is no guarantee it is 220 | // attached yet 221 | const menu: Menu = (widget as any).menus[0]; 222 | menu.addItem({ type: "separator" }); 223 | for (const server_process of data.server_processes) { 224 | // create args, overriding all to launch in new heavyweight browser tabs 225 | let args = { 226 | ...argsForServer(server_process), 227 | newBrowserTab: true, 228 | }; 229 | menu.addItem({ command: CommandIDs.open, args }); 230 | } 231 | } 232 | } 233 | } 234 | } 235 | } 236 | 237 | /** 238 | * Create a new `iframe`, with a wrapper for including in the main area. 239 | */ 240 | function newServerProxyWidget( 241 | id: string, 242 | url: string, 243 | text: string, 244 | ): MainAreaWidget