diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f07e727..f08878c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,8 @@ jobs: - name: Install the extension run: | set -eux - python -m pip install "jupyterlab>=4.0.0,<5" jupyterlab_nebari_mode*.whl + WHEEL=$(ls jupyterlab_nebari_mode*.whl) + python -m pip install "jupyterlab>=4.0.0,<5" "$WHEEL[integration-test]" - name: Install dependencies working-directory: ui-tests diff --git a/.github/workflows/update-integration-tests.yml b/.github/workflows/update-integration-tests.yml index 7ea5a66..cddbfac 100644 --- a/.github/workflows/update-integration-tests.yml +++ b/.github/workflows/update-integration-tests.yml @@ -40,7 +40,7 @@ jobs: run: | set -eux jlpm - python -m pip install . + python -m pip install .[integration-test] - uses: jupyterlab/maintainer-tools/.github/actions/update-snapshots@v1 with: diff --git a/README.md b/README.md index e1dc383..f65c8a1 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,16 @@ Nebari customizations for JupyterLab. - `jupyterlab-nebari-mode:logo`: replaces `@jupyterlab/application-extension:logo`, adding clickable Nebari logo: ![](https://raw.githubusercontent.com/nebari-dev/jupyterlab-nebari-mode/main/ui-tests/tests/jupyterlab_nebari_mode.spec.ts-snapshots/top-panel-linux.png) +- `jupyterlab-nebari-mode:commands` adds `nebari:open-proxy` command for opening proxied processes, such as VSCode. This command can be used to add a menu entry, e.g.: + ```json + { + "command": "nebari:open-proxy", + "rank": 1, + "args": { + "name": "vscode" + } + } + ``` ## Requirements diff --git a/binder/environment.yml b/binder/environment.yml index 5b26c8a..89acfe3 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -17,5 +17,6 @@ dependencies: - nodejs >=18,<19 - pip - wheel - # additional packages for demos - # - ipywidgets + - pip: + - jupyter-server-proxy + - jupyter-vscode-proxy diff --git a/binder/overrides.json b/binder/overrides.json new file mode 100644 index 0000000..24846e9 --- /dev/null +++ b/binder/overrides.json @@ -0,0 +1,21 @@ +{ + "@jupyterlab/mainmenu-extension:plugin": { + "menus": [ + { + "id": "jp-mainmenu-services", + "disabled": false, + "label": "Services", + "rank": 1000, + "items": [ + { + "command": "nebari:open-proxy", + "rank": 1, + "args": { + "name": "vscode" + } + } + ] + } + ] + } +} diff --git a/binder/postBuild b/binder/postBuild index 0b72431..15f0cf1 100755 --- a/binder/postBuild +++ b/binder/postBuild @@ -8,6 +8,7 @@ python3 binder/postBuild """ +import os import subprocess import sys from pathlib import Path @@ -42,6 +43,10 @@ _("jupyter", "server", "extension", "list") # initially list installed extensions to determine if there are any surprises _("jupyter", "labextension", "list") +# copy overrides.json to right location +NB_PYTHON_PREFIX = os.environ["NB_PYTHON_PREFIX"] +_("mkdir", "-p", f"{NB_PYTHON_PREFIX}/share/jupyter/lab/settings/") +_("cp", "binder/overrides.json", f"{NB_PYTHON_PREFIX}/share/jupyter/lab/settings/") print("JupyterLab with jupyterlab_nebari_mode is ready to run with:\n") print("\tjupyter lab\n") diff --git a/package.json b/package.json index 38adaec..ecf1f24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jupyterlab-nebari-mode", - "version": "0.1.0", + "version": "0.2.0", "description": "Nebari customizations for JupyterLab.", "keywords": [ "jupyter", @@ -59,6 +59,7 @@ "@jupyterlab/application": "^4.0.0" }, "devDependencies": { + "@jupyterhub/jupyter-server-proxy": "^4.1.0", "@jupyterlab/builder": "^4.0.0", "@jupyterlab/testutils": "^4.0.0", "@types/jest": "^29.2.0", diff --git a/pyproject.toml b/pyproject.toml index 65113fd..c2b17a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,12 @@ dependencies = [ ] dynamic = ["version", "description", "authors", "urls", "keywords"] +[project.optional-dependencies] +integration-test = [ + "jupyter-server-proxy", + "jupyter-vscode-proxy" +] + [tool.hatch.version] source = "nodejs" diff --git a/src/index.ts b/src/index.ts index e01633c..36bdf8b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,11 @@ +import type { IServersInfo } from '@jupyterhub/jupyter-server-proxy/lib/tokens'; import { ILabShell, JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; -import { URLExt } from '@jupyterlab/coreutils'; +import { ServerConnection } from '@jupyterlab/services'; +import { PageConfig, URLExt } from '@jupyterlab/coreutils'; import { Widget } from '@lumino/widgets'; import { nebariIcon } from './icons'; @@ -47,9 +49,82 @@ class NebariLogo extends Widget { } } -/** - * Initialization data for the jupyterlab-nebari-mode extension. - */ +namespace CommandIDs { + /** + * Opens a process proxied by jupyter-server-proxy (such as VSCode). + */ + export const openProxy = 'nebari:open-proxy'; +} + +interface IOpenProxyArgs { + /** + * Name of the server process to open. + */ + name?: string; +} + +const commandsPlugin: JupyterFrontEndPlugin = { + id: 'jupyterlab-nebari-mode:commands', + description: 'Adds additional commands used by nebari.', + autoStart: true, + requires: [], + activate: async (app: JupyterFrontEnd) => { + const serverSettings = ServerConnection.makeSettings(); + const baseUrl = PageConfig.getBaseUrl(); + const url = URLExt.join(baseUrl, 'server-proxy/servers-info'); + const response = await ServerConnection.makeRequest( + url, + {}, + serverSettings + ); + if (!response.ok) { + console.warn('Server proxy info not available'); + return; + } + const data = (await response.json()) as IServersInfo; + + const findProcess = (name?: string) => { + if (!name) { + return null; + } + const matches = data.server_processes.filter( + process => process.name === name + ); + if (matches.length === 0) { + return null; + } + return matches[0]; + }; + + app.commands.addCommand(CommandIDs.openProxy, { + execute: (args: IOpenProxyArgs) => { + const processs = findProcess(args.name); + if (!processs) { + throw Error(`Process for ${args.name} not found`); + } + + const url = `${baseUrl}${processs.launcher_entry.path_info}`; + + if (processs.new_browser_tab) { + window.open(url, '_blank', 'noopener,noreferrer'); + } else { + window.location.href = url; + } + }, + isEnabled: (args: IOpenProxyArgs) => { + const processs = findProcess(args.name); + return !!processs; + }, + label: (args: IOpenProxyArgs) => { + const processs = findProcess(args.name); + return processs + ? `Open ${processs.launcher_entry.title}` + : 'Open Proxied Process'; + } + }); + } +}; + const logoPlugin: JupyterFrontEndPlugin = { id: 'jupyterlab-nebari-mode:logo', description: 'Sets the application logo.', @@ -65,4 +140,6 @@ const logoPlugin: JupyterFrontEndPlugin = { } }; -export default logoPlugin; +const plugins = [commandsPlugin, logoPlugin]; + +export default plugins; diff --git a/ui-tests/tests/jupyterlab_nebari_mode.spec.ts b/ui-tests/tests/jupyterlab_nebari_mode.spec.ts index 86520f5..d29f795 100644 --- a/ui-tests/tests/jupyterlab_nebari_mode.spec.ts +++ b/ui-tests/tests/jupyterlab_nebari_mode.spec.ts @@ -26,3 +26,23 @@ test('should swap Jupyter logo with clickable Nebari logo', async ({ await link.focus(); expect(await link.screenshot()).toMatchSnapshot('nebari-logo-focus.png'); }); + +test('should register custom commands', async ({ page }) => { + const openVScodeProxy = await page.evaluate(async () => { + const registry = window.jupyterapp.commands; + const id = 'nebari:open-proxy'; + const args = { name: 'vscode' }; + + return { + id, + label: registry.label(id, args), + isEnabled: registry.isEnabled(id, args) + }; + }); + + // Should set correct label for given command + expect(openVScodeProxy.label).toBe('Open VS Code'); + + // Should be enabled when `jupyter-vscode-proxy` is installed + expect(openVScodeProxy.isEnabled).toBe(true); +}); diff --git a/yarn.lock b/yarn.lock index 63f753f..1d94784 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2092,7 +2092,18 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/application@npm:^4.0.0, @jupyterlab/application@npm:^4.1.2": +"@jupyterhub/jupyter-server-proxy@npm:^4.1.0": + version: 4.1.0 + resolution: "@jupyterhub/jupyter-server-proxy@npm:4.1.0" + dependencies: + "@jupyterlab/application": ^3.0 || ^4.0 + "@jupyterlab/filebrowser": ^3.0 || ^4.0 + "@jupyterlab/launcher": ^3.0 || ^4.0 + checksum: d8006409ff0ca6029f00c7255447e052957bb4fa03ca02a1441ef277b128dde4f3d095c1f3ef920bb8e27c91386812bf81c3894b9cdac61b7dd9b70a616586df + languageName: node + linkType: hard + +"@jupyterlab/application@npm:^3.0 || ^4.0, @jupyterlab/application@npm:^4.0.0, @jupyterlab/application@npm:^4.1.2": version: 4.1.2 resolution: "@jupyterlab/application@npm:4.1.2" dependencies: @@ -2388,7 +2399,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/filebrowser@npm:^4.1.2": +"@jupyterlab/filebrowser@npm:^3.0 || ^4.0, @jupyterlab/filebrowser@npm:^4.1.2": version: 4.1.2 resolution: "@jupyterlab/filebrowser@npm:4.1.2" dependencies: @@ -2416,6 +2427,24 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/launcher@npm:^3.0 || ^4.0": + version: 4.1.2 + resolution: "@jupyterlab/launcher@npm:4.1.2" + dependencies: + "@jupyterlab/apputils": ^4.2.2 + "@jupyterlab/translation": ^4.1.2 + "@jupyterlab/ui-components": ^4.1.2 + "@lumino/algorithm": ^2.0.1 + "@lumino/commands": ^2.2.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/properties": ^2.0.1 + "@lumino/widgets": ^2.3.1 + react: ^18.2.0 + checksum: bc6f4fb53bf273e28b94b41ec9c87e7f2747053f3266519e1c2472dcc42636d10a80ecc99aaaa20d15ecd77230307dbba7696536177b01fb4631588ffa8bba53 + languageName: node + linkType: hard + "@jupyterlab/lsp@npm:^4.1.2": version: 4.1.2 resolution: "@jupyterlab/lsp@npm:4.1.2" @@ -7199,6 +7228,7 @@ __metadata: version: 0.0.0-use.local resolution: "jupyterlab-nebari-mode@workspace:." dependencies: + "@jupyterhub/jupyter-server-proxy": ^4.1.0 "@jupyterlab/application": ^4.0.0 "@jupyterlab/builder": ^4.0.0 "@jupyterlab/testutils": ^4.0.0