From a7e7ae8302224937b6c3c072c0c7bad27fedda9a Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 13 Mar 2024 15:26:12 -0700 Subject: [PATCH 01/30] update: adjust data to new esse schema --- extensions/src/data_bridge/index.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index f4db562..f2c6569 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -3,7 +3,8 @@ import { JupyterFrontEndPlugin, } from "@jupyterlab/application"; -import { NotebookPanel, INotebookTracker } from "@jupyterlab/notebook"; +import {NotebookPanel, INotebookTracker} from "@jupyterlab/notebook"; +import {JupyterliteMessageSchema} from "@mat3ra/esse/lib/js/types"; /** * Initialization data for the data-bridge extension. @@ -29,7 +30,9 @@ const plugin: JupyterFrontEndPlugin = { window.parent.postMessage( { type: "from-iframe-to-host", - path: currentPath, + payload: { + data: currentPath, + }, }, "*" ); @@ -41,7 +44,9 @@ const plugin: JupyterFrontEndPlugin = { window.parent.postMessage( { type: "from-iframe-to-host", - data: data, + payload: { + data: data, + }, }, "*" ); @@ -52,17 +57,19 @@ const plugin: JupyterFrontEndPlugin = { window.parent.postMessage( { type: "from-iframe-to-host", - requestData: true, - variableName, + payload: { + requestData: true, + variableName, + }, }, "*" ); }; - window.addEventListener("message", async (event) => { + window.addEventListener("message", async (event: MessageEvent) => { if (event.data.type === "from-host-to-iframe") { - let data = event.data.data; - let variableName = event.data.variableName || "data"; + let data = event.data.payload.data; + let variableName = event.data.payload.variableName || "data"; const dataJson = JSON.stringify(data); const code = ` import json @@ -76,7 +83,7 @@ const plugin: JupyterFrontEndPlugin = { const notebookPanel = currentWidget; const kernel = notebookPanel.sessionContext.session?.kernel; if (kernel) { - kernel.requestExecute({ code: code }); + kernel.requestExecute({code: code}); } else { console.error("No active kernel found"); } From 1a2af6253d03a5f18fd08686fa22d3664f24a216 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 13 Mar 2024 15:26:32 -0700 Subject: [PATCH 02/30] chore: import esse from commit hash --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index abd3ffe..0f532bc 100644 --- a/package.json +++ b/package.json @@ -3,4 +3,5 @@ "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" } + } From 20faa843c04e49b84700d2ccd343457ce8db9ba2 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 15 Mar 2024 18:30:07 -0700 Subject: [PATCH 03/30] update: adjust messages for schema --- extensions/src/data_bridge/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index f2c6569..3179e5f 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -30,6 +30,7 @@ const plugin: JupyterFrontEndPlugin = { window.parent.postMessage( { type: "from-iframe-to-host", + action: "set-data", payload: { data: currentPath, }, @@ -44,6 +45,7 @@ const plugin: JupyterFrontEndPlugin = { window.parent.postMessage( { type: "from-iframe-to-host", + action: "set-data", payload: { data: data, }, @@ -54,11 +56,12 @@ const plugin: JupyterFrontEndPlugin = { // @ts-ignore window.requestDataFromHost = (variableName = "data") => { + console.log("requestDataFromHost", variableName); window.parent.postMessage( { type: "from-iframe-to-host", + action: "get-data", payload: { - requestData: true, variableName, }, }, From 9a28c4fa0ae896f1a248b4c3fb02d3c72a17d8a9 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sat, 16 Mar 2024 18:03:51 -0700 Subject: [PATCH 04/30] update: listen to kernel status change --- extensions/src/data_bridge/index.ts | 114 +++++++++++++++------------- update.sh | 108 ++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 52 deletions(-) create mode 100644 update.sh diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index 3179e5f..66e5109 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -1,10 +1,11 @@ +// @ts-nocheck import { JupyterFrontEnd, JupyterFrontEndPlugin, } from "@jupyterlab/application"; -import {NotebookPanel, INotebookTracker} from "@jupyterlab/notebook"; -import {JupyterliteMessageSchema} from "@mat3ra/esse/lib/js/types"; +import {NotebookPanel, INotebookTracker, NotebookAdapter} from "@jupyterlab/notebook"; +import {IframeMessageSchema} from "@mat3ra/esse/lib/js/types"; /** * Initialization data for the data-bridge extension. @@ -18,28 +19,64 @@ const plugin: JupyterFrontEndPlugin = { requires: [INotebookTracker], activate: async ( app: JupyterFrontEnd, - notebookTracker: INotebookTracker + notebookTracker: INotebookTracker, + notebookAdapter: NotebookAdapter ) => { 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) => { + // variable to hold the data from the host page + // @ts-ignore + app.dataFromHost = ""; + + // @ts-ignore + notebookTracker.currentChanged.connect(async (sender, notebookPanel: NotebookPanel) => { if (notebookPanel) { - const currentPath = notebookPanel.context.path; + console.log("Notebook opened", notebookPanel.context.path); + await notebookPanel.sessionContext.ready; + const sessionContext = notebookPanel.sessionContext; - window.parent.postMessage( - { - type: "from-iframe-to-host", - action: "set-data", - payload: { - data: currentPath, - }, - }, - "*" - ); + sessionContext.kernelChanged.connect((_, kernel) => { + console.log("Kernel changed", kernel); + console.log("sessionContext.kernel", sessionContext); + }); + + sessionContext.session?.kernel?.statusChanged.connect((_, status) => { + console.log("Kernel status changed", status); + console.log("_", _); + }); + + if (notebookPanel.sessionContext.session?.kernel?.status === 'idle') { + console.log("Kernel is idle"); + const kernel = notebookPanel.sessionContext.session.kernel; + // @ts-ignore + console.log("dataFromHost", app.dataFromHost); + // @ts-ignore + kernel.requestExecute({ code: `data = ${app.dataFromHost}` }); + } } }); + + // 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 sessionContext = notebookPanel.sessionContext; + sessionContext.kernelChanged.connect((_, kernel) => { + console.log("Kernel changed", kernel); + }); + const kernel = notebookPanel.sessionContext.session?.kernel; + if (kernel) { + // @ts-ignore + kernel.requestExecute(`data = ${app.dataFromHost}`); + } else { + console.error("No active kernel found"); + } + } else { + console.error("Current active widget is not a notebook"); + } + // @ts-ignore window.sendDataToHost = (data: any) => { window.parent.postMessage( @@ -54,48 +91,21 @@ const plugin: JupyterFrontEndPlugin = { ); }; - // @ts-ignore - window.requestDataFromHost = (variableName = "data") => { - console.log("requestDataFromHost", variableName); - window.parent.postMessage( - { - type: "from-iframe-to-host", - action: "get-data", - payload: { - variableName, - }, - }, - "*" - ); - }; - window.addEventListener("message", async (event: MessageEvent) => { + // @ts-ignore + window.addEventListener("message", async (event: MessageEvent) => { if (event.data.type === "from-host-to-iframe") { let data = event.data.payload.data; - let variableName = event.data.payload.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; - if (kernel) { - kernel.requestExecute({code: code}); - } else { - console.error("No active kernel found"); - } - } else { - console.error("Current active widget is not a notebook"); - } + // @ts-ignore + app.dataFromHost = dataJson; + //@ts-ignore + console.log("Data from host received. app:", app.dataFromHost); } + }); }, }; -export default plugin; + +export default plugin; \ No newline at end of file diff --git a/update.sh b/update.sh new file mode 100644 index 0000000..2f72e10 --- /dev/null +++ b/update.sh @@ -0,0 +1,108 @@ +#!/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 +# JupyterLab environment. +# It assumes that pyenv and nvm are installed and configured correctly. + +PYTHON_VERSION="3.10" +NODE_VERSION="18" +EXTENSION_NAME="data_bridge" +COOKIECUTTER_TEMPLATE_PATH="$HOME/.cookiecutters/extension-cookiecutter-ts" +GITHUB_TEMPLATE_URL="https://github.com/jupyterlab/extension-cookiecutter-ts" + +kind="frontend" +author_name="Mat3ra" +author_email="info@mat3ra.com" +labextension_name=$EXTENSION_NAME +python_name=$EXTENSION_NAME +project_short_description="A JupyterLab extension that allows you to send data between notebook and host page" +has_settings=n +has_binder=n +test=n +repository="https://github.com/exabyte-io/jupyter-lite" + +COOKIECUTTER_OPTIONS=( + "$GITHUB_TEMPLATE_URL" + "--no-input" + "kind=$kind" + "author_name=$author_name" + "author_email=$author_email" + "labextension_name=$labextension_name" + "python_name=$python_name" + "project_short_description=$project_short_description" + "has_settings=$has_settings" + "has_binder=$has_binder" + "test=$test" + "repository=$repository" +) + +# Ensure Python and Node.js are installed and switch to the correct versions +if [ ! -d "$HOME/.pyenv/versions/$PYTHON_VERSION" ]; then + pyenv install $PYTHON_VERSION +fi +pyenv local $PYTHON_VERSION || echo "pyenv not found" + +python -m venv .venv +source .venv/bin/activate + +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + +nvm install $NODE_VERSION +nvm use $NODE_VERSION + +pip install cookiecutter jupyterlab==4 jupyterlite-core + +# Create directory if it doesn't exist +if [ ! -d "extensions/dist" ]; then + mkdir -p extensions/dist +fi +cd extensions/dist + +# Use cookiecutter with the template path if it exists, otherwise use the URL +if [ ! -d "$COOKIECUTTER_TEMPLATE_PATH" ]; then + cookiecutter "${COOKIECUTTER_OPTIONS[@]}" + echo "Created extension using cookiecutter template." +else + # COOKIECUTTER_OPTIONS[0]="$COOKIECUTTER_TEMPLATE_PATH" + cookiecutter "${COOKIECUTTER_OPTIONS[@]}" + echo "Created extension using cached cookiecutter template." +fi + +# Copy the index.ts file if both source and destination directories exist +SRC_FILE="../src/$EXTENSION_NAME/index.ts" +DEST_DIR="./$EXTENSION_NAME/src" +if [ -f "$SRC_FILE" ] && [ -d "$DEST_DIR" ]; then + cp "$SRC_FILE" "$DEST_DIR/index.ts" +else + echo "Source file or destination directory not found. Skipping copy." +fi + +# 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 . +jupyter labextension develop --overwrite . + +# Install dependencies +jlpm add @jupyterlab/application +jlpm add @jupyterlab/notebook + +# Build the extension +jlpm run build + +cd ../../../ + +# add to requirements.txt +LINE="./extensions/dist/$EXTENSION_NAME" +FILE='requirements.txt' +grep -qF -- "$LINE" "$FILE" || echo "$LINE" >> "$FILE" + +# Install extension +[[ ! -z $INSTALL ]] && python -m pip install -r requirements.txt + +# Build JupyterLite +[[ ! -z $BUILD ]] && jupyter lite build --contents content --output-dir dist + +# Exit with zero (for GH workflow) +exit 0 From 9c860dd5c66e44a4db0c776a6fb84ba5f3623c21 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 17 Mar 2024 22:23:16 -0700 Subject: [PATCH 05/30] update: materials lodaded to data variable when selected materials change --- extensions/src/data_bridge/index.ts | 56 ++++++++++++++--------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index 66e5109..810ba96 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -43,40 +43,22 @@ const plugin: JupyterFrontEndPlugin = { sessionContext.session?.kernel?.statusChanged.connect((_, status) => { console.log("Kernel status changed", status); console.log("_", _); + // @ts-ignore + if (_.status === 'idle' && !_.isInitated) { + console.log("Kernel is idle"); + // @ts-ignore + console.log("dataFromHost", app.dataFromHost); + // @ts-ignore + _.isInitated = true; + const kernel = sessionContext.session?.kernel; + console.log("kernel", kernel); + loadData(kernel); + } }); - if (notebookPanel.sessionContext.session?.kernel?.status === 'idle') { - console.log("Kernel is idle"); - const kernel = notebookPanel.sessionContext.session.kernel; - // @ts-ignore - console.log("dataFromHost", app.dataFromHost); - // @ts-ignore - kernel.requestExecute({ code: `data = ${app.dataFromHost}` }); - } } }); - - // 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 sessionContext = notebookPanel.sessionContext; - sessionContext.kernelChanged.connect((_, kernel) => { - console.log("Kernel changed", kernel); - }); - const kernel = notebookPanel.sessionContext.session?.kernel; - if (kernel) { - // @ts-ignore - kernel.requestExecute(`data = ${app.dataFromHost}`); - } else { - console.error("No active kernel found"); - } - } else { - console.error("Current active widget is not a notebook"); - } - // @ts-ignore window.sendDataToHost = (data: any) => { window.parent.postMessage( @@ -101,11 +83,25 @@ const plugin: JupyterFrontEndPlugin = { app.dataFromHost = dataJson; //@ts-ignore console.log("Data from host received. app:", app.dataFromHost); + // Execute code in the kernel + const notebookPanel = notebookTracker.currentWidget; + await notebookPanel.sessionContext.ready; + const sessionContext = notebookPanel.sessionContext; + const kernel = sessionContext.session?.kernel; + loadData(kernel); } }); + + const loadData = (kernel: any) => { + const dataFromHostString = JSON.stringify(app.dataFromHost); + // @ts-ignore + const result = kernel.requestExecute({code: `import json\ndata = json.loads(${dataFromHostString})`}); +// @ts-ignore + console.log("Execution result", result, app.dataFromHost); + } }, }; -export default plugin; \ No newline at end of file +export default plugin; From fab7e04b3078d1f7ba4997c27367e7b4dee3e7ca Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:34:35 -0700 Subject: [PATCH 06/30] update: materials lodaded to data variable when selected materials change and new notebook --- extensions/src/data_bridge/index.ts | 35 +++++++++++------------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index 810ba96..26ae927 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -35,27 +35,16 @@ const plugin: JupyterFrontEndPlugin = { await notebookPanel.sessionContext.ready; const sessionContext = notebookPanel.sessionContext; - sessionContext.kernelChanged.connect((_, kernel) => { - console.log("Kernel changed", kernel); - console.log("sessionContext.kernel", sessionContext); - }); - - sessionContext.session?.kernel?.statusChanged.connect((_, status) => { - console.log("Kernel status changed", status); - console.log("_", _); + sessionContext.session?.kernel?.statusChanged.connect((kernel, status) => { // @ts-ignore - if (_.status === 'idle' && !_.isInitated) { - console.log("Kernel is idle"); - // @ts-ignore - console.log("dataFromHost", app.dataFromHost); + console.log( status, kernel.id); + // @ts-ignore + if (kernel.status === 'idle' && kernel.dataFromHost !== app.dataFromHost) { // @ts-ignore - _.isInitated = true; - const kernel = sessionContext.session?.kernel; - console.log("kernel", kernel); - loadData(kernel); + kernel.dataFromHost = app.dataFromHost; + loadData(kernel, app.dataFromHost); } }); - } }); @@ -88,16 +77,18 @@ const plugin: JupyterFrontEndPlugin = { await notebookPanel.sessionContext.ready; const sessionContext = notebookPanel.sessionContext; const kernel = sessionContext.session?.kernel; - loadData(kernel); + loadData(kernel, data); } }); - const loadData = (kernel: any) => { - const dataFromHostString = JSON.stringify(app.dataFromHost); + const loadData = (kernel: any, data: any) => { + const dataFromHostString = JSON.stringify(data); + + const code = `import json\ndata = json.loads(${dataFromHostString})`; + // @ts-ignore + const result = kernel.requestExecute({code: code}); // @ts-ignore - const result = kernel.requestExecute({code: `import json\ndata = json.loads(${dataFromHostString})`}); -// @ts-ignore console.log("Execution result", result, app.dataFromHost); } }, From 66fc33c9c814d50eff6c518c25355002975246c6 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:44:42 -0700 Subject: [PATCH 07/30] chore: add JSdoc --- extensions/src/data_bridge/index.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index 26ae927..6c42e42 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -28,16 +28,19 @@ const plugin: JupyterFrontEndPlugin = { // @ts-ignore app.dataFromHost = ""; + /** + * Listen for the current notebook being changed, and on kernel status change load the data into the kernel + */ // @ts-ignore notebookTracker.currentChanged.connect(async (sender, notebookPanel: NotebookPanel) => { if (notebookPanel) { - console.log("Notebook opened", notebookPanel.context.path); + console.debug("Notebook opened", notebookPanel.context.path); await notebookPanel.sessionContext.ready; const sessionContext = notebookPanel.sessionContext; sessionContext.session?.kernel?.statusChanged.connect((kernel, status) => { // @ts-ignore - console.log( status, kernel.id); + console.debug(status, kernel.id); // @ts-ignore if (kernel.status === 'idle' && kernel.dataFromHost !== app.dataFromHost) { // @ts-ignore @@ -48,6 +51,10 @@ const plugin: JupyterFrontEndPlugin = { } }); + /** + * Send data to the host page + * @param data + */ // @ts-ignore window.sendDataToHost = (data: any) => { window.parent.postMessage( @@ -62,7 +69,10 @@ const plugin: JupyterFrontEndPlugin = { ); }; - + /** + * Listen for messages from the host page, and update the data in the kernel + * @param event MessageEvent + */ // @ts-ignore window.addEventListener("message", async (event: MessageEvent) => { if (event.data.type === "from-host-to-iframe") { @@ -71,7 +81,7 @@ const plugin: JupyterFrontEndPlugin = { // @ts-ignore app.dataFromHost = dataJson; //@ts-ignore - console.log("Data from host received. app:", app.dataFromHost); + console.debug("Data from host received. app:", app.dataFromHost); // Execute code in the kernel const notebookPanel = notebookTracker.currentWidget; await notebookPanel.sessionContext.ready; @@ -82,14 +92,18 @@ const plugin: JupyterFrontEndPlugin = { }); - const loadData = (kernel: any, data: any) => { + /** + * Load the data into the kernel by executing code + * @param kernel + * @param data + */ + const loadData = (kernel: IKernelConnection, data: JSON) => { const dataFromHostString = JSON.stringify(data); - const code = `import json\ndata = json.loads(${dataFromHostString})`; // @ts-ignore const result = kernel.requestExecute({code: code}); // @ts-ignore - console.log("Execution result", result, app.dataFromHost); + console.debug("Execution result", result, app.dataFromHost); } }, }; From 23ce79a87a8cbba27e5058555da2ab925541b96e Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:55:45 -0700 Subject: [PATCH 08/30] chore: explain process in README --- README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d2cf2c0..2b25195 100644 --- a/README.md +++ b/README.md @@ -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` +To build and run the JupyterLite server with extension, we use the following steps: +- check that `pyenv` and `npm` are installed +- build `data_bridge` extension running the `sh setup.sh` - pass `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 `sh setup.sh` to update the extension +- change code in `extensions/dist/data_bridge/src/index.ts` +- run `sh update.sh` to build the extension, install it, and start 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 From 0a995509d897909960921d7fe413be3cebf0a6dc Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:58:32 -0700 Subject: [PATCH 09/30] chore: add update.sh --- update.sh | 107 ++---------------------------------------------------- 1 file changed, 4 insertions(+), 103 deletions(-) diff --git a/update.sh b/update.sh index 2f72e10..a062196 100644 --- a/update.sh +++ b/update.sh @@ -1,108 +1,9 @@ #!/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 -# JupyterLab environment. -# It assumes that pyenv and nvm are installed and configured correctly. -PYTHON_VERSION="3.10" -NODE_VERSION="18" -EXTENSION_NAME="data_bridge" -COOKIECUTTER_TEMPLATE_PATH="$HOME/.cookiecutters/extension-cookiecutter-ts" -GITHUB_TEMPLATE_URL="https://github.com/jupyterlab/extension-cookiecutter-ts" - -kind="frontend" -author_name="Mat3ra" -author_email="info@mat3ra.com" -labextension_name=$EXTENSION_NAME -python_name=$EXTENSION_NAME -project_short_description="A JupyterLab extension that allows you to send data between notebook and host page" -has_settings=n -has_binder=n -test=n -repository="https://github.com/exabyte-io/jupyter-lite" - -COOKIECUTTER_OPTIONS=( - "$GITHUB_TEMPLATE_URL" - "--no-input" - "kind=$kind" - "author_name=$author_name" - "author_email=$author_email" - "labextension_name=$labextension_name" - "python_name=$python_name" - "project_short_description=$project_short_description" - "has_settings=$has_settings" - "has_binder=$has_binder" - "test=$test" - "repository=$repository" -) - -# Ensure Python and Node.js are installed and switch to the correct versions -if [ ! -d "$HOME/.pyenv/versions/$PYTHON_VERSION" ]; then - pyenv install $PYTHON_VERSION -fi -pyenv local $PYTHON_VERSION || echo "pyenv not found" - -python -m venv .venv -source .venv/bin/activate - -export NVM_DIR="$HOME/.nvm" -[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - -nvm install $NODE_VERSION -nvm use $NODE_VERSION - -pip install cookiecutter jupyterlab==4 jupyterlite-core - -# Create directory if it doesn't exist -if [ ! -d "extensions/dist" ]; then - mkdir -p extensions/dist -fi -cd extensions/dist - -# Use cookiecutter with the template path if it exists, otherwise use the URL -if [ ! -d "$COOKIECUTTER_TEMPLATE_PATH" ]; then - cookiecutter "${COOKIECUTTER_OPTIONS[@]}" - echo "Created extension using cookiecutter template." -else - # COOKIECUTTER_OPTIONS[0]="$COOKIECUTTER_TEMPLATE_PATH" - cookiecutter "${COOKIECUTTER_OPTIONS[@]}" - echo "Created extension using cached cookiecutter template." -fi - -# Copy the index.ts file if both source and destination directories exist -SRC_FILE="../src/$EXTENSION_NAME/index.ts" -DEST_DIR="./$EXTENSION_NAME/src" -if [ -f "$SRC_FILE" ] && [ -d "$DEST_DIR" ]; then - cp "$SRC_FILE" "$DEST_DIR/index.ts" -else - echo "Source file or destination directory not found. Skipping copy." -fi - -# 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 . -jupyter labextension develop --overwrite . - -# Install dependencies -jlpm add @jupyterlab/application -jlpm add @jupyterlab/notebook - -# Build the extension +rm -rf dist/extensions/data_bridge +cd extensions/dist/data_bridge jlpm run build -cd ../../../ - -# add to requirements.txt -LINE="./extensions/dist/$EXTENSION_NAME" -FILE='requirements.txt' -grep -qF -- "$LINE" "$FILE" || echo "$LINE" >> "$FILE" - -# Install extension -[[ ! -z $INSTALL ]] && python -m pip install -r requirements.txt - -# Build JupyterLite -[[ ! -z $BUILD ]] && jupyter lite build --contents content --output-dir dist +cd ../../.. -# Exit with zero (for GH workflow) -exit 0 +npm run build && npm run start -p=8000 From 1deaf959a2773c3b484bacd669906f65bc1a03c3 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 18 Mar 2024 16:24:34 -0700 Subject: [PATCH 10/30] chore: cleanup --- extensions/src/data_bridge/index.ts | 30 ++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index 6c42e42..a67ac4b 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -28,6 +28,18 @@ const plugin: JupyterFrontEndPlugin = { // @ts-ignore app.dataFromHost = ""; + // On JupyterLite startup send get-data message to the host to request data + // @ts-ignore + window.parent.postMessage( + { + type: "from-iframe-to-host", + action: "get-data", + payload: {} + }, + "*" + ); + + /** * Listen for the current notebook being changed, and on kernel status change load the data into the kernel */ @@ -40,7 +52,7 @@ const plugin: JupyterFrontEndPlugin = { sessionContext.session?.kernel?.statusChanged.connect((kernel, status) => { // @ts-ignore - console.debug(status, kernel.id); + console.debug(status, kernel.id, kernel.dataFromHost); // @ts-ignore if (kernel.status === 'idle' && kernel.dataFromHost !== app.dataFromHost) { // @ts-ignore @@ -56,14 +68,12 @@ const plugin: JupyterFrontEndPlugin = { * @param data */ // @ts-ignore - window.sendDataToHost = (data: any) => { + window.sendDataToHost = (data: object) => { window.parent.postMessage( { type: "from-iframe-to-host", action: "set-data", - payload: { - data: data, - }, + payload: data }, "*" ); @@ -76,10 +86,8 @@ const plugin: JupyterFrontEndPlugin = { // @ts-ignore window.addEventListener("message", async (event: MessageEvent) => { if (event.data.type === "from-host-to-iframe") { - let data = event.data.payload.data; - const dataJson = JSON.stringify(data); // @ts-ignore - app.dataFromHost = dataJson; + app.dataFromHost = JSON.stringify(event.data.payload); //@ts-ignore console.debug("Data from host received. app:", app.dataFromHost); // Execute code in the kernel @@ -87,9 +95,9 @@ const plugin: JupyterFrontEndPlugin = { await notebookPanel.sessionContext.ready; const sessionContext = notebookPanel.sessionContext; const kernel = sessionContext.session?.kernel; - loadData(kernel, data); + // @ts-ignore + loadData(kernel, app.dataFromHost); } - }); /** @@ -99,7 +107,7 @@ const plugin: JupyterFrontEndPlugin = { */ const loadData = (kernel: IKernelConnection, data: JSON) => { const dataFromHostString = JSON.stringify(data); - const code = `import json\ndata = json.loads(${dataFromHostString})`; + const code = `import json\ndata_from_host = json.loads(${dataFromHostString})`; // @ts-ignore const result = kernel.requestExecute({code: code}); // @ts-ignore From 8c841d4f5858265cede60e10ef09fc1f1aec6006 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 18 Mar 2024 18:59:11 -0700 Subject: [PATCH 11/30] update: load data on restart --- extensions/src/data_bridge/index.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index a67ac4b..514c658 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -24,12 +24,11 @@ const plugin: JupyterFrontEndPlugin = { ) => { console.log("JupyterLab extension data-bridge is activated!"); - // variable to hold the data from the host page + // Variable to hold the data from the host page, accessible from any notebook and kernel // @ts-ignore app.dataFromHost = ""; // On JupyterLite startup send get-data message to the host to request data - // @ts-ignore window.parent.postMessage( { type: "from-iframe-to-host", @@ -54,11 +53,16 @@ const plugin: JupyterFrontEndPlugin = { // @ts-ignore console.debug(status, kernel.id, kernel.dataFromHost); // @ts-ignore - if (kernel.status === 'idle' && kernel.dataFromHost !== app.dataFromHost) { + if (status === 'idle' && kernel.dataFromHost !== app.dataFromHost) { + // Custom flag to prevent from loading the same data multiple times // @ts-ignore kernel.dataFromHost = app.dataFromHost; loadData(kernel, app.dataFromHost); } + // Reset the flag when the kernel is restarting, since this flag is not affected by the kernel restart + if (status === 'restarting') { + kernel.dataFromHost = ""; + } }); } }); @@ -111,8 +115,9 @@ const plugin: JupyterFrontEndPlugin = { // @ts-ignore const result = kernel.requestExecute({code: code}); // @ts-ignore - console.debug("Execution result", result, app.dataFromHost); + console.debug("Execution result", result); } + }, }; From e0a531db1c6d1799f0975dcb36f6ba191a6ddb32 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:50:16 -0700 Subject: [PATCH 12/30] update: address PR comments --- extensions/src/data_bridge/index.ts | 31 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index 514c658..f39dc30 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -24,17 +24,19 @@ const plugin: JupyterFrontEndPlugin = { ) => { console.log("JupyterLab extension data-bridge is activated!"); - // Variable to hold the data from the host page, accessible from any notebook and kernel + // Reusing the `app` variable to hold the data from the host page, accessible from any notebook and kernel // @ts-ignore app.dataFromHost = ""; + 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( - { - type: "from-iframe-to-host", - action: "get-data", - payload: {} - }, + MESSAGE_GET_DATA_CONTENT, "*" ); @@ -50,11 +52,9 @@ const plugin: JupyterFrontEndPlugin = { const sessionContext = notebookPanel.sessionContext; sessionContext.session?.kernel?.statusChanged.connect((kernel, status) => { - // @ts-ignore - console.debug(status, kernel.id, kernel.dataFromHost); // @ts-ignore if (status === 'idle' && kernel.dataFromHost !== app.dataFromHost) { - // Custom flag to prevent from loading the same data multiple times + // Save previous data inside the current kernel to avoid reloading the same data // @ts-ignore kernel.dataFromHost = app.dataFromHost; loadData(kernel, app.dataFromHost); @@ -73,12 +73,13 @@ const plugin: JupyterFrontEndPlugin = { */ // @ts-ignore window.sendDataToHost = (data: object) => { + const MESSAGE_SET_DATA_CONTENT = { + type: "from-iframe-to-host", + action: "set-data", + payload: data + }; window.parent.postMessage( - { - type: "from-iframe-to-host", - action: "set-data", - payload: data - }, + MESSAGE_SET_DATA_CONTENT, "*" ); }; @@ -92,8 +93,6 @@ const plugin: JupyterFrontEndPlugin = { if (event.data.type === "from-host-to-iframe") { // @ts-ignore app.dataFromHost = JSON.stringify(event.data.payload); - //@ts-ignore - console.debug("Data from host received. app:", app.dataFromHost); // Execute code in the kernel const notebookPanel = notebookTracker.currentWidget; await notebookPanel.sessionContext.ready; From 2e399ae1953b2968856cd848db1048e1dc72a9ed Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 19 Mar 2024 15:36:02 -0700 Subject: [PATCH 13/30] chore: add types gymnastics --- extensions/src/data_bridge/index.ts | 141 ++++++++++++++-------------- 1 file changed, 70 insertions(+), 71 deletions(-) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index f39dc30..f00cd9a 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -1,71 +1,75 @@ -// @ts-nocheck +/* eslint-disable @typescript-eslint/ban-ts-comment */ import { JupyterFrontEnd, - JupyterFrontEndPlugin, -} from "@jupyterlab/application"; + JupyterFrontEndPlugin +} from '@jupyterlab/application'; -import {NotebookPanel, INotebookTracker, NotebookAdapter} from "@jupyterlab/notebook"; -import {IframeMessageSchema} from "@mat3ra/esse/lib/js/types"; +import { IKernelConnection } from '@jupyterlab/services/lib/kernel/kernel'; +import { NotebookPanel, INotebookTracker } from '@jupyterlab/notebook'; +import { IframeMessageSchema } from '@mat3ra/esse/lib/js/types'; + +interface IExtendedJupyterFrontEnd extends JupyterFrontEnd { + dataFromHost: string; +} /** * Initialization data for the data-bridge extension. * Similar to https://jupyterlite.readthedocs.io/en/latest/howto/configure/advanced/iframe.html */ const plugin: JupyterFrontEndPlugin = { - id: "data-bridge:plugin", + id: 'data-bridge:plugin', description: - "Extension to pass JSON data between host page and Jupyter Lite instance", + 'Extension to pass JSON data between host page and Jupyter Lite instance', autoStart: true, requires: [INotebookTracker], - activate: async ( - app: JupyterFrontEnd, - notebookTracker: INotebookTracker, - notebookAdapter: NotebookAdapter - ) => { - console.log("JupyterLab extension data-bridge is activated!"); - + activate: async (app: JupyterFrontEnd, notebookTracker: INotebookTracker) => { + console.log('JupyterLab extension data-bridge is activated!'); + const extendedApp = app as IExtendedJupyterFrontEnd; // Reusing the `app` variable to hold the data from the host page, accessible from any notebook and kernel - // @ts-ignore - app.dataFromHost = ""; + extendedApp.dataFromHost = ''; const MESSAGE_GET_DATA_CONTENT = { - type: "from-iframe-to-host", - action: "get-data", + 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, - "*" - ); - + 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 */ - // @ts-ignore - notebookTracker.currentChanged.connect(async (sender, notebookPanel: NotebookPanel) => { - if (notebookPanel) { - console.debug("Notebook opened", notebookPanel.context.path); - await notebookPanel.sessionContext.ready; - const sessionContext = notebookPanel.sessionContext; + 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) => { - // @ts-ignore - if (status === 'idle' && kernel.dataFromHost !== app.dataFromHost) { - // Save previous data inside the current kernel to avoid reloading the same data - // @ts-ignore - kernel.dataFromHost = app.dataFromHost; - loadData(kernel, app.dataFromHost); - } - // Reset the flag when the kernel is restarting, since this flag is not affected by the kernel restart - if (status === 'restarting') { - kernel.dataFromHost = ""; - } - }); + sessionContext.session?.kernel?.statusChanged.connect( + (kernel, status) => { + if ( + status === 'idle' && + // @ts-ignore + kernel.dataFromHost !== extendedApp.dataFromHost + ) { + // Save previous data inside the current kernel to avoid reloading the same data + // @ts-ignore + kernel.dataFromHost = extendedApp.dataFromHost; + loadData(kernel, extendedApp.dataFromHost); + } + // Reset the flag when the kernel is restarting, since this flag is not affected by the kernel restart + if (status === 'restarting') { + // @ts-ignore + kernel.dataFromHost = ''; + } + } + ); + } } - }); + ); /** * Send data to the host page @@ -74,14 +78,11 @@ const plugin: JupyterFrontEndPlugin = { // @ts-ignore window.sendDataToHost = (data: object) => { const MESSAGE_SET_DATA_CONTENT = { - type: "from-iframe-to-host", - action: "set-data", + type: 'from-iframe-to-host', + action: 'set-data', payload: data }; - window.parent.postMessage( - MESSAGE_SET_DATA_CONTENT, - "*" - ); + window.parent.postMessage(MESSAGE_SET_DATA_CONTENT, '*'); }; /** @@ -89,36 +90,34 @@ const plugin: JupyterFrontEndPlugin = { * @param event MessageEvent */ // @ts-ignore - window.addEventListener("message", async (event: MessageEvent) => { - if (event.data.type === "from-host-to-iframe") { - // @ts-ignore - app.dataFromHost = JSON.stringify(event.data.payload); - // Execute code in the kernel - const notebookPanel = notebookTracker.currentWidget; - await notebookPanel.sessionContext.ready; - const sessionContext = notebookPanel.sessionContext; - const kernel = sessionContext.session?.kernel; - // @ts-ignore - loadData(kernel, app.dataFromHost); + window.addEventListener( + 'message', + async (event: MessageEvent) => { + if (event.data.type === 'from-host-to-iframe') { + extendedApp.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) { + loadData(kernel, extendedApp.dataFromHost); + } + } } - }); + ); /** * Load the data into the kernel by executing code * @param kernel - * @param data + * @param data string representation of JSON */ - const loadData = (kernel: IKernelConnection, data: JSON) => { + const loadData = (kernel: IKernelConnection, data: string) => { const dataFromHostString = JSON.stringify(data); const code = `import json\ndata_from_host = json.loads(${dataFromHostString})`; - // @ts-ignore - const result = kernel.requestExecute({code: code}); - // @ts-ignore - console.debug("Execution result", result); - } - - }, + const result = kernel.requestExecute({ code: code }); + console.debug('Execution result', result); + }; + } }; - export default plugin; From 28f2127f596903edfc86f0b5f2eed642b44bd3ec Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 19 Mar 2024 15:50:56 -0700 Subject: [PATCH 14/30] chore: remove stringify: --- extensions/src/data_bridge/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index f00cd9a..f3e79f8 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -89,12 +89,11 @@ const plugin: JupyterFrontEndPlugin = { * Listen for messages from the host page, and update the data in the kernel * @param event MessageEvent */ - // @ts-ignore window.addEventListener( 'message', async (event: MessageEvent) => { if (event.data.type === 'from-host-to-iframe') { - extendedApp.dataFromHost = JSON.stringify(event.data.payload); + extendedApp.dataFromHost = event.data.payload; const notebookPanel = notebookTracker.currentWidget; await notebookPanel?.sessionContext.ready; const sessionContext = notebookPanel?.sessionContext; From f6d9b533e470eed624f61d5507591d282586fab8 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 19 Mar 2024 15:51:15 -0700 Subject: [PATCH 15/30] update: add install of esse --- setup.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/setup.sh b/setup.sh index 6a43870..260d511 100644 --- a/setup.sh +++ b/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. @@ -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 @@ -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 . @@ -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 From c272e8277193dbda119dd66780fce2f9ab47ed93 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 19 Mar 2024 15:51:32 -0700 Subject: [PATCH 16/30] chore: add description --- update.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/update.sh b/update.sh index a062196..fb05e07 100644 --- a/update.sh +++ b/update.sh @@ -1,4 +1,6 @@ #!/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 From 750fe2a1ff6638b1f8daf9f93df6b51f1f4a9164 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 19 Mar 2024 16:25:04 -0700 Subject: [PATCH 17/30] chore --- extensions/src/data_bridge/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index f3e79f8..d9121db 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -93,7 +93,7 @@ const plugin: JupyterFrontEndPlugin = { 'message', async (event: MessageEvent) => { if (event.data.type === 'from-host-to-iframe') { - extendedApp.dataFromHost = event.data.payload; + extendedApp.dataFromHost = JSON.stringify(event.data.payload); const notebookPanel = notebookTracker.currentWidget; await notebookPanel?.sessionContext.ready; const sessionContext = notebookPanel?.sessionContext; From 92f923d7efe420e6ac4a31f440a2ff140a76f58d Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 19 Mar 2024 16:58:39 -0700 Subject: [PATCH 18/30] chore: explain second stringification --- extensions/src/data_bridge/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index d9121db..070c807 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -111,6 +111,7 @@ const plugin: JupyterFrontEndPlugin = { * @param data string representation of JSON */ const loadData = (kernel: IKernelConnection, data: string) => { + // Stringify the data again to escape quotes and other special characters, so that this string can be used directly in Python code const dataFromHostString = JSON.stringify(data); const code = `import json\ndata_from_host = json.loads(${dataFromHostString})`; const result = kernel.requestExecute({ code: code }); From c8d36ae13af6471121f314b6ae6862b3b935790b Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 19 Mar 2024 17:16:49 -0700 Subject: [PATCH 19/30] try: add no-check --- extensions/src/data_bridge/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index 070c807..720bc0f 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ +// @ts-nocheck import { JupyterFrontEnd, JupyterFrontEndPlugin From 6beba60a89fd3b0569ae467776ca96028219c573 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 19 Mar 2024 19:12:25 -0700 Subject: [PATCH 20/30] udpate: write data to separate variable --- extensions/src/data_bridge/index.ts | 36 +++++++++++------------------ 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index 720bc0f..aa9d4b2 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -// @ts-nocheck import { JupyterFrontEnd, JupyterFrontEndPlugin @@ -9,10 +8,6 @@ import { IKernelConnection } from '@jupyterlab/services/lib/kernel/kernel'; import { NotebookPanel, INotebookTracker } from '@jupyterlab/notebook'; import { IframeMessageSchema } from '@mat3ra/esse/lib/js/types'; -interface IExtendedJupyterFrontEnd extends JupyterFrontEnd { - dataFromHost: string; -} - /** * Initialization data for the data-bridge extension. * Similar to https://jupyterlite.readthedocs.io/en/latest/howto/configure/advanced/iframe.html @@ -25,9 +20,11 @@ const plugin: JupyterFrontEndPlugin = { requires: [INotebookTracker], activate: async (app: JupyterFrontEnd, notebookTracker: INotebookTracker) => { console.log('JupyterLab extension data-bridge is activated!'); - const extendedApp = app as IExtendedJupyterFrontEnd; - // Reusing the `app` variable to hold the data from the host page, accessible from any notebook and kernel - extendedApp.dataFromHost = ''; + + // 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 } = {}; const MESSAGE_GET_DATA_CONTENT = { type: 'from-iframe-to-host', @@ -53,18 +50,15 @@ const plugin: JupyterFrontEndPlugin = { (kernel, status) => { if ( status === 'idle' && - // @ts-ignore - kernel.dataFromHost !== extendedApp.dataFromHost + kernelsDataFromHost[kernel.id] !== dataFromHost ) { - // Save previous data inside the current kernel to avoid reloading the same data - // @ts-ignore - kernel.dataFromHost = extendedApp.dataFromHost; - loadData(kernel, extendedApp.dataFromHost); + // Save data for the current kernel to avoid reloading the same data + kernelsDataFromHost[kernel.id] = dataFromHost; + loadData(kernel, dataFromHost); } - // Reset the flag when the kernel is restarting, since this flag is not affected by the kernel restart + // Reset the data when the kernel is restarting, since the loaded data is lost if (status === 'restarting') { - // @ts-ignore - kernel.dataFromHost = ''; + kernelsDataFromHost[kernel.id] = ''; } } ); @@ -94,13 +88,13 @@ const plugin: JupyterFrontEndPlugin = { 'message', async (event: MessageEvent) => { if (event.data.type === 'from-host-to-iframe') { - extendedApp.dataFromHost = JSON.stringify(event.data.payload); + 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) { - loadData(kernel, extendedApp.dataFromHost); + loadData(kernel, dataFromHost); } } } @@ -112,9 +106,7 @@ const plugin: JupyterFrontEndPlugin = { * @param data string representation of JSON */ const loadData = (kernel: IKernelConnection, data: string) => { - // Stringify the data again to escape quotes and other special characters, so that this string can be used directly in Python code - const dataFromHostString = JSON.stringify(data); - const code = `import json\ndata_from_host = json.loads(${dataFromHostString})`; + const code = `import json\ndata_from_host = json.loads(${data})`; const result = kernel.requestExecute({ code: code }); console.debug('Execution result', result); }; From c7fe9225a58cf04b431bb5016402a8895699d046 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 19 Mar 2024 21:10:16 -0700 Subject: [PATCH 21/30] chore: add ts-ignore --- extensions/src/data_bridge/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index aa9d4b2..d605b6c 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ +// @ts-ignore import { JupyterFrontEnd, JupyterFrontEndPlugin From 9b7a2e169903e5ebbcd28ce43d4c9ae7a35c299b Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 19 Mar 2024 21:13:57 -0700 Subject: [PATCH 22/30] chore: add no-check --- extensions/src/data_bridge/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index d605b6c..ba174bd 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -// @ts-ignore +// @ts-no-check import { JupyterFrontEnd, JupyterFrontEndPlugin From f4b10842a80e12c8f964c05593af86f74412efc0 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 19 Mar 2024 21:20:39 -0700 Subject: [PATCH 23/30] chore: fix --- extensions/src/data_bridge/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index ba174bd..c690849 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -53,9 +53,9 @@ const plugin: JupyterFrontEndPlugin = { 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; - loadData(kernel, dataFromHost); } // Reset the data when the kernel is restarting, since the loaded data is lost if (status === 'restarting') { @@ -107,9 +107,9 @@ const plugin: JupyterFrontEndPlugin = { * @param data string representation of JSON */ const loadData = (kernel: IKernelConnection, data: string) => { - const code = `import json\ndata_from_host = json.loads(${data})`; + const code = `import json\ndata_from_host = json.loads('${data}')`; const result = kernel.requestExecute({ code: code }); - console.debug('Execution result', result); + console.debug('Execution result:', result); }; } }; From f3a4c8c632acdee6c09c67c55187f6ff1f8a35f6 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 20 Mar 2024 09:15:00 -0700 Subject: [PATCH 24/30] update: fix a typo --- extensions/src/data_bridge/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index c690849..ed9764c 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -// @ts-no-check +// @ts-nocheck import { JupyterFrontEnd, JupyterFrontEndPlugin From c7f7647b9c6e3326c994c63af07daa2fb018d208 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 20 Mar 2024 09:48:49 -0700 Subject: [PATCH 25/30] api-examples++: adjust get_data --- content/api-examples | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/api-examples b/content/api-examples index c5931e6..43b4eff 160000 --- a/content/api-examples +++ b/content/api-examples @@ -1 +1 @@ -Subproject commit c5931e6babe84c8448837c95dee3abb146ff7fb4 +Subproject commit 43b4eff0afc87f337131fb896eaa503629a63c5b From da986a03a4dbe02c9fd25b61a243884c4b1c36df Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 20 Mar 2024 18:01:06 -0700 Subject: [PATCH 26/30] api-examples++ --- content/api-examples | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/api-examples b/content/api-examples index 43b4eff..858efe9 160000 --- a/content/api-examples +++ b/content/api-examples @@ -1 +1 @@ -Subproject commit 43b4eff0afc87f337131fb896eaa503629a63c5b +Subproject commit 858efe957a18f20d674db7eeb9186e4b75e8683c From bdd4901749cd35cf16a9d9530a471b4eeb0177b4 Mon Sep 17 00:00:00 2001 From: Timur Bazhirov Date: Wed, 20 Mar 2024 18:33:06 -0700 Subject: [PATCH 27/30] Update package.json --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 0f532bc..abd3ffe 100644 --- a/package.json +++ b/package.json @@ -3,5 +3,4 @@ "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" } - } From 3c523bbd5191409637140106a9728bf5bbf0e2e0 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 20 Mar 2024 20:11:25 -0700 Subject: [PATCH 28/30] update: direct to using npm --- README.md | 8 ++++---- package.json | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2b25195..08e6fcf 100644 --- a/README.md +++ b/README.md @@ -32,16 +32,16 @@ For more info, keep an eye on the JupyterLite documentation: To build and run the JupyterLite server with extension, we use the following steps: - check that `pyenv` and `npm` are installed -- build `data_bridge` extension running the `sh setup.sh` -- pass `INSTALL=1 BUILD=1` to also build and install the jupyter lite with extension +- 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 - 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 `sh setup.sh` to update the extension +- run `npm install` or `sh setup.sh` to create the extension - change code in `extensions/dist/data_bridge/src/index.ts` -- run `sh update.sh` to build the extension, install it, and start the server with it +- run `npm run restart` or `sh update.sh` to build the extension, install it, and restart the server with it To publish: - commit changes to the `extensions/src/data_bridge/index.ts` file diff --git a/package.json b/package.json index 0f532bc..ad567e5 100644 --- a/package.json +++ b/package.json @@ -1,7 +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" } - } From 1d4c22aa49efd5004f0e436a1209141873bb00e3 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 20 Mar 2024 21:00:49 -0700 Subject: [PATCH 29/30] chore: replace quotes single->double --- extensions/src/data_bridge/index.ts | 44 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index ed9764c..5897460 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -3,38 +3,38 @@ import { JupyterFrontEnd, JupyterFrontEndPlugin -} from '@jupyterlab/application'; +} 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'; +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. * Similar to https://jupyterlite.readthedocs.io/en/latest/howto/configure/advanced/iframe.html */ const plugin: JupyterFrontEndPlugin = { - id: 'data-bridge:plugin', + id: "data-bridge:plugin", description: - 'Extension to pass JSON data between host page and Jupyter Lite instance', + "Extension to pass JSON data between host page and Jupyter Lite instance", autoStart: true, requires: [INotebookTracker], activate: async (app: JupyterFrontEnd, notebookTracker: INotebookTracker) => { - console.log('JupyterLab extension data-bridge is activated!'); + console.log("JupyterLab extension data-bridge is activated!"); // Variable to hold the data from the host page - let dataFromHost = ''; + 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 } = {}; const MESSAGE_GET_DATA_CONTENT = { - type: 'from-iframe-to-host', - action: 'get-data', + 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, '*'); + 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 @@ -43,14 +43,14 @@ const plugin: JupyterFrontEndPlugin = { // @ts-ignore async (sender, notebookPanel: NotebookPanel) => { if (notebookPanel) { - console.debug('Notebook opened', notebookPanel.context.path); + 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' && + status === "idle" && kernelsDataFromHost[kernel.id] !== dataFromHost ) { loadData(kernel, dataFromHost); @@ -58,8 +58,8 @@ const plugin: JupyterFrontEndPlugin = { kernelsDataFromHost[kernel.id] = dataFromHost; } // Reset the data when the kernel is restarting, since the loaded data is lost - if (status === 'restarting') { - kernelsDataFromHost[kernel.id] = ''; + if (status === "restarting") { + kernelsDataFromHost[kernel.id] = ""; } } ); @@ -74,11 +74,11 @@ const plugin: JupyterFrontEndPlugin = { // @ts-ignore window.sendDataToHost = (data: object) => { const MESSAGE_SET_DATA_CONTENT = { - type: 'from-iframe-to-host', - action: 'set-data', + type: "from-iframe-to-host", + action: "set-data", payload: data }; - window.parent.postMessage(MESSAGE_SET_DATA_CONTENT, '*'); + window.parent.postMessage(MESSAGE_SET_DATA_CONTENT, "*"); }; /** @@ -86,9 +86,9 @@ const plugin: JupyterFrontEndPlugin = { * @param event MessageEvent */ window.addEventListener( - 'message', + "message", async (event: MessageEvent) => { - if (event.data.type === 'from-host-to-iframe') { + if (event.data.type === "from-host-to-iframe") { dataFromHost = JSON.stringify(event.data.payload); const notebookPanel = notebookTracker.currentWidget; await notebookPanel?.sessionContext.ready; @@ -107,9 +107,9 @@ const plugin: JupyterFrontEndPlugin = { * @param data string representation of JSON */ const loadData = (kernel: IKernelConnection, data: string) => { - const code = `import json\ndata_from_host = json.loads('${data}')`; + const code = `import json\ndata_from_host = json.loads("${data}")`; const result = kernel.requestExecute({ code: code }); - console.debug('Execution result:', result); + console.debug("Execution result:", result); }; } }; From 5c95b76c1c1b13fd6ef57a1aea637f07bdd0c5e1 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 20 Mar 2024 21:15:48 -0700 Subject: [PATCH 30/30] chore: revert erroneous --- extensions/src/data_bridge/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/src/data_bridge/index.ts b/extensions/src/data_bridge/index.ts index 5897460..78ed27e 100644 --- a/extensions/src/data_bridge/index.ts +++ b/extensions/src/data_bridge/index.ts @@ -107,7 +107,7 @@ const plugin: JupyterFrontEndPlugin = { * @param data string representation of JSON */ const loadData = (kernel: IKernelConnection, data: string) => { - const code = `import json\ndata_from_host = json.loads("${data}")`; + const code = `import json\ndata_from_host = json.loads('${data}')`; const result = kernel.requestExecute({ code: code }); console.debug("Execution result:", result); };