Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 19 additions & 53 deletions packages/voila/src/plugins/outputs/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,7 @@ import { Widget } from '@lumino/widgets';

import { VoilaApp } from '../../app';
import { RenderedCells } from './renderedcells';
import {
createSkeleton,
getExecutionURL,
handleExecutionResult,
IExecutionMessage,
IReceivedWidgetModel
} from './tools';
import { createOutputArea, createSkeleton, executeCode } from './tools';

/**
* The plugin that renders outputs.
Expand Down Expand Up @@ -115,53 +109,25 @@ export const renderOutputsProgressivelyPlugin: JupyterFrontEndPlugin<void> = {
return;
}

const kernelId = widgetManager.kernel.id;

const receivedWidgetModel: IReceivedWidgetModel = {};
const modelRegisteredHandler = (_: any, modelId: string) => {
if (receivedWidgetModel[modelId]) {
const { outputModel, executionModel } = receivedWidgetModel[modelId];
outputModel.add(executionModel);
widgetManager.removeRegisteredModel(modelId);
}
};
widgetManager.modelRegistered.connect(modelRegisteredHandler);
const wsUrl = getExecutionURL(kernelId);
const ws = new WebSocket(wsUrl);
ws.onmessage = async (msg) => {
const { action, payload }: IExecutionMessage = JSON.parse(msg.data);
switch (action) {
case 'execution_result': {
const result = handleExecutionResult({
payload,
rendermime,
widgetManager
});
if (result) {
Object.entries(result).forEach(([key, val]) => {
receivedWidgetModel[key] = val;
});
const cells = document.querySelectorAll('[cell-index]');
const cellsNumber = cells.length;
for (let cellIdx = 0; cellIdx < cellsNumber; cellIdx++) {
const el = cells.item(cellIdx);
const codeCell =
el.getElementsByClassName('jp-CodeCell').item(0) ||
el.getElementsByClassName('code_cell').item(0); // for classic template;
if (codeCell) {
const { area } = createOutputArea({ rendermime, parent: codeCell });
const source = `${cellIdx}`;
executeCode(source, area, widgetManager.kernel).then((future) => {
const skeleton = el
.getElementsByClassName('voila-skeleton-container')
.item(0);
if (skeleton) {
el.removeChild(skeleton);
}
const { cell_index, total_cell } = payload;
if (cell_index + 1 === total_cell) {
// Executed all cells
ws.close();
}

break;
}
case 'execution_error': {
console.error(`Execution error: ${payload.error}`);
break;
}
default:
break;
});
}
};
ws.onopen = () => {
ws.send(
JSON.stringify({ action: 'execute', payload: { kernel_id: kernelId } })
);
};
}
}
};
163 changes: 52 additions & 111 deletions packages/voila/src/plugins/outputs/tools.ts
Original file line number Diff line number Diff line change
@@ -1,109 +1,12 @@
import { IOutput } from '@jupyterlab/nbformat';
import { OutputAreaModel, SimplifiedOutputArea } from '@jupyterlab/outputarea';
import {
OutputArea,
OutputAreaModel,
SimplifiedOutputArea
} from '@jupyterlab/outputarea';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { Kernel, KernelMessage } from '@jupyterlab/services';
import { JSONObject } from '@lumino/coreutils';
import { Widget } from '@lumino/widgets';
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
import { type VoilaWidgetManager } from '@voila-dashboards/widgets-manager8/lib/manager';

/**
* Interface representing the structure of an execution result message.
*/
export interface IExecutionResultMessage {
action: 'execution_result';
payload: {
output_cell: { outputs: IOutput[] };
cell_index: number;
total_cell: number;
};
}

/**
* Interface representing the structure of an execution error message.
*/
export interface IExecutionErrorMessage {
action: 'execution_error';
payload: {
error: string;
};
}

/**
* Interface representing a received widget model
* containing output and execution models.
*/
export interface IReceivedWidgetModel {
[modelId: string]: {
outputModel: OutputAreaModel;
executionModel: IOutput;
};
}
export type IExecutionMessage =
| IExecutionResultMessage
| IExecutionErrorMessage;

export function getExecutionURL(kernelId?: string): string {
const wsUrl = PageConfig.getWsUrl();
return URLExt.join(wsUrl, 'voila/execution', kernelId ?? '');
}

/**
* Handles the execution result by rendering the output area and managing widget models.
*
* @param payload - The payload from the execution result message,
* including the output cell and cell index.
* @param rendermime - A render mime registry to render the output.
* @param widgetManager - The Voila widget manager to manage Jupyter widgets.
* @returns An object containing a model ID and its corresponding output and
* execution models if the model is not ready to be rendered, undefined otherwise.
*/
export function handleExecutionResult({
payload,
rendermime,
widgetManager
}: {
payload: IExecutionResultMessage['payload'];
rendermime: IRenderMimeRegistry;
widgetManager: VoilaWidgetManager;
}): IReceivedWidgetModel | undefined {
const { cell_index, output_cell } = payload;
const element = document.querySelector(`[cell-index="${cell_index + 1}"]`);
if (element) {
const skeleton = element
.getElementsByClassName('voila-skeleton-container')
.item(0);
if (skeleton) {
element.removeChild(skeleton);
}
const model = createOutputArea({ rendermime, parent: element });
if (!output_cell.outputs) {
return;
}
if (output_cell.outputs.length > 0) {
element.lastElementChild?.classList.remove(
'jp-mod-noOutputs',
'jp-mod-noInput'
);
}
const key = 'application/vnd.jupyter.widget-view+json';
for (const outputData of output_cell.outputs) {
const modelId = (outputData?.data as any)?.[key]?.model_id;
if (modelId) {
if (widgetManager.registeredModels.has(modelId)) {
model.add(outputData);
} else {
return {
[modelId]: {
outputModel: model,
executionModel: outputData
}
};
}
} else {
model.add(outputData);
}
}
}
}

/**
* Creates an output area model and attaches the output area to a specified parent element.
Expand All @@ -118,7 +21,7 @@ export function createOutputArea({
}: {
rendermime: IRenderMimeRegistry;
parent: Element;
}): OutputAreaModel {
}): { model: OutputAreaModel; area: SimplifiedOutputArea } {
const model = new OutputAreaModel({ trusted: true });
const area = new SimplifiedOutputArea({
model,
Expand All @@ -134,14 +37,19 @@ export function createOutputArea({
'jp-Cell-outputCollapser'
);
wrapper.appendChild(collapser);
parent.lastElementChild?.appendChild(wrapper);
parent.appendChild(wrapper);
area.node.classList.add('jp-Cell-outputArea');

area.node.style.display = 'flex';
area.node.style.flexDirection = 'column';

Widget.attach(area, wrapper);
return model;
area.outputLengthChanged.connect((_, number) => {
if (number) {
parent.classList.remove('jp-mod-noOutputs');
}
});
return { model, area };
}

export function createSkeleton(): void {
Expand All @@ -152,9 +60,42 @@ export function createSkeleton(): void {
</div>`;
const elements = document.querySelectorAll('[cell-index]');
elements.forEach((it) => {
const element = document.createElement('div');
element.className = 'voila-skeleton-container';
element.innerHTML = innerHtml;
it.appendChild(element);
const codeCell =
it.getElementsByClassName('jp-CodeCell').item(0) ||
it.getElementsByClassName('code_cell').item(0); // for classic template
if (codeCell) {
const element = document.createElement('div');
element.className = 'voila-skeleton-container';
element.innerHTML = innerHtml;
it.appendChild(element);
}
});
}

export async function executeCode(
code: string,
output: OutputArea,
kernel: Kernel.IKernelConnection | null | undefined,
metadata?: JSONObject
): Promise<KernelMessage.IExecuteReplyMsg | undefined> {
// Override the default for `stop_on_error`.
let stopOnError = false;
if (
metadata &&
Array.isArray(metadata.tags) &&
metadata.tags.indexOf('raises-exception') !== -1
) {
stopOnError = false;
}
const content: KernelMessage.IExecuteRequestMsg['content'] = {
code,
stop_on_error: stopOnError
};

if (!kernel) {
throw new Error('Session has no kernel.');
}
const future = kernel.requestExecute(content, false, metadata);
output.future = future;
return future.done;
}
6 changes: 3 additions & 3 deletions packages/widgets_manager7/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';
import { PageConfig } from '@jupyterlab/coreutils';
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
import { IRenderMime, IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { KernelAPI, ServerConnection } from '@jupyterlab/services';
import { KernelConnection } from '@jupyterlab/services/lib/kernel/default';
Expand Down Expand Up @@ -70,7 +70,7 @@ const widgetManager: JupyterFrontEndPlugin<IJupyterWidgetRegistry> = {
'The Voila Widget Manager plugin must be activated in a VoilaApp'
);
}
const baseUrl = PageConfig.getBaseUrl();
const baseUrl = URLExt.join(PageConfig.getBaseUrl(), 'voila/');
const kernelId = PageConfig.getOption('kernelId');
const serverSettings = ServerConnection.makeSettings({ baseUrl });

Expand Down Expand Up @@ -185,7 +185,7 @@ const widgetManager: JupyterFrontEndPlugin<IJupyterWidgetRegistry> = {
const xsrfToken = (matches && matches[1]) || '';
data.append('_xsrf', xsrfToken);
window.navigator.sendBeacon(
`${baseUrl}voila/api/shutdown/${kernel.id}`,
URLExt.join(baseUrl, `api/shutdown/${kernel.id}?_xsrf=${xsrfToken}`),
data
);
kernel.dispose();
Expand Down
7 changes: 4 additions & 3 deletions packages/widgets_manager8/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';
import { PageConfig } from '@jupyterlab/coreutils';
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { KernelAPI, ServerConnection } from '@jupyterlab/services';
import { KernelConnection } from '@jupyterlab/services/lib/kernel/default';
Expand All @@ -41,7 +41,8 @@ const widgetManager: JupyterFrontEndPlugin<IJupyterWidgetRegistry> = {
'The Voila Widget Manager plugin must be activated in a VoilaApp'
);
}
const baseUrl = PageConfig.getBaseUrl();
const baseUrl = URLExt.join(PageConfig.getBaseUrl(), 'voila/');

const kernelId = PageConfig.getOption('kernelId');
const serverSettings = ServerConnection.makeSettings({ baseUrl });

Expand Down Expand Up @@ -73,7 +74,7 @@ const widgetManager: JupyterFrontEndPlugin<IJupyterWidgetRegistry> = {
const xsrfToken = (matches && matches[1]) || '';
data.append('_xsrf', xsrfToken);
window.navigator.sendBeacon(
`${baseUrl}voila/api/shutdown/${kernel.id}`,
URLExt.join(baseUrl, `api/shutdown/${kernel.id}?_xsrf=${xsrfToken}`),
data
);
kernel.dispose();
Expand Down
18 changes: 5 additions & 13 deletions voila/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@

from .tornado.kernel_websocket_handler import VoilaKernelWebsocketHandler

from .tornado.execution_request_handler import ExecutionRequestHandler

from .tornado.contentshandler import VoilaContentsHandler

from urllib.parse import urljoin
Expand Down Expand Up @@ -653,6 +651,7 @@ def init_settings(self) -> Dict:
connection_dir=self.connection_dir,
kernel_spec_manager=self.kernel_spec_manager,
allowed_message_types=[
"execute_request",
"comm_open",
"comm_close",
"comm_msg",
Expand Down Expand Up @@ -715,6 +714,7 @@ def init_settings(self) -> Dict:
config_manager=self.config_manager,
mathjax_config=self.mathjax_config,
mathjax_url=self.mathjax_url,
static_path=self.static_root,
)
settings[self.name] = self # Why???

Expand All @@ -738,13 +738,14 @@ def init_handlers(self) -> List:
[
(
url_path_join(
self.server_url, r"/api/kernels/%s" % _kernel_id_regex
self.server_url, r"/voila/api/kernels/%s" % _kernel_id_regex
),
KernelHandler,
),
(
url_path_join(
self.server_url, r"/api/kernels/%s/channels" % _kernel_id_regex
self.server_url,
r"/voila/api/kernels/%s/channels" % _kernel_id_regex,
),
VoilaKernelWebsocketHandler,
),
Expand Down Expand Up @@ -788,15 +789,6 @@ def init_handlers(self) -> List:
RequestInfoSocketHandler,
)
)
if self.voila_configuration.progressive_rendering:
handlers.append(
(
url_path_join(
self.server_url, r"/voila/execution/%s" % _kernel_id_regex
),
ExecutionRequestHandler,
)
)
# Serving JupyterLab extensions
handlers.append(
(
Expand Down
Loading
Loading