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

Feature/SOF-7286 #12

Merged
merged 32 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a7e7ae8
update: adjust data to new esse schema
VsevolodX Mar 13, 2024
1a2af62
chore: import esse from commit hash
VsevolodX Mar 13, 2024
20faa84
update: adjust messages for schema
VsevolodX Mar 16, 2024
9a28c4f
update: listen to kernel status change
VsevolodX Mar 17, 2024
9c860dd
update: materials lodaded to data variable when selected materials ch…
VsevolodX Mar 18, 2024
fab7e04
update: materials lodaded to data variable when selected materials ch…
VsevolodX Mar 18, 2024
66fc33c
chore: add JSdoc
VsevolodX Mar 18, 2024
23ce79a
chore: explain process in README
VsevolodX Mar 18, 2024
0a99550
chore: add update.sh
VsevolodX Mar 18, 2024
1deaf95
chore: cleanup
VsevolodX Mar 18, 2024
8c841d4
update: load data on restart
VsevolodX Mar 19, 2024
2f6f9af
Merge branch 'main' into feature/SOF-7286
VsevolodX Mar 19, 2024
e0a531d
update: address PR comments
VsevolodX Mar 19, 2024
2e399ae
chore: add types gymnastics
VsevolodX Mar 19, 2024
28f2127
chore: remove stringify:
VsevolodX Mar 19, 2024
f6d9b53
update: add install of esse
VsevolodX Mar 19, 2024
c272e82
chore: add description
VsevolodX Mar 19, 2024
750fe2a
chore
VsevolodX Mar 19, 2024
92f923d
chore: explain second stringification
VsevolodX Mar 19, 2024
c8d36ae
try: add no-check
VsevolodX Mar 20, 2024
6beba60
udpate: write data to separate variable
VsevolodX Mar 20, 2024
c7fe922
chore: add ts-ignore
VsevolodX Mar 20, 2024
9b7a2e1
chore: add no-check
VsevolodX Mar 20, 2024
f4b1084
chore: fix
VsevolodX Mar 20, 2024
f3a4c8c
update: fix a typo
VsevolodX Mar 20, 2024
c7f7647
api-examples++: adjust get_data
VsevolodX Mar 20, 2024
da986a0
api-examples++
VsevolodX Mar 21, 2024
bdd4901
Update package.json
timurbazhirov Mar 21, 2024
3c523bb
update: direct to using npm
VsevolodX Mar 21, 2024
0474588
Merge remote-tracking branch 'refs/remotes/origin/feature/SOF-7286' i…
VsevolodX Mar 21, 2024
1d4c22a
chore: replace quotes single->double
VsevolodX Mar 21, 2024
5c95b76
chore: revert erroneous
VsevolodX Mar 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash
timurbazhirov marked this conversation as resolved.
Show resolved Hide resolved
# 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