Skip to content

Commit

Permalink
Create render plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
trungleduc committed Sep 27, 2024
1 parent bc8b5e7 commit acdece6
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 135 deletions.
12 changes: 6 additions & 6 deletions packages/voila/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -121,23 +121,23 @@ export class VoilaApp extends JupyterFrontEnd<IShell> {
/**
* A promise that resolves when the Voila Widget Manager is created
*/
get widgetManagerPromise(): PromiseDelegate<KernelWidgetManager> {
get widgetManagerPromise(): PromiseDelegate<VoilaWidgetManager> {
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<KernelWidgetManager>();
protected _widgetManager: VoilaWidgetManager | null = null;
protected _widgetManagerPromise = new PromiseDelegate<VoilaWidgetManager>();
}

/**
Expand Down
249 changes: 147 additions & 102 deletions packages/voila/src/plugins/widget/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -72,7 +59,7 @@ export const widgetManager: JupyterFrontEndPlugin<IJupyterWidgetRegistry> = {
};
}
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);
Expand Down Expand Up @@ -128,111 +115,169 @@ export const renderOutputsPlugin: JupyterFrontEndPlugin<void> = {
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<void> = {
id: '@voila-dashboards/voila:render-outputs-progressively',
autoStart: true,
requires: [IRenderMimeRegistry, IJupyterWidgetRegistry],
activate: async (
app: JupyterFrontEnd,
rendermime: IRenderMimeRegistry
): Promise<void> => {
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 };
22 changes: 22 additions & 0 deletions packages/voila/src/plugins/widget/manager.ts
Original file line number Diff line number Diff line change
@@ -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<WidgetModel>): void {
super.register_model(model_id, modelPromise);
this._registeredModels.add(model_id);
this._modelRegistered.emit(model_id);
}
get registeredModels(): ReadonlySet<string> {
return this._registeredModels;
}
get modelRegistered(): ISignal<VoilaWidgetManager, string> {
return this._modelRegistered;
}
removeRegisteredModel(modelId: string) {
this._registeredModels.delete(modelId);
}
private _modelRegistered = new Signal<VoilaWidgetManager, string>(this);
private _registeredModels = new Set<string>();
}
3 changes: 3 additions & 0 deletions packages/voila/src/voilaplugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -21,6 +22,7 @@ const plugins: JupyterFrontEndPlugin<any>[] = [
translatorPlugin,
widgetManager,
renderOutputsPlugin,
renderOutputsProgressivelyPlugin,
themesManagerPlugin,
themePlugin
];
Expand All @@ -32,6 +34,7 @@ export {
translatorPlugin,
widgetManager,
renderOutputsPlugin,
renderOutputsProgressivelyPlugin,
themesManagerPlugin,
themePlugin
};
Loading

0 comments on commit acdece6

Please sign in to comment.