Skip to content

Commit 4cbfd85

Browse files
committed
Send execution requests from frontend
1 parent 00338cb commit 4cbfd85

File tree

9 files changed

+152
-62
lines changed

9 files changed

+152
-62
lines changed

packages/voila/src/plugins/outputs/plugins.ts

Lines changed: 21 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ import { Widget } from '@lumino/widgets';
1717
import { VoilaApp } from '../../app';
1818
import { RenderedCells } from './renderedcells';
1919
import {
20+
createOutputArea,
2021
createSkeleton,
22+
executeCode,
2123
getExecutionURL,
2224
handleExecutionResult,
2325
IExecutionMessage,
2426
IReceivedWidgetModel
2527
} from './tools';
28+
import { SimplifiedOutputArea } from '@jupyterlab/outputarea';
2629

2730
/**
2831
* The plugin that renders outputs.
@@ -115,53 +118,25 @@ export const renderOutputsProgressivelyPlugin: JupyterFrontEndPlugin<void> = {
115118
return;
116119
}
117120

118-
const kernelId = widgetManager.kernel.id;
119-
120-
const receivedWidgetModel: IReceivedWidgetModel = {};
121-
const modelRegisteredHandler = (_: any, modelId: string) => {
122-
if (receivedWidgetModel[modelId]) {
123-
const { outputModel, executionModel } = receivedWidgetModel[modelId];
124-
outputModel.add(executionModel);
125-
widgetManager.removeRegisteredModel(modelId);
126-
}
127-
};
128-
widgetManager.modelRegistered.connect(modelRegisteredHandler);
129-
const wsUrl = getExecutionURL(kernelId);
130-
const ws = new WebSocket(wsUrl);
131-
ws.onmessage = async (msg) => {
132-
const { action, payload }: IExecutionMessage = JSON.parse(msg.data);
133-
switch (action) {
134-
case 'execution_result': {
135-
const result = handleExecutionResult({
136-
payload,
137-
rendermime,
138-
widgetManager
139-
});
140-
if (result) {
141-
Object.entries(result).forEach(([key, val]) => {
142-
receivedWidgetModel[key] = val;
143-
});
144-
}
145-
const { cell_index, total_cell } = payload;
146-
if (cell_index + 1 === total_cell) {
147-
// Executed all cells
148-
ws.close();
121+
const cells = document.querySelectorAll('[cell-index]');
122+
const cellsNumber = cells.length;
123+
for (let cellIdx = 0; cellIdx < cellsNumber; cellIdx++) {
124+
const el = cells.item(cellIdx);
125+
const codeCell = el.getElementsByClassName('jp-CodeCell').item(0);
126+
if (codeCell) {
127+
// codeCell.classList.remove('jp-mod-noOutputs', 'jp-mod-noInput');
128+
129+
const { area } = createOutputArea({ rendermime, parent: codeCell });
130+
const source = `${cellIdx}`;
131+
executeCode(source, area, widgetManager.kernel).then((future) => {
132+
const skeleton = el
133+
.getElementsByClassName('voila-skeleton-container')
134+
.item(0);
135+
if (skeleton) {
136+
el.removeChild(skeleton);
149137
}
150-
151-
break;
152-
}
153-
case 'execution_error': {
154-
console.error(`Execution error: ${payload.error}`);
155-
break;
156-
}
157-
default:
158-
break;
138+
});
159139
}
160-
};
161-
ws.onopen = () => {
162-
ws.send(
163-
JSON.stringify({ action: 'execute', payload: { kernel_id: kernelId } })
164-
);
165-
};
140+
}
166141
}
167142
};

packages/voila/src/plugins/outputs/tools.ts

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import { IOutput } from '@jupyterlab/nbformat';
2-
import { OutputAreaModel, SimplifiedOutputArea } from '@jupyterlab/outputarea';
2+
import {
3+
OutputArea,
4+
OutputAreaModel,
5+
SimplifiedOutputArea
6+
} from '@jupyterlab/outputarea';
37
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
48
import { Widget } from '@lumino/widgets';
59
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
610
import { type VoilaWidgetManager } from '@voila-dashboards/widgets-manager8/lib/manager';
11+
import { Kernel, KernelMessage } from '@jupyterlab/services';
12+
import { JSONObject } from '@lumino/coreutils';
713

814
/**
915
* Interface representing the structure of an execution result message.
@@ -74,7 +80,7 @@ export function handleExecutionResult({
7480
if (skeleton) {
7581
element.removeChild(skeleton);
7682
}
77-
const model = createOutputArea({ rendermime, parent: element });
83+
const { model } = createOutputArea({ rendermime, parent: element });
7884
if (!output_cell.outputs) {
7985
return;
8086
}
@@ -118,7 +124,7 @@ export function createOutputArea({
118124
}: {
119125
rendermime: IRenderMimeRegistry;
120126
parent: Element;
121-
}): OutputAreaModel {
127+
}): { model: OutputAreaModel; area: SimplifiedOutputArea } {
122128
const model = new OutputAreaModel({ trusted: true });
123129
const area = new SimplifiedOutputArea({
124130
model,
@@ -134,14 +140,19 @@ export function createOutputArea({
134140
'jp-Cell-outputCollapser'
135141
);
136142
wrapper.appendChild(collapser);
137-
parent.lastElementChild?.appendChild(wrapper);
143+
parent.appendChild(wrapper);
138144
area.node.classList.add('jp-Cell-outputArea');
139145

140146
area.node.style.display = 'flex';
141147
area.node.style.flexDirection = 'column';
142148

143149
Widget.attach(area, wrapper);
144-
return model;
150+
area.outputLengthChanged.connect((_, number) => {
151+
if (number) {
152+
parent.classList.remove('jp-mod-noOutputs');
153+
}
154+
});
155+
return { model, area };
145156
}
146157

147158
export function createSkeleton(): void {
@@ -152,9 +163,40 @@ export function createSkeleton(): void {
152163
</div>`;
153164
const elements = document.querySelectorAll('[cell-index]');
154165
elements.forEach((it) => {
155-
const element = document.createElement('div');
156-
element.className = 'voila-skeleton-container';
157-
element.innerHTML = innerHtml;
158-
it.appendChild(element);
166+
const codeCell = it.getElementsByClassName('jp-CodeCell').item(0);
167+
if (codeCell) {
168+
const element = document.createElement('div');
169+
element.className = 'voila-skeleton-container';
170+
element.innerHTML = innerHtml;
171+
it.appendChild(element);
172+
}
159173
});
160174
}
175+
176+
export async function executeCode(
177+
code: string,
178+
output: OutputArea,
179+
kernel: Kernel.IKernelConnection | null | undefined,
180+
metadata?: JSONObject
181+
): Promise<KernelMessage.IExecuteReplyMsg | undefined> {
182+
// Override the default for `stop_on_error`.
183+
let stopOnError = false;
184+
if (
185+
metadata &&
186+
Array.isArray(metadata.tags) &&
187+
metadata.tags.indexOf('raises-exception') !== -1
188+
) {
189+
stopOnError = false;
190+
}
191+
const content: KernelMessage.IExecuteRequestMsg['content'] = {
192+
code,
193+
stop_on_error: stopOnError
194+
};
195+
196+
if (!kernel) {
197+
throw new Error('Session has no kernel.');
198+
}
199+
const future = kernel.requestExecute(content, false, metadata);
200+
output.future = future;
201+
return future.done;
202+
}

share/jupyter/voila/templates/classic/index.html.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
<style>
4545
/*Hide empty cells*/
4646
.jp-mod-noOutputs.jp-mod-noInput {
47-
display: none;
47+
padding: 0!important;
4848
}
4949
</style>
5050

share/jupyter/voila/templates/lab/index.html.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<style>
2020
/*Hide empty cells*/
2121
.jp-mod-noOutputs.jp-mod-noInput {
22-
display: none;
22+
padding: 0!important;
2323
}
2424
</style>
2525
{% if resources.show_margins %}

share/jupyter/voila/templates/reveal/index.html.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
<style>
4646
/*Hide empty cells*/
4747
.jp-mod-noOutputs.jp-mod-noInput {
48-
display: none;
48+
padding: 0!important;
4949
}
5050
#rendered_cells {
5151
padding: 0px!important

tsconfigbase.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"moduleResolution": "node",
1111
"noEmitOnError": true,
1212
"noImplicitAny": true,
13-
"noUnusedLocals": true,
13+
"noUnusedLocals": false,
1414
"preserveWatchOutput": true,
1515
"resolveJsonModule": true,
1616
"strict": true,

voila/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,7 @@ def init_settings(self) -> Dict:
653653
connection_dir=self.connection_dir,
654654
kernel_spec_manager=self.kernel_spec_manager,
655655
allowed_message_types=[
656+
"execute_request",
656657
"comm_open",
657658
"comm_close",
658659
"comm_msg",

voila/handler.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from tornado.httputil import split_host_and_port
2020
from traitlets.traitlets import Bool
2121

22-
from voila.tornado.execution_request_handler import ExecutionRequestHandler
22+
from voila.tornado.kernel_websocket_handler import VoilaKernelWebsocketHandler
2323

2424
from .configuration import VoilaConfiguration
2525

@@ -253,8 +253,8 @@ def time_out():
253253
kernel_future = self.kernel_manager.get_kernel(kernel_id)
254254
queue = asyncio.Queue()
255255
if self.voila_configuration.progressive_rendering:
256-
ExecutionRequestHandler._execution_data[kernel_id] = {
257-
"nb": gen.notebook,
256+
VoilaKernelWebsocketHandler._execution_data[kernel_id] = {
257+
"cells": gen.notebook.cells,
258258
"config": self.traitlet_config,
259259
"show_tracebacks": self.voila_configuration.show_tracebacks,
260260
}

voila/tornado/kernel_websocket_handler.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
from jupyter_server.services.kernels.websocket import (
66
KernelWebsocketHandler as WebsocketHandler,
77
)
8+
from jupyter_server.services.kernels.connection.base import (
9+
deserialize_binary_message,
10+
deserialize_msg_from_ws_v1,
11+
)
812
except ImportError:
913
from jupyter_server.services.kernels.handlers import (
1014
ZMQChannelsHandler as WebsocketHandler,
@@ -27,9 +31,77 @@ def read_header_from_binary_message(ws_msg: bytes) -> Optional[Dict]:
2731

2832

2933
class VoilaKernelWebsocketHandler(WebsocketHandler):
34+
35+
_execution_data = {}
36+
37+
def on_message(self, ws_msg):
38+
connection = self.connection
39+
subprotocol = connection.subprotocol
40+
if not connection.channels:
41+
# already closed, ignore the message
42+
connection.log.debug("Received message on closed websocket %r", ws_msg)
43+
return
44+
45+
if subprotocol == "v1.kernel.websocket.jupyter.org":
46+
channel, msg_list = deserialize_msg_from_ws_v1(ws_msg)
47+
msg = {"header": None, "content": None}
48+
else:
49+
if isinstance(ws_msg, bytes): # type:ignore[unreachable]
50+
msg = deserialize_binary_message(ws_msg) # type:ignore[unreachable]
51+
else:
52+
msg = json.loads(ws_msg)
53+
msg_list = []
54+
channel = msg.pop("channel", None)
55+
56+
if channel is None:
57+
connection.log.warning("No channel specified, assuming shell: %s", msg)
58+
channel = "shell"
59+
if channel not in connection.channels:
60+
connection.log.warning("No such channel: %r", channel)
61+
return
62+
am = connection.multi_kernel_manager.allowed_message_types
63+
ignore_msg = False
64+
msg_header = connection.get_part("header", msg["header"], msg_list)
65+
msg_content = connection.get_part("content", msg["content"], msg_list)
66+
if msg_header["msg_type"] == "execute_request":
67+
execution_data = self._execution_data.get(self.kernel_id, None)
68+
cells = execution_data["cells"]
69+
code = msg_content.get("code")
70+
try:
71+
cell_idx = int(code)
72+
cell = cells[cell_idx]
73+
if cell["cell_type"] != "code":
74+
cell["source"] = ""
75+
76+
if subprotocol == "v1.kernel.websocket.jupyter.org":
77+
msg_content["code"] = cell["source"]
78+
msg_list[3] = connection.session.pack(msg_content)
79+
else:
80+
msg["content"]["code"] = cell["source"]
81+
82+
except Exception:
83+
connection.log.warning("Unsupported code cell %s" % code)
84+
85+
if am:
86+
msg["header"] = connection.get_part("header", msg["header"], msg_list)
87+
assert msg["header"] is not None
88+
if msg["header"]["msg_type"] not in am: # type:ignore[unreachable]
89+
connection.log.warning(
90+
'Received message of type "%s", which is not allowed. Ignoring.'
91+
% msg["header"]["msg_type"]
92+
)
93+
ignore_msg = True
94+
if not ignore_msg:
95+
stream = connection.channels[channel]
96+
if subprotocol == "v1.kernel.websocket.jupyter.org":
97+
connection.session.send_raw(stream, msg_list)
98+
else:
99+
connection.session.send(stream, msg)
100+
30101
def write_message(
31102
self, message: Union[bytes, Dict[str, Any]], binary: bool = False
32103
):
104+
33105
if isinstance(message, bytes):
34106
header = read_header_from_binary_message(message)
35107
elif isinstance(message, dict):

0 commit comments

Comments
 (0)