From adf0c3ef481f5a0c01cbc98d0bfa6586eb6e905e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Krassowski?= <5832902+krassowski@users.noreply.github.com> Date: Wed, 20 Mar 2024 17:27:08 +0000 Subject: [PATCH] Add run first enabled (#2) * Add new `nebari:run-first-enabled` command * Bump version to 0.3.0 --- README.md | 40 +++++++-- package.json | 2 +- src/index.ts | 72 +++++++++++++++ ui-tests/tests/jupyterlab_nebari_mode.spec.ts | 89 +++++++++++++++---- 4 files changed, 178 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index f65c8a1..ca58847 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,38 @@ 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" +- `jupyterlab-nebari-mode:commands` adds the following commands: + - `nebari:open-proxy` which opens 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" + } } - } - ``` + ``` + - `nebari:run-first-enabled` which runs the first available and enabled command; it differs from the built-in `apputils:run-first-enabled` command in that it takes a list of objects representing the commands, allowing to customise the `label`, `iconClass`, `caption`, `usage`, and `className` properties. An example usage for menu customization would be adding a menu entry labelled `Import numpy in File Editor` when user has the File Editor open and `Import numpy in Notebook` when user has a Notebook open: + ```json + { + "command": "nebari:run-first-enabled", + "rank": 1, + "args": { + "commands": [ + { + "id": "fileeditor:replace-selection", + "label": "Import numpy in File Editor", + "args": { "text": "import numpy as np" } + }, + { + "id": "notebook:replace-selection", + "label": "Import numpy in Notebook", + "args": { "text": "import numpy as np" } + } + ] + } + } + ``` ## Requirements diff --git a/package.json b/package.json index ecf1f24..08aec2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jupyterlab-nebari-mode", - "version": "0.2.0", + "version": "0.3.0", "description": "Nebari customizations for JupyterLab.", "keywords": [ "jupyter", diff --git a/src/index.ts b/src/index.ts index 36bdf8b..ef1da62 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { import { ServerConnection } from '@jupyterlab/services'; import { PageConfig, URLExt } from '@jupyterlab/coreutils'; import { Widget } from '@lumino/widgets'; +import { CommandRegistry } from '@lumino/commands'; import { nebariIcon } from './icons'; class NebariLogo extends Widget { @@ -54,6 +55,11 @@ namespace CommandIDs { * Opens a process proxied by jupyter-server-proxy (such as VSCode). */ export const openProxy = 'nebari:open-proxy'; + /** + * Run first enabled command, similar to `apputils:run-first-enabled`, + * but assumes all properties of the first enabled command. + */ + export const runFirstEnabled = 'nebari:run-first-enabled'; } interface IOpenProxyArgs { @@ -63,6 +69,25 @@ interface IOpenProxyArgs { name?: string; } +interface ICommandDescription + extends Omit { + /** + * The identifier of the command. + */ + id: string; + /** + * The arguments for the command. + */ + args: any; +} + +interface IRunFirstEnabledArgs { + /** + * Name of the server process to open. + */ + commands?: ICommandDescription[]; +} + const commandsPlugin: JupyterFrontEndPlugin = { id: 'jupyterlab-nebari-mode:commands', description: 'Adds additional commands used by nebari.', @@ -122,6 +147,53 @@ const commandsPlugin: JupyterFrontEndPlugin = { : 'Open Proxied Process'; } }); + + const returnFirstEnabled = ( + args: IRunFirstEnabledArgs, + method: 'label' | 'iconClass' | 'caption' | 'usage' | 'className' + ): string | undefined => { + const commands = args.commands ?? []; + for (const command of commands) { + if ( + app.commands.hasCommand(command.id) && + app.commands.isEnabled(command.id, command.args) + ) { + return ( + (command[method] as string | undefined) ?? + app.commands[method](command.id, command.args) + ); + } + } + }; + + app.commands.addCommand(CommandIDs.runFirstEnabled, { + execute: (args: IRunFirstEnabledArgs) => { + const commands = args.commands ?? []; + for (const command of commands) { + if ( + app.commands.hasCommand(command.id) && + app.commands.isEnabled(command.id, command.args) + ) { + return app.commands.execute(command.id, command.args); + } + } + }, + label: (args: IRunFirstEnabledArgs) => { + return returnFirstEnabled(args, 'label') ?? 'Run First Enabled'; + }, + iconClass: (args: IRunFirstEnabledArgs) => { + return returnFirstEnabled(args, 'iconClass') ?? ''; + }, + caption: (args: IRunFirstEnabledArgs) => { + return returnFirstEnabled(args, 'caption') ?? ''; + }, + usage: (args: IRunFirstEnabledArgs) => { + return returnFirstEnabled(args, 'usage') ?? ''; + }, + className: (args: IRunFirstEnabledArgs) => { + return returnFirstEnabled(args, 'className') ?? ''; + } + }); } }; diff --git a/ui-tests/tests/jupyterlab_nebari_mode.spec.ts b/ui-tests/tests/jupyterlab_nebari_mode.spec.ts index d29f795..68045dc 100644 --- a/ui-tests/tests/jupyterlab_nebari_mode.spec.ts +++ b/ui-tests/tests/jupyterlab_nebari_mode.spec.ts @@ -27,22 +27,81 @@ test('should swap Jupyter logo with clickable Nebari logo', async ({ 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) - }; +test.describe('should register custom commands', () => { + test('nebari:open-proxy command works', 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); }); - // Should set correct label for given command - expect(openVScodeProxy.label).toBe('Open VS Code'); + test('nebari:run-first-enabled command returns default label of first enabled command', async ({ + page + }) => { + const runFirstEnabled = await page.evaluate(async () => { + const registry = window.jupyterapp.commands; + const id = 'nebari:run-first-enabled'; + const args = { + commands: [ + { + id: 'this-command-does-not-exist' + }, + { + id: 'nebari:open-proxy', + args: { name: 'vscode' } + } + ] + }; - // Should be enabled when `jupyter-vscode-proxy` is installed - expect(openVScodeProxy.isEnabled).toBe(true); + return { + id, + label: registry.label(id, args), + isEnabled: registry.isEnabled(id, args) + }; + }); + // Should set correct label for given command + expect(runFirstEnabled.label).toBe('Open VS Code'); + }); + + test('nebari:run-first-enabled command returns custom label if given', async ({ + page + }) => { + const runFirstEnabled = await page.evaluate(async () => { + const registry = window.jupyterapp.commands; + const id = 'nebari:run-first-enabled'; + const args = { + commands: [ + { + id: 'this-command-does-not-exist' + }, + { + id: 'nebari:open-proxy', + args: { name: 'vscode' }, + label: 'Open Service VSCode' + } + ] + }; + + return { + id, + label: registry.label(id, args), + isEnabled: registry.isEnabled(id, args) + }; + }); + // Should set correct label for given command + expect(runFirstEnabled.label).toBe('Open Service VSCode'); + }); });