Skip to content

Commit

Permalink
Merge pull request #12 from Exabyte-io/feature/SOF-7286
Browse files Browse the repository at this point in the history
Feature/SOF-7286
  • Loading branch information
VsevolodX committed Mar 21, 2024
2 parents 42b3c80 + 5c95b76 commit 2d1d52a
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 72 deletions.
21 changes: 14 additions & 7 deletions README.md
Expand Up @@ -28,16 +28,23 @@ For more info, keep an eye on the JupyterLite documentation:
- How-to Guides: https://jupyterlite.readthedocs.io/en/latest/howto/index.html
- Reference: https://jupyterlite.readthedocs.io/en/latest/reference/index.html

## Additional Notes
## Development Notes

From Team Mat3ra:

- `data_bridge` extensions is built using the `setup.sh`
- pass `INSTALL=1 BUILD=1` to also build and install the jupyter lite with extension
To build and run the JupyterLite server with extension, we use the following steps:
- check that `pyenv` and `npm` are installed
- run `npm install` to install the required packages and setup the `data_bridge` extension
- run `npm install INSTALL=1 BUILD=1` to also build and install the jupyter lite with extension
- `requirements.txt` is updated as part of the above to include the extension
- requires `pyenv` and `npm` installed
- run `npm run start -p=8000` to start the server (specify the port if needed)
- content is populated with a submodule of `exabyte-io/api-examples`

To develop the extension:
- run `npm install` or `sh setup.sh` to create the extension
- change code in `extensions/dist/data_bridge/src/index.ts`
- run `npm run restart` or `sh update.sh` to build the extension, install it, and restart the server with it

- content is populated with a submodule of `exabyte-io/api-examples`:
To publish:
- commit changes to the `extensions/src/data_bridge/index.ts` file

```shell
cd content
Expand Down
146 changes: 86 additions & 60 deletions extensions/src/data_bridge/index.ts
@@ -1,9 +1,13 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import {
JupyterFrontEnd,
JupyterFrontEndPlugin,
JupyterFrontEndPlugin
} from "@jupyterlab/application";

import { IKernelConnection } from "@jupyterlab/services/lib/kernel/kernel";
import { NotebookPanel, INotebookTracker } from "@jupyterlab/notebook";
import { IframeMessageSchema } from "@mat3ra/esse/lib/js/types";

/**
* Initialization data for the data-bridge extension.
Expand All @@ -15,77 +19,99 @@ const plugin: JupyterFrontEndPlugin<void> = {
"Extension to pass JSON data between host page and Jupyter Lite instance",
autoStart: true,
requires: [INotebookTracker],
activate: async (
app: JupyterFrontEnd,
notebookTracker: INotebookTracker
) => {
activate: async (app: JupyterFrontEnd, notebookTracker: INotebookTracker) => {
console.log("JupyterLab extension data-bridge is activated!");

// Send path of the currently opened notebook to the host page when the notebook is opened
notebookTracker.currentChanged.connect((sender, notebookPanel) => {
if (notebookPanel) {
const currentPath = notebookPanel.context.path;
// Variable to hold the data from the host page
let dataFromHost = "";
// When data is loaded into the kernel, save it into this object to later check it to avoid reloading the same data
const kernelsDataFromHost: { [id: string]: string } = {};

window.parent.postMessage(
{
type: "from-iframe-to-host",
path: currentPath,
},
"*"
);
}
});

// @ts-ignore
window.sendDataToHost = (data: any) => {
window.parent.postMessage(
{
type: "from-iframe-to-host",
data: data,
},
"*"
);
const MESSAGE_GET_DATA_CONTENT = {
type: "from-iframe-to-host",
action: "get-data",
payload: {}
};

// On JupyterLite startup send get-data message to the host to request data
window.parent.postMessage(MESSAGE_GET_DATA_CONTENT, "*");

/**
* Listen for the current notebook being changed, and on kernel status change load the data into the kernel
*/
notebookTracker.currentChanged.connect(
// @ts-ignore
async (sender, notebookPanel: NotebookPanel) => {
if (notebookPanel) {
console.debug("Notebook opened", notebookPanel.context.path);
await notebookPanel.sessionContext.ready;
const sessionContext = notebookPanel.sessionContext;

sessionContext.session?.kernel?.statusChanged.connect(
(kernel, status) => {
if (
status === "idle" &&
kernelsDataFromHost[kernel.id] !== dataFromHost
) {
loadData(kernel, dataFromHost);
// Save data for the current kernel to avoid reloading the same data
kernelsDataFromHost[kernel.id] = dataFromHost;
}
// Reset the data when the kernel is restarting, since the loaded data is lost
if (status === "restarting") {
kernelsDataFromHost[kernel.id] = "";
}
}
);
}
}
);

/**
* Send data to the host page
* @param data
*/
// @ts-ignore
window.requestDataFromHost = (variableName = "data") => {
window.parent.postMessage(
{
type: "from-iframe-to-host",
requestData: true,
variableName,
},
"*"
);
window.sendDataToHost = (data: object) => {
const MESSAGE_SET_DATA_CONTENT = {
type: "from-iframe-to-host",
action: "set-data",
payload: data
};
window.parent.postMessage(MESSAGE_SET_DATA_CONTENT, "*");
};

window.addEventListener("message", async (event) => {
if (event.data.type === "from-host-to-iframe") {
let data = event.data.data;
let variableName = event.data.variableName || "data";
const dataJson = JSON.stringify(data);
const code = `
import json
${variableName} = json.loads('${dataJson}')
`;
// Similar to https://jupyterlab.readthedocs.io/en/stable/api/classes/application.LabShell.html#currentWidget
// https://jupyterlite.readthedocs.io/en/latest/reference/api/ts/interfaces/jupyterlite_application.ISingleWidgetShell.html#currentwidget
const currentWidget = app.shell.currentWidget;

if (currentWidget instanceof NotebookPanel) {
const notebookPanel = currentWidget;
const kernel = notebookPanel.sessionContext.session?.kernel;
/**
* Listen for messages from the host page, and update the data in the kernel
* @param event MessageEvent
*/
window.addEventListener(
"message",
async (event: MessageEvent<IframeMessageSchema>) => {
if (event.data.type === "from-host-to-iframe") {
dataFromHost = JSON.stringify(event.data.payload);
const notebookPanel = notebookTracker.currentWidget;
await notebookPanel?.sessionContext.ready;
const sessionContext = notebookPanel?.sessionContext;
const kernel = sessionContext?.session?.kernel;
if (kernel) {
kernel.requestExecute({ code: code });
} else {
console.error("No active kernel found");
loadData(kernel, dataFromHost);
}
} else {
console.error("Current active widget is not a notebook");
}
}
});
},
);

/**
* Load the data into the kernel by executing code
* @param kernel
* @param data string representation of JSON
*/
const loadData = (kernel: IKernelConnection, data: string) => {
const code = `import json\ndata_from_host = json.loads('${data}')`;
const result = kernel.requestExecute({ code: code });
console.debug("Execution result:", result);
};
}
};

export default plugin;
4 changes: 3 additions & 1 deletion package.json
@@ -1,6 +1,8 @@
{
"scripts": {
"install": "sh setup.sh",
"start": "python -m http.server -b localhost -d ./dist",
"build": "python -m pip install -r requirements.txt; cp -rL content content-resolved; jupyter lite build --contents content-resolved --output-dir dist"
"build": "python -m pip install -r requirements.txt; cp -rL content content-resolved; jupyter lite build --contents content-resolved --output-dir dist",
"restart": "sh update.sh"
}
}
9 changes: 6 additions & 3 deletions setup.sh
@@ -1,6 +1,6 @@
#!/bin/bash
# This script creates a JupyterLab extension using the cookiecutter template
# and updates the requirements.txt file to make it installable in the current
# and updates the requirements.txt file to make it installable in the current
# JupyterLab environment.
# It assumes that pyenv and nvm are installed and configured correctly.

Expand Down Expand Up @@ -64,7 +64,7 @@ if [ ! -d "$COOKIECUTTER_TEMPLATE_PATH" ]; then
cookiecutter "${COOKIECUTTER_OPTIONS[@]}"
echo "Created extension using cookiecutter template."
else
# COOKIECUTTER_OPTIONS[0]="$COOKIECUTTER_TEMPLATE_PATH"
# COOKIECUTTER_OPTIONS[0]="$COOKIECUTTER_TEMPLATE_PATH"
cookiecutter "${COOKIECUTTER_OPTIONS[@]}"
echo "Created extension using cached cookiecutter template."
fi
Expand All @@ -78,7 +78,7 @@ else
echo "Source file or destination directory not found. Skipping copy."
fi

# The extension is a separate package so it requires to have a yarn.lock file
# The extension is treated here as a separate package so it requires to have a yarn.lock file
cd $EXTENSION_NAME
touch yarn.lock
pip install -ve .
Expand All @@ -88,6 +88,9 @@ jupyter labextension develop --overwrite .
jlpm add @jupyterlab/application
jlpm add @jupyterlab/notebook

# Install mat3ra specific dependencies
jlpm add @mat3ra/esse

# Build the extension
jlpm run build

Expand Down
11 changes: 11 additions & 0 deletions update.sh
@@ -0,0 +1,11 @@
#!/bin/bash
# This script rebuilds the JupyterLab extension and starts the JupyterLite server
# Meant to automate the process during development

rm -rf dist/extensions/data_bridge
cd extensions/dist/data_bridge
jlpm run build

cd ../../..

npm run build && npm run start -p=8000

0 comments on commit 2d1d52a

Please sign in to comment.