diff --git a/packages/voila/src/app.ts b/packages/voila/src/app.ts index f06399f2e..3db7d0851 100644 --- a/packages/voila/src/app.ts +++ b/packages/voila/src/app.ts @@ -10,7 +10,7 @@ import { PageConfig } from '@jupyterlab/coreutils'; import { IRenderMime } from '@jupyterlab/rendermime'; -import { KernelWidgetManager } from '@jupyter-widgets/jupyterlab-manager'; +import { VoilaWidgetManager } from './plugins/widget'; import { IShell, VoilaShell } from './shell'; @@ -121,23 +121,23 @@ export class VoilaApp extends JupyterFrontEnd { /** * A promise that resolves when the Voila Widget Manager is created */ - get widgetManagerPromise(): PromiseDelegate { + get widgetManagerPromise(): PromiseDelegate { return this._widgetManagerPromise; } - set widgetManager(manager: KernelWidgetManager | null) { + set widgetManager(manager: VoilaWidgetManager | null) { this._widgetManager = manager; if (this._widgetManager) { this._widgetManagerPromise.resolve(this._widgetManager); } } - get widgetManager(): KernelWidgetManager | null { + get widgetManager(): VoilaWidgetManager | null { return this._widgetManager; } - protected _widgetManager: KernelWidgetManager | null = null; - protected _widgetManagerPromise = new PromiseDelegate(); + protected _widgetManager: VoilaWidgetManager | null = null; + protected _widgetManagerPromise = new PromiseDelegate(); } /** diff --git a/packages/voila/src/plugins/widget/index.ts b/packages/voila/src/plugins/widget/index.ts index 505ad8f3c..8ad1402cf 100644 --- a/packages/voila/src/plugins/widget/index.ts +++ b/packages/voila/src/plugins/widget/index.ts @@ -6,39 +6,26 @@ * * * The full license is in the file LICENSE, distributed with this software. * ****************************************************************************/ - +import { + IJupyterWidgetRegistry, + IWidgetRegistryData +} from '@jupyter-widgets/base'; +import { WidgetRenderer } from '@jupyter-widgets/jupyterlab-manager'; import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; - import { PageConfig } from '@jupyterlab/coreutils'; - +import { IOutput } from '@jupyterlab/nbformat'; +import { OutputAreaModel, SimplifiedOutputArea } from '@jupyterlab/outputarea'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; - import { KernelAPI, ServerConnection } from '@jupyterlab/services'; - import { KernelConnection } from '@jupyterlab/services/lib/kernel/default'; - -import { - WidgetRenderer, - KernelWidgetManager -} from '@jupyter-widgets/jupyterlab-manager'; - -import { - IJupyterWidgetRegistry, - IWidgetRegistryData -} from '@jupyter-widgets/base'; +import { Widget } from '@lumino/widgets'; import { VoilaApp } from '../../app'; - -import { Widget } from '@lumino/widgets'; +import { VoilaWidgetManager } from './manager'; import { RenderedCells } from './renderedcells'; -import { - // OutputArea, - OutputAreaModel, - SimplifiedOutputArea -} from '@jupyterlab/outputarea'; const WIDGET_MIMETYPE = 'application/vnd.jupyter.widget-view+json'; @@ -72,7 +59,7 @@ export const widgetManager: JupyterFrontEndPlugin = { }; } const kernel = new KernelConnection({ model, serverSettings }); - const manager = new KernelWidgetManager(kernel, rendermime); + const manager = new VoilaWidgetManager(kernel, rendermime); app.widgetManager = manager; rendermime.removeMimeType(WIDGET_MIMETYPE); @@ -128,111 +115,169 @@ export const renderOutputsPlugin: JupyterFrontEndPlugin = { rendermime.latexTypesetter?.typeset(md as HTMLElement); }); // Render code cell - // const cellOutputs = document.body.querySelectorAll( - // 'script[type="application/vnd.voila.cell-output+json"]' - // ); - // cellOutputs.forEach(async (cellOutput) => { - // const model = JSON.parse(cellOutput.innerHTML); - - // const mimeType = rendermime.preferredMimeType(model.data, 'any'); - - // if (!mimeType) { - // return null; - // } - // const output = rendermime.createRenderer(mimeType); - // output.renderModel(model).catch((error) => { - // // Manually append error message to output - // const pre = document.createElement('pre'); - // pre.textContent = `Javascript Error: ${error.message}`; - // output.node.appendChild(pre); - - // // Remove mime-type-specific CSS classes - // pre.className = 'lm-Widget jp-RenderedText'; - // pre.setAttribute('data-mime-type', 'application/vnd.jupyter.stderr'); - // }); - - // output.addClass('jp-OutputArea-output'); - - // if (cellOutput.parentElement) { - // const container = cellOutput.parentElement; - - // container.removeChild(cellOutput); - - // // Attach output - // Widget.attach(output, container); - // } - // }); + const cellOutputs = document.body.querySelectorAll( + 'script[type="application/vnd.voila.cell-output+json"]' + ); + cellOutputs.forEach(async (cellOutput) => { + const model = JSON.parse(cellOutput.innerHTML); + + const mimeType = rendermime.preferredMimeType(model.data, 'any'); + + if (!mimeType) { + return null; + } + const output = rendermime.createRenderer(mimeType); + output.renderModel(model).catch((error) => { + // Manually append error message to output + const pre = document.createElement('pre'); + pre.textContent = `Javascript Error: ${error.message}`; + output.node.appendChild(pre); + + // Remove mime-type-specific CSS classes + pre.className = 'lm-Widget jp-RenderedText'; + pre.setAttribute('data-mime-type', 'application/vnd.jupyter.stderr'); + }); + + output.addClass('jp-OutputArea-output'); + + if (cellOutput.parentElement) { + const container = cellOutput.parentElement; + + container.removeChild(cellOutput); + + // Attach output + Widget.attach(output, container); + } + }); + + const node = document.getElementById('rendered_cells'); + if (node) { + const cells = new RenderedCells({ node }); + app.shell.add(cells, 'main'); + } + } +}; + +function createOutputArea({ + rendermime, + parent +}: { + rendermime: IRenderMimeRegistry; + parent: Element; +}): OutputAreaModel { + const model = new OutputAreaModel({ trusted: true }); + const area = new SimplifiedOutputArea({ + model, + rendermime + }); + + const wrapper = document.createElement('div'); + wrapper.classList.add('jp-Cell-outputWrapper'); + const collapser = document.createElement('div'); + collapser.classList.add( + 'jp-Collapser', + 'jp-OutputCollapser', + 'jp-Cell-outputCollapser' + ); + wrapper.appendChild(collapser); + parent.lastElementChild?.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; +} + +/** + * The plugin that renders outputs. + */ +export const renderOutputsProgressivelyPlugin: JupyterFrontEndPlugin = { + id: '@voila-dashboards/voila:render-outputs-progressively', + autoStart: true, + requires: [IRenderMimeRegistry, IJupyterWidgetRegistry], + activate: async ( + app: JupyterFrontEnd, + rendermime: IRenderMimeRegistry + ): Promise => { + const widgetManager = (app as VoilaApp).widgetManager; + if (!widgetManager) { + return; + } + const kernelId = (app as VoilaApp).widgetManager?.kernel.id; - console.log('using kernel', kernelId); + + const receivedWidgetModel: { + [modelId: string]: { + outputModel: OutputAreaModel; + executionModel: IOutput; + }; + } = {}; + const modelRegisteredHandler = (_: VoilaWidgetManager, modelId: string) => { + if (receivedWidgetModel[modelId]) { + const { outputModel, executionModel } = receivedWidgetModel[modelId]; + console.log('render later'); + outputModel.add(executionModel); + widgetManager.removeRegisteredModel(modelId); + } + }; + widgetManager.modelRegistered.connect(modelRegisteredHandler); + const ws = new WebSocket(`ws://localhost:8866/voila/execution/${kernelId}`); - ws.onmessage = (msg) => { + + ws.onmessage = async (msg) => { const { action, payload } = JSON.parse(msg.data); if (action === 'execution_result') { - const { cell_index, output_cell, request_kernel_id } = payload; + const { cell_index, output_cell } = payload; const element = document.querySelector( `[cell-index="${cell_index + 1}"]` ); if (element) { - const model = new OutputAreaModel({ trusted: true }); - const area = new SimplifiedOutputArea({ - model, - rendermime - }); - - const wrapper = document.createElement('div'); - wrapper.classList.add('jp-Cell-outputWrapper'); - const collapser = document.createElement('div'); - collapser.classList.add( - 'jp-Collapser', - 'jp-OutputCollapser', - 'jp-Cell-outputCollapser' - ); - wrapper.appendChild(collapser); - element.lastElementChild?.appendChild(wrapper); - - area.node.classList.add('jp-Cell-outputArea'); - - // Why do we need this? Are we missing a CSS class? - area.node.style.display = 'flex'; - area.node.style.flexDirection = 'column'; - - Widget.attach(area, wrapper); const skeleton = element .getElementsByClassName('voila-skeleton-container') .item(0); if (skeleton) { element.removeChild(skeleton); } - const outputData = output_cell.outputs[0]; - if (outputData) { - console.log( - 'adding', - outputData, - 'request_kernel_id', - request_kernel_id, - 'kernelId', - kernelId - ); + const model = createOutputArea({ rendermime, parent: element }); + + if (output_cell.outputs.length > 0) { element.lastElementChild?.classList.remove( 'jp-mod-noOutputs', 'jp-mod-noInput' ); - model.add(outputData); + } + for (const outputData of output_cell.outputs) { + const modelId = + outputData?.data?.['application/vnd.jupyter.widget-view+json'] + ?.model_id; + if (modelId) { + if (widgetManager.has_model(modelId)) { + console.log('render immediatly'); + model.add(outputData); + } else { + receivedWidgetModel[modelId] = { + outputModel: model, + executionModel: outputData + }; + } + } else { + model.add(outputData); + } } } + } else if (action === 'finished') { + widgetManager.modelRegistered.disconnect(modelRegisteredHandler); + ws.close(); } }; ws.onopen = () => { - console.log('opened'); ws.send( JSON.stringify({ action: 'execute', payload: { kernel_id: kernelId } }) ); }; - - const node = document.getElementById('rendered_cells'); - if (node) { - const cells = new RenderedCells({ node }); - app.shell.add(cells, 'main'); - } } }; + +export { VoilaWidgetManager }; diff --git a/packages/voila/src/plugins/widget/manager.ts b/packages/voila/src/plugins/widget/manager.ts new file mode 100644 index 000000000..32132ca7c --- /dev/null +++ b/packages/voila/src/plugins/widget/manager.ts @@ -0,0 +1,22 @@ +import { WidgetModel } from '@jupyter-widgets/base'; +import { KernelWidgetManager } from '@jupyter-widgets/jupyterlab-manager'; +import { ISignal, Signal } from '@lumino/signaling'; + +export class VoilaWidgetManager extends KernelWidgetManager { + register_model(model_id: string, modelPromise: Promise): void { + super.register_model(model_id, modelPromise); + this._registeredModels.add(model_id); + this._modelRegistered.emit(model_id); + } + get registeredModels(): ReadonlySet { + return this._registeredModels; + } + get modelRegistered(): ISignal { + return this._modelRegistered; + } + removeRegisteredModel(modelId: string) { + this._registeredModels.delete(modelId); + } + private _modelRegistered = new Signal(this); + private _registeredModels = new Set(); +} diff --git a/packages/voila/src/voilaplugins.ts b/packages/voila/src/voilaplugins.ts index 7e52e7113..dfcb67526 100644 --- a/packages/voila/src/voilaplugins.ts +++ b/packages/voila/src/voilaplugins.ts @@ -12,6 +12,7 @@ import { pathsPlugin } from './plugins/path'; import { translatorPlugin } from './plugins/translator'; import { renderOutputsPlugin, widgetManager } from './plugins/widget'; import { themePlugin, themesManagerPlugin } from './plugins/themes'; +import { renderOutputsProgressivelyPlugin } from './plugins/widget/index'; /** * Export the plugins as default. @@ -21,6 +22,7 @@ const plugins: JupyterFrontEndPlugin[] = [ translatorPlugin, widgetManager, renderOutputsPlugin, + renderOutputsProgressivelyPlugin, themesManagerPlugin, themePlugin ]; @@ -32,6 +34,7 @@ export { translatorPlugin, widgetManager, renderOutputsPlugin, + renderOutputsProgressivelyPlugin, themesManagerPlugin, themePlugin }; diff --git a/voila/execution_request_handler.py b/voila/execution_request_handler.py index 9bacec16f..8d492da5c 100644 --- a/voila/execution_request_handler.py +++ b/voila/execution_request_handler.py @@ -1,6 +1,5 @@ import json -import logging -from typing import Awaitable, Dict +from typing import Awaitable from jupyter_server.base.handlers import JupyterHandler from tornado.websocket import WebSocketHandler from jupyter_server.base.websocket import WebSocketMixin @@ -19,7 +18,6 @@ class ExecutionRequestHandler(WebSocketMixin, WebSocketHandler, JupyterHandler): def initialize(self, **kwargs): super().initialize() - print("cccc", self.kernel_manager) def open(self, kernel_id: str) -> None: """Create a new websocket connection, this connection is @@ -30,7 +28,6 @@ def open(self, kernel_id: str) -> None: the websocket connection. """ super().open() - print("self", self) self._kernel_id = kernel_id ExecutionRequestHandler._kernels[kernel_id] = self self.write_message({"action": "initialized", "payload": {}}) @@ -43,20 +40,20 @@ async def on_message(self, message_str: str | bytes) -> Awaitable[None] | None: payload = message.get("payload", {}) if action == "execute": request_kernel_id = payload.get("kernel_id") - print("RECEIVEDDDDDDDD", request_kernel_id, self._kernel_id) kernel_future = self.kernel_manager.get_kernel(self._kernel_id) km = await ensure_async(kernel_future) execution_data = self._execution_data.get(self._kernel_id) nb = execution_data["nb"] - executor = VoilaExecutor( + self._executor = executor = VoilaExecutor( nb, km=km, config=execution_data["config"], show_tracebacks=execution_data["show_tracebacks"], ) executor.kc = await executor.async_start_new_kernel_client() + for cell_idx, input_cell in enumerate(nb.cells): try: output_cell = await executor.execute_cell( @@ -64,7 +61,7 @@ async def on_message(self, message_str: str | bytes) -> Awaitable[None] | None: ) except TimeoutError: output_cell = input_cell - break + except CellExecutionError: self.log.exception( "Error at server while executing cell: %r", input_cell @@ -73,7 +70,7 @@ async def on_message(self, message_str: str | bytes) -> Awaitable[None] | None: strip_code_cell_warnings(input_cell) executor.strip_code_cell_errors(input_cell) output_cell = input_cell - break + except Exception as e: self.log.exception( "Error at server while executing cell: %r", input_cell @@ -113,20 +110,5 @@ async def on_message(self, message_str: str | bytes) -> Awaitable[None] | None: ) def on_close(self) -> None: - for k_id, waiter in ExecutionRequestHandler._kernels.items(): - if waiter == self: - break - del ExecutionRequestHandler._kernels[k_id] - - @classmethod - def send_updates(cls: "ExecutionRequestHandler", msg: Dict) -> None: - kernel_id = msg["kernel_id"] - payload = msg["payload"] - waiter = cls._kernels.get(kernel_id, None) - if waiter is not None: - try: - waiter.write_message(payload) - except Exception: - logging.error("Error sending message", exc_info=True) - else: - cls._cache[kernel_id] = payload + if self._executor: + del self._executor.kc diff --git a/voila/notebook_renderer.py b/voila/notebook_renderer.py index 9f6231104..a2d39d4c8 100644 --- a/voila/notebook_renderer.py +++ b/voila/notebook_renderer.py @@ -239,7 +239,6 @@ async def _jinja_kernel_start(self, nb, kernel_id, kernel_future): return kernel_id async def _jinja_notebook_execute(self, nb, kernel_id): - print("VVVVVVVVVVVVVVVV _jinja_notebook_execute") result = await self.executor.async_execute(cleanup_kc=False) # we modify the notebook in place, since the nb variable cannot be # reassigned it seems in jinja2 e.g. if we do {% with nb = notebook_execute(nb, kernel_id) %} @@ -251,7 +250,6 @@ async def _jinja_notebook_execute(self, nb, kernel_id): async def _jinja_cell_generator(self, nb, kernel_id): """Generator that will execute a single notebook cell at a time""" - print("VVVVVVVVVVVVVVVV _jinja_cell_generator") nb, _ = ClearOutputPreprocessor().preprocess( nb, {"metadata": {"path": self.cwd}} )