Skip to content

Commit 61bd1ad

Browse files
authored
Add interface to extension (#35)
* Add interface to extension * Update readme and lite scripts * Update nx stuff * Remove caching * Add error dialog * Add additional command instructions to readme * Restructure host * Improve error messages * Implement suggestions
1 parent f8596a0 commit 61bd1ad

File tree

13 files changed

+104
-75
lines changed

13 files changed

+104
-75
lines changed

README.md

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,22 @@ npm install jupyter-iframe-commands-host
3838
2. Import and use the `CommandBridge`:
3939

4040
```typescript
41-
import { CommandBridge } from 'jupyter-iframe-commands-host';
41+
import { createBridge } from 'jupyter-iframe-commands-host';
4242

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

4846
// Execute JupyterLab commands
4947
// Example: Toggle the left sidebar
50-
await bridge.commandBridge.execute('application:toggle-left-area');
48+
await commandBridge.execute('application:toggle-left-area');
5149

5250
// Example: Change the theme
53-
await bridge.commandBridge.execute('apputils:change-theme', {
51+
await commandBridge.execute('apputils:change-theme', {
5452
theme: 'JupyterLab Dark'
5553
});
5654

5755
// List available JupyterLab commands
58-
const commands = await bridge.commandBridge.listCommands();
56+
const commands = await commandBridge.listCommands();
5957
console.log(commands);
6058
```
6159

@@ -121,6 +119,15 @@ Examples of commands with arguments:
121119
> [!TIP]
122120
> For reference JupyterLab defines a list of default commands here: https://jupyterlab.readthedocs.io/en/latest/user/commands.html#commands-list
123121
122+
### Adding Additional Commands
123+
124+
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.
125+
126+
For further information please consult the Jupyter documentation:
127+
128+
- Creating an extension: https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html
129+
- Adding commands to the command registry: https://jupyterlab.readthedocs.io/en/stable/extension/extension_points.html#commands
130+
124131
## Demos
125132

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

154161
```bash
162+
# Build the lite assets
163+
jlpm build:lite
164+
155165
# Build the demo
156166
jlpm build:ghpages
157167

@@ -183,10 +193,12 @@ The `jlpm` command is JupyterLab's pinned version of
183193
# Change directory to the jupyter-iframe-commands directory
184194
# Install package in development mode
185195
pip install -e "."
196+
# Install dependencies
197+
jlpm install
198+
# Build extension Typescript source after making changes
199+
jlpm build
186200
# Link your development version of the extension with JupyterLab
187201
jupyter labextension develop . --overwrite
188-
# Rebuild extension Typescript source after making changes
189-
jlpm build
190202
```
191203

192204
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.

demo/example.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
"name": "python",
6666
"nbconvert_exporter": "python",
6767
"pygments_lexer": "ipython3",
68-
"version": "3.13.1"
68+
"version": "3.12.8"
6969
}
7070
},
7171
"nbformat": 4,

demo/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ <h1>%VITE_TITLE% Demo</h1>
2424
</button>
2525
<div class="mode-toggle">
2626
<label>
27-
<input type="radio" name="mode" value="lab" checked>
27+
<input type="radio" name="mode" value="lab" checked />
2828
<span>JupyterLab</span>
2929
</label>
3030
<label>
31-
<input type="radio" name="mode" value="notebook">
31+
<input type="radio" name="mode" value="notebook" />
3232
<span>Jupyter Notebook</span>
3333
</label>
3434
</div>

demo/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
"type": "module",
66
"scripts": {
77
"dev": "VITE_DEMO_SRC='./lite/index.html' VITE_TITLE='Lite' vite",
8-
"build": "tsc && vite build --base=./",
8+
"build": "tsc && VITE_DEMO_SRC='http://localhost:8888' VITE_TITLE='Local' vite build --base=./",
99
"build:ghpages": "tsc && VITE_DEMO_SRC='./lite/index.html' VITE_TITLE='Lite' vite build --base=./",
10+
"build:lite": "jupyter lite build --contents ../README.md --contents ./example.ipynb --output-dir ./public/lite",
1011
"preview": "VITE_DEMO_SRC='./lite/index.html' VITE_TITLE='Lite' vite preview",
1112
"start:lab": "jupyter lab --config jupyter_server_config.py",
1213
"start:lite": "jlpm dev",

demo/src/main.js

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
/* eslint-disable @typescript-eslint/quotes */
22
/* eslint-disable no-undef */
3-
import { CommandBridge } from 'jupyter-iframe-commands-host';
3+
import { createBridge } from 'jupyter-iframe-commands-host';
44

5-
const commandBridge = new CommandBridge({ iframeId: 'jupyterlab' })
6-
.commandBridge;
5+
const commandBridge = createBridge({ iframeId: 'jupyterlab' });
6+
7+
const submitCommand = async (command, args) => {
8+
try {
9+
await commandBridge.execute(command, args ? JSON.parse(args) : {});
10+
} catch (e) {
11+
document.getElementById('error-dialog').innerHTML = `<code>${e}</code>`;
12+
errorDialog.showModal();
13+
}
14+
};
715

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

80+
const errorDialog = document.createElement('dialog');
81+
errorDialog.innerHTML = `
82+
<form method="dialog">
83+
<h2 style="margin: 0; color: #ED4337;">⚠ Error</h2>
84+
<div id="error-dialog"></div>
85+
<div class="dialog-buttons">
86+
<button value="close">Close</button>
87+
</div>
88+
</form>
89+
`;
90+
7291
document.body.appendChild(instructionsDialog);
7392
document.body.appendChild(listCommandsDialog);
93+
document.body.appendChild(errorDialog);
7494

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

7999
document
80100
.getElementById('command-select-submit')
81-
.addEventListener('click', e => {
101+
.addEventListener('click', async e => {
82102
e.preventDefault();
83103
const select = document.getElementById('command-select');
84104
let command = select.value;
@@ -89,7 +109,7 @@ document
89109
args = `{"theme": "${command}"}`;
90110
command = 'apputils:change-theme';
91111
}
92-
commandBridge.execute(command, args ? JSON.parse(args) : {});
112+
await submitCommand(command, args);
93113
}
94114
instructionsDialog.close();
95115
});
@@ -103,15 +123,16 @@ document.getElementById('list-commands').addEventListener('click', async () => {
103123
listCommandsDialog.showModal();
104124
});
105125

106-
document.getElementById('commands').addEventListener('submit', e => {
126+
document.getElementById('commands').addEventListener('submit', async e => {
107127
e.preventDefault();
108128
const command = document.querySelector('input[name="command"]').value;
109129

110130
// Single quotes cause an error
111131
const args = document
112132
.querySelector('input[name="args"]')
113133
.value.replace(/'/g, '"');
114-
commandBridge.execute(command, args ? JSON.parse(args) : {});
134+
135+
await submitCommand(command, args);
115136
});
116137

117138
// Handle mode toggle

nx.json

Lines changed: 0 additions & 15 deletions
This file was deleted.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
],
2121
"scripts": {
2222
"build": "lerna run build",
23-
"build:lite": "jupyter lite build --contents README.md --output-dir demo/public/lite",
2423
"build:prod": "lerna run build:prod",
2524
"clean": "lerna run clean",
2625
"clean:all": "lerna run clean:all",

packages/extension/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
},
5555
"dependencies": {
5656
"@jupyterlab/application": "^4.3.2",
57-
"@jupyterlab/settingregistry": "^4.3.2"
57+
"@jupyterlab/settingregistry": "^4.3.2",
58+
"@lumino/coreutils": "^2.2.0"
5859
},
5960
"devDependencies": {
6061
"@jupyterlab/builder": "^4.3.2"

packages/extension/src/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { ISettingRegistry } from '@jupyterlab/settingregistry';
99
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
1010
import { expose, windowEndpoint } from 'comlink';
11+
import { ICommandBridgeRemote } from './interface';
1112

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

45-
const api = {
46-
execute(command: string, args: ReadonlyPartialJSONObject) {
47-
commands.execute(command, args);
46+
const api: ICommandBridgeRemote = {
47+
async execute(command: string, args: ReadonlyPartialJSONObject) {
48+
await commands.execute(command, args);
4849
},
4950
listCommands() {
5051
return commands.listCommands();
@@ -57,3 +58,4 @@ const plugin: JupyterFrontEndPlugin<void> = {
5758
};
5859

5960
export default plugin;
61+
export * from './interface';

packages/extension/src/interface.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
2+
3+
/**
4+
* Represents a remote command bridge interface.
5+
*
6+
* This interface provides a standardized way to interact with a Jupyter environment contained in an iframe.
7+
*/
8+
export interface ICommandBridgeRemote {
9+
/**
10+
* Executes a command with the given arguments.
11+
*
12+
* @param command - The name of the command to execute.
13+
* @param args - An object containing arguments for the command.
14+
*/
15+
execute(command: string, args: ReadonlyPartialJSONObject): void;
16+
17+
/**
18+
* Lists all available commands.
19+
*
20+
* @returns An array of strings representing the names of all available commands.
21+
*/
22+
listCommands(): string[];
23+
}

0 commit comments

Comments
 (0)