Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add interface to extension #35

Merged
merged 9 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,22 @@ npm install jupyter-iframe-commands-host
2. Import and use the `CommandBridge`:

```typescript
import { CommandBridge } from 'jupyter-iframe-commands-host';
import { createBridge } from 'jupyter-iframe-commands-host';

// Initialize the bridge with your iframe ID
const bridge = new CommandBridge({
iframeId: 'your-jupyter-iframe-id'
});
const commandBridge = createBridge({ iframeId: 'your-jupyter-iframe-id' });

// Execute JupyterLab commands
// Example: Toggle the left sidebar
await bridge.commandBridge.execute('application:toggle-left-area');
await commandBridge.execute('application:toggle-left-area');

// Example: Change the theme
await bridge.commandBridge.execute('apputils:change-theme', {
await commandBridge.execute('apputils:change-theme', {
theme: 'JupyterLab Dark'
});

// List available JupyterLab commands
const commands = await bridge.commandBridge.listCommands();
const commands = await commandBridge.listCommands();
console.log(commands);
```

Expand Down Expand Up @@ -121,6 +119,15 @@ Examples of commands with arguments:
> [!TIP]
> For reference JupyterLab defines a list of default commands here: https://jupyterlab.readthedocs.io/en/latest/user/commands.html#commands-list

### Adding Additional Commands

This package utilizes a bridge mechanism to transmit commands from the host to the extension running in Jupyter. To expand functionality beyond what's currently offered, you can develop a custom extension that defines new commands. If this custom extension is installed within the same Jupyter environment as the `jupyter-iframe-commands` extension, those commands will become available.

For further information please cosult the Jupyter documentation:
gjmooney marked this conversation as resolved.
Show resolved Hide resolved

- Creating an extension: https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html
- Adding commands to the command registry: https://jupyterlab.readthedocs.io/en/stable/extension/extension_points.html#commands

## Demos

### Local Demo
Expand Down Expand Up @@ -152,6 +159,9 @@ To run the demo on a Jupyter Lite instance:
3. Build and start the demo app:

```bash
# Build the lite assets
jlpm build:lite

# Build the demo
jlpm build:ghpages

Expand Down Expand Up @@ -183,10 +193,12 @@ The `jlpm` command is JupyterLab's pinned version of
# Change directory to the jupyter-iframe-commands directory
# Install package in development mode
pip install -e "."
# Install dependencies
jlpm install
# Build extension Typescript source after making changes
jlpm build
# Link your development version of the extension with JupyterLab
jupyter labextension develop . --overwrite
# Rebuild extension Typescript source after making changes
jlpm build
```

You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension.
Expand Down
2 changes: 1 addition & 1 deletion demo/example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.1"
"version": "3.12.8"
}
},
"nbformat": 4,
Expand Down
4 changes: 2 additions & 2 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ <h1>%VITE_TITLE% Demo</h1>
</button>
<div class="mode-toggle">
<label>
<input type="radio" name="mode" value="lab" checked>
<input type="radio" name="mode" value="lab" checked />
<span>JupyterLab</span>
</label>
<label>
<input type="radio" name="mode" value="notebook">
<input type="radio" name="mode" value="notebook" />
<span>Jupyter Notebook</span>
</label>
</div>
Expand Down
3 changes: 2 additions & 1 deletion demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
"type": "module",
"scripts": {
"dev": "VITE_DEMO_SRC='./lite/index.html' VITE_TITLE='Lite' vite",
"build": "tsc && vite build --base=./",
"build": "tsc && VITE_DEMO_SRC='http://localhost:8888' VITE_TITLE='Local' vite build --base=./",
"build:ghpages": "tsc && VITE_DEMO_SRC='./lite/index.html' VITE_TITLE='Lite' vite build --base=./",
"build:lite": "jupyter lite build --contents ../README.md --contents ./example.ipynb --output-dir ./public/lite",
gjmooney marked this conversation as resolved.
Show resolved Hide resolved
"preview": "VITE_DEMO_SRC='./lite/index.html' VITE_TITLE='Lite' vite preview",
"start:lab": "jupyter lab --config jupyter_server_config.py",
"start:lite": "jlpm dev",
Expand Down
37 changes: 29 additions & 8 deletions demo/src/main.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
/* eslint-disable @typescript-eslint/quotes */
/* eslint-disable no-undef */
import { CommandBridge } from 'jupyter-iframe-commands-host';
import { createBridge } from 'jupyter-iframe-commands-host';

const commandBridge = new CommandBridge({ iframeId: 'jupyterlab' })
.commandBridge;
const commandBridge = createBridge({ iframeId: 'jupyterlab' });

const submitCommand = async (command, args) => {
try {
await commandBridge.execute(command, args ? JSON.parse(args) : {});
} catch (e) {
document.getElementById('error-dialog').innerHTML = `<code>${e}</code>`;
errorDialog.showModal();
}
};

// Create and append dialogs to the document
const instructionsDialog = document.createElement('dialog');
Expand All @@ -12,7 +20,7 @@ instructionsDialog.innerHTML = `
<div>
<h2 style="margin-top: 0;">Instructions</h2>
<p>To use this demo simply enter a command in the command input and any arguments for that command in the args input.</p>
<p>Click the <code style="background-color: lightsteelblue;">List Commands</code> button to see a list of available commands.</p>
<p>Click the <code style="background-color: lightsteelblue;">List Available Commands</code> button to see a list of available commands.</p>
<div style="display: flex; gap: 0.4rem; flex-direction: column; text-align: left; font-size: 0.9rem;">
<p style="font-weight: bold; padding: 0;">Some commands are listed here for convenience:</p>
<div class="command-example">
Expand Down Expand Up @@ -69,16 +77,28 @@ listCommandsDialog.innerHTML = `
</form>
`;

const errorDialog = document.createElement('dialog');
errorDialog.innerHTML = `
<form method="dialog">
<h2 style="margin: 0; color: #ED4337;">⚠ Error</h2>
<div id="error-dialog"></div>
<div class="dialog-buttons">
<button value="close">Close</button>
</div>
</form>
`;

document.body.appendChild(instructionsDialog);
document.body.appendChild(listCommandsDialog);
document.body.appendChild(errorDialog);

document.getElementById('instructions').addEventListener('click', () => {
instructionsDialog.showModal();
});

document
.getElementById('command-select-submit')
.addEventListener('click', e => {
.addEventListener('click', async e => {
e.preventDefault();
const select = document.getElementById('command-select');
let command = select.value;
Expand All @@ -89,7 +109,7 @@ document
args = `{"theme": "${command}"}`;
command = 'apputils:change-theme';
}
commandBridge.execute(command, args ? JSON.parse(args) : {});
await submitCommand(command, args);
}
instructionsDialog.close();
});
Expand All @@ -103,15 +123,16 @@ document.getElementById('list-commands').addEventListener('click', async () => {
listCommandsDialog.showModal();
});

document.getElementById('commands').addEventListener('submit', e => {
document.getElementById('commands').addEventListener('submit', async e => {
e.preventDefault();
const command = document.querySelector('input[name="command"]').value;

// Single quotes cause an error
const args = document
.querySelector('input[name="args"]')
.value.replace(/'/g, '"');
commandBridge.execute(command, args ? JSON.parse(args) : {});

await submitCommand(command, args);
});

// Handle mode toggle
Expand Down
15 changes: 0 additions & 15 deletions nx.json

This file was deleted.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
],
"scripts": {
"build": "lerna run build",
"build:lite": "jupyter lite build --contents README.md --output-dir demo/public/lite",
"build:prod": "lerna run build:prod",
"clean": "lerna run clean",
"clean:all": "lerna run clean:all",
Expand Down
3 changes: 2 additions & 1 deletion packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
},
"dependencies": {
"@jupyterlab/application": "^4.3.2",
"@jupyterlab/settingregistry": "^4.3.2"
"@jupyterlab/settingregistry": "^4.3.2",
"@lumino/coreutils": "^2.2.0"
},
"devDependencies": {
"@jupyterlab/builder": "^4.3.2"
Expand Down
8 changes: 5 additions & 3 deletions packages/extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
import { expose, windowEndpoint } from 'comlink';
import { ICommandBridgeRemote } from './interface';

/**
* A plugin to expose an API for interacting with JupyterLab from a parent page.
Expand Down Expand Up @@ -42,9 +43,9 @@ const plugin: JupyterFrontEndPlugin<void> = {
});
}

const api = {
execute(command: string, args: ReadonlyPartialJSONObject) {
commands.execute(command, args);
const api: ICommandBridgeRemote = {
async execute(command: string, args: ReadonlyPartialJSONObject) {
await commands.execute(command, args);
},
listCommands() {
return commands.listCommands();
Expand All @@ -57,3 +58,4 @@ const plugin: JupyterFrontEndPlugin<void> = {
};

export default plugin;
export * from './interface';
6 changes: 6 additions & 0 deletions packages/extension/src/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';

export interface ICommandBridgeRemote {
execute(command: string, args: ReadonlyPartialJSONObject): void;
listCommands(): string[];
}
gjmooney marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 2 additions & 3 deletions packages/host/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@
"watch": "tsc -b --watch"
},
"dependencies": {
"@lumino/commands": "^2.3.1",
"@lumino/coreutils": "^2.2.0",
"comlink": "^4.4.2"
"comlink": "^4.4.2",
"jupyter-iframe-commands": "0.0.1"
}
}
42 changes: 14 additions & 28 deletions packages/host/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,24 @@
// Copyright (c) TileDB, Inc.
// Distributed under the terms of the Modified BSD License.
import { Endpoint, Remote, windowEndpoint, wrap } from 'comlink';

import { windowEndpoint, wrap } from 'comlink';
import { ICommandBridgeRemote } from 'jupyter-iframe-commands';
/**
* A bridge to expose actions on JupyterLab commands.
*/
export class CommandBridge {
constructor({ iframeId }: CommandBridge.IOptions) {
this._iframe = document.getElementById(iframeId) as HTMLIFrameElement;

if (!this._iframe) {
console.error('iframe not found');
return;
}

this._childWindow = this._iframe.contentWindow;

if (!this._childWindow) {
console.error('child window not found');
return;
}
export function createBridge({ iframeId }: { iframeId: string }) {
const iframe = document.getElementById(iframeId) as HTMLIFrameElement;

this._endpoint = windowEndpoint(this._childWindow);
this.commandBridge = wrap(this._endpoint);
if (!iframe) {
throw new Error(
`Cannot create bridge: iframe with id "${iframeId}" not found`
);
}

private _iframe: HTMLIFrameElement | null;
private _childWindow: Window | undefined | null;
private _endpoint: Endpoint | undefined;
commandBridge: Remote<unknown> | undefined;
}

export namespace CommandBridge {
export interface IOptions {
iframeId: string;
if (!iframe.contentWindow) {
throw new Error(
`Cannot create bridge: iframe with id "${iframeId}" has no content window`
);
}

return wrap<ICommandBridgeRemote>(windowEndpoint(iframe.contentWindow));
}
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5885,9 +5885,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "jupyter-iframe-commands-host@workspace:packages/host"
dependencies:
"@lumino/commands": ^2.3.1
"@lumino/coreutils": ^2.2.0
comlink: ^4.4.2
jupyter-iframe-commands: 0.0.1
languageName: unknown
linkType: soft

Expand Down Expand Up @@ -5921,13 +5920,14 @@ __metadata:
languageName: unknown
linkType: soft

"jupyter-iframe-commands@workspace:packages/extension":
"jupyter-iframe-commands@0.0.1, jupyter-iframe-commands@workspace:packages/extension":
version: 0.0.0-use.local
resolution: "jupyter-iframe-commands@workspace:packages/extension"
dependencies:
"@jupyterlab/application": ^4.3.2
"@jupyterlab/builder": ^4.3.2
"@jupyterlab/settingregistry": ^4.3.2
"@lumino/coreutils": ^2.2.0
languageName: unknown
linkType: soft

Expand Down
Loading